diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..58c0361 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,84 @@ +name: build module + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build-module: + name: Run build and upload artifacts + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 + + - name: Install Modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module ModuleBuilder,PSScriptAnalyzer,EZOut + Install-Module Pester -MinimumVersion 5.5.0 + + - name: Run build + shell: pwsh + run: | + $Version = git describe --tags --abbrev=0 + Write-Host "Version : $Version" + .\tools\build.ps1 -Version $Version + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: psnotes-build + path: bin + + - name: Build Tests + shell: pwsh + run: .\tools\tests-build.ps1 + + - name: Build Report + uses: dorny/test-reporter@v2 + if: ${{ !cancelled() }} + with: + name: Build Tests + path: 'bin/*.TestResults.xml' + reporter: java-junit + + test-module: + needs: build-module + name: Test Module + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: bin + pattern: psnotes-build + merge-multiple: true + - run: ls -R bin + + - name: Install Modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module Pester -MinimumVersion 5.5.0 + Install-Module PSScriptAnalyzer + + - name: Unit Tests + shell: pwsh + run: .\tools\tests.ps1 + + - name: Test Report + uses: dorny/test-reporter@v2 + if: ${{ !cancelled() }} + with: + name: Pester Tests + path: 'bin/*.TestResults.xml' + reporter: java-junit \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6ff30a..24573b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -Publish/APIKey.json -Publish/PSNotes/ -*.code-workspace \ No newline at end of file +*.code-workspace +bin/ +Documentation/demos/* +.vscode/* +*/APIKey.json diff --git a/Documentation/Commands.MD b/Documentation/Commands.MD deleted file mode 100644 index 8090339..0000000 --- a/Documentation/Commands.MD +++ /dev/null @@ -1,25 +0,0 @@ -# Manage Notes -These commands allow you to create, update, and delete different notes. -* [New-PSNote](New-PSNote.MD) - Use to add or update a PSNote object -* [Remove-PSNote](Remove-PSNote.MD) - Use to remove a Note from you personal store -* [Set-PSNote](Set-PSNote.MD) - Use to add or update a PSNote object - -# Working with Notes -The commands below provide multiple different ways for you to use notes outside of the default alias - -* [Get-PSNote](Get-PSNote.MD) - Use to search for or list the different PSNotes -* [Copy-PSNote](Copy-PSNote.MD) - Use to display a list of notes in a selectable menu so you can choose which to copy to your clipboard -* [Get-PSNoteAlias](Get-PSNoteAlias.MD) - Use display snippet and copy to clipboard using an Alias -* [Invoke-PSNote](Invoke-PSNote.MD) - Use to display a list of notes in a selectable menu so you can choose which to run - -# Sharing -You can export and import notes to share with other or copy to a different machine. -* [Import-PSNote](Import-PSNote.MD) - Use to import a PSNotes JSON fiile -* [Export-PSNote](Export-PSNote.MD) - Use to export your PSNotes to copy to another machine or share with others - -# Splatting Commands -Splatting is a much cleaner and safer way to shorten command lines without needing to use backtick. These cmdlets will help you automatically format commands for splatting. - -* [ConvertTo-Splatting](ConvertTo-Splatting.MD) - Use to convert an existing PowerShell command to splatting -* [Get-CommandSplatting](Get-CommandSplatting.MD) - Use to output the parameters for a command in splatting format - diff --git a/Documentation/Commands.md b/Documentation/Commands.md new file mode 100644 index 0000000..b73eeb7 --- /dev/null +++ b/Documentation/Commands.md @@ -0,0 +1,84 @@ +--- +document type: module +Help Version: 1.0.0.0 +HelpInfoUri: +Locale: en-US +Module Guid: 040757f4-ee7b-4e93-9883-f6a1930b6966 +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: PSNotes Module +--- + +# PSNotes Module + +## Description + +PSNotes is a PowerShell module that provides a structured, versioned snippet and script library for reusable automation patterns. Create notes with aliases, tags, and metadata to quickly execute, copy, or preview commands. Organize notes into local or remote catalogs, search by name, tag, details, or snippet content, and turn frequently used automation into first-class commands. + +## PSNotes + +### [ConvertTo-Splatting](ConvertTo-Splatting.md) + +Converts an existing PowerShell command into a splatting hashtable and splatted command. + +### [Export-PSNote](Export-PSNote.md) + +Exports PSNotes to a JSON file for backup or sharing. + +### [Get-CommandSplatting](Get-CommandSplatting.md) + +Generates a splatting template for a PowerShell command. + +### [Get-PSNote](Get-PSNote.md) + +Retrieves PSNotes from the note store by listing or searching. + +### [Get-PSNoteAlias](Get-PSNoteAlias.md) + +Resolves a PSNote by alias and outputs, copies, or executes its content. + +### [Get-PSNoteMenu](Get-PSNoteMenu.md) + +Displays an interactive, paged console menu for browsing and selecting PSNotes. + +### [Get-RemoteCatalog](Get-RemoteCatalog.md) + +Gets remote catalogs registered with PSNotes. + +### [Import-PSNote](Import-PSNote.md) + +Imports PSNotes from a JSON export file into the local note store. + +### [Import-RemoteCatalog](Import-RemoteCatalog.md) + +Registers a remote PSNotes catalog or imports it as a local catalog. + +### [Initialize-PSNoteStore](Initialize-PSNoteStore.md) + +Initializes the PSNotes store and required supporting files. + +### [Move-PSNote](Move-PSNote.md) + +Moves one or more PSNotes to a different catalog. + +### [New-PSNote](New-PSNote.md) + +Creates a new PSNote for storing reusable snippets or script references. + +### [Remove-PSNote](Remove-PSNote.md) + +Removes one or more PSNotes from the note store. + +### [Remove-RemoteCatalog](Remove-RemoteCatalog.md) + +Removes a remote catalog registration from PSNotes. + +### [Set-PSNote](Set-PSNote.md) + +Updates an existing PSNote or creates it if it does not already exist. + +### [Update-PSNoteStore](Update-PSNoteStore.md) + +Updates PSNotes catalogs to the latest format. + diff --git a/Documentation/ConvertTo-Splatting.MD b/Documentation/ConvertTo-Splatting.MD deleted file mode 100644 index 7d3f081..0000000 --- a/Documentation/ConvertTo-Splatting.MD +++ /dev/null @@ -1,96 +0,0 @@ -# ConvertTo-Splatting - - -## ConvertTo-Splatting - -![ConvertTo-Splatting01](media/ConvertTo-Splatting01.png) - -### Synopsis -Use to convert an existing PowerShell command to splatting -### Description -Splatting is a much cleaner and safer way to shorten command lines without needing to use backtick. -This function excepts any command as a string or a scriptblock and will convert the existing parameters -to a hashtable and output the fully splatted command for you. - -### Syntax -```powershell -ConvertTo-Splatting [[-Command] ] [] - -ConvertTo-Splatting [[-ScriptBlock] ] [] -``` -### Parameters -#### Command <String> - The command string you want to convert to using splatting - - Required? false - Position? 1 - Default value - Accept pipeline input? false - Accept wildcard characters? false -#### ScriptBlock <ScriptBlock> - The command scriptblock you want to convert to using splatting - - Required? false - Position? 1 - Default value - Accept pipeline input? false - Accept wildcard characters? false -### Examples -#### Example 1: Converts the string splatme to splatting -```powershell -$splatme = @' -Set-AzVMExtension -ExtensionName "MicrosoftMonitoringAgent" -ResourceGroupName "rg-xxxx" -VMName "vm-xxxx" -Publisher "Microsoft.EnterpriseCloud.Monitoring" -ExtensionType "MicrosoftMonitoringAgent" -TypeHandlerVersion "1.0" -Settings @{"workspaceId" = -"xxxx" } -ProtectedSettings @{"workspaceKey" = "xxxx"} -Location "uksouth" -'@ -ConvertTo-Splatting $splatme -``` -###### Output -``` -$SetAzVMExtensionParam = @{ - ExtensionName = "MicrosoftMonitoringAgent" - ResourceGroupName = "rg-xxxx" - VMName = "vm-xxxx" - Publisher = "Microsoft.EnterpriseCloud.Monitoring" - ExtensionType = "MicrosoftMonitoringAgent" - TypeHandlerVersion = "1.0" - Settings = @{ "workspaceId" = "xxxx" } - ProtectedSettings = @{ "workspaceKey" = "xxxx" } - Location = "uksouth" -} -Set-AzVMExtension @SetAzVMExtensionParam -``` -#### Example 2: Converts the scriptblock splatme to splatting -```powershell -$splatme = { -Copy-Item -Path "test.txt" -Destination "test2.txt" -WhatIf -} -ConvertTo-Splatting $splatme -``` -###### Output -``` -$CopyItemParam = @{ - Path = "test.txt" - Destination = "test2.txt" - WhatIf = $true -} -Copy-Item @CopyItemParam -``` -#### Example 3: Removed backticks and converts the scriptblock splatme to splatting -```powershell -$splatme = { -Get-AzVM ` - -ResourceGroupName "ResourceGroup11" ` - -Name "VirtualMachine07" ` - -Status -} -ConvertTo-Splatting $splatme -``` -###### Output -``` -$GetAzVMParam = @{ - ResourceGroupName = "ResourceGroup11" - Name = "VirtualMachine07" - Status = $true -} -Get-AzVM @GetAzVMParam -``` diff --git a/Documentation/ConvertTo-Splatting.md b/Documentation/ConvertTo-Splatting.md new file mode 100644 index 0000000..57a775f --- /dev/null +++ b/Documentation/ConvertTo-Splatting.md @@ -0,0 +1,157 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: ConvertTo-Splatting +--- + +# ConvertTo-Splatting + +## SYNOPSIS + +Converts an existing PowerShell command into a splatting hashtable and splatted command. + +## SYNTAX + +### string + +``` +ConvertTo-Splatting [[-Command] ] [] +``` + +### scriptblock + +``` +ConvertTo-Splatting [[-ScriptBlock] ] [] +``` + +## DESCRIPTION + +ConvertTo-Splatting takes a PowerShell command provided as a string or script block and rewrites it +into a splatting-friendly format. +It parses the command, identifies the command name and parameters, +and produces: + +- A hashtable assignment (for example, $GetItemParam = @{ ... +}) +- A command invocation that uses splatting (for example, Get-Item @GetItemParam) + +This is useful for refactoring long command lines into a clearer, more maintainable structure, and for +turning backtick-continued commands into a single normalized form. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +$splatme = @' +Set-AzVMExtension -ExtensionName "MicrosoftMonitoringAgent" -ResourceGroupName "rg-xxxx" -VMName "vm-xxxx" ` + -Publisher "Microsoft.EnterpriseCloud.Monitoring" -ExtensionType "MicrosoftMonitoringAgent" ` + -TypeHandlerVersion "1.0" -Settings @{"workspaceId" = "xxxx"} ` + -ProtectedSettings @{"workspaceKey" = "xxxx"} -Location "uksouth" +'@ +ConvertTo-Splatting $splatme +``` + +Creates a parameter hashtable and a splatted Set-AzVMExtension call. + +### EXAMPLE 2 + +```powershell +$splatme = { Copy-Item -Path "test.txt" -Destination "test2.txt" -WhatIf } +ConvertTo-Splatting $splatme +``` + +Converts a script block command into a hashtable and a splatted Copy-Item call. +Switch parameters are +represented as $true. + +### EXAMPLE 3 + +```powershell +$splatme = { + Get-AzVM ` + -ResourceGroupName "ResourceGroup11" ` + -Name "VirtualMachine07" ` + -Status +} +ConvertTo-Splatting $splatme +``` + +Normalizes backtick line continuations and converts the command to splatting. + +## PARAMETERS + +### -Command + +The command text to convert to splatting. +Provide a full command line as a string. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: string + Position: 0 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -ScriptBlock + +The command to convert to splatting, provided as a script block. +The script block is converted to text +and normalized prior to parsing. + +```yaml +Type: System.Management.Automation.ScriptBlock +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: scriptblock + Position: 0 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +- If the command starts with a variable assignment, the variable(s) are preserved as part of the parsed command. +- The hashtable variable name is derived from the command name (for example, Get-Item -> $GetItemParam). +If that + name conflicts with an existing constant variable, a fallback name is used. +- For background on splatting, see: + about_Splatting - https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_splatting + + +## RELATED LINKS + + diff --git a/Documentation/Copy-PSNote.MD b/Documentation/Copy-PSNote.MD deleted file mode 100644 index 6c2be0d..0000000 Binary files a/Documentation/Copy-PSNote.MD and /dev/null differ diff --git a/Documentation/Export-PSNote.MD b/Documentation/Export-PSNote.MD deleted file mode 100644 index 163e549..0000000 Binary files a/Documentation/Export-PSNote.MD and /dev/null differ diff --git a/Documentation/Export-PSNote.md b/Documentation/Export-PSNote.md new file mode 100644 index 0000000..bd91b06 --- /dev/null +++ b/Documentation/Export-PSNote.md @@ -0,0 +1,201 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Export-PSNote +--- + +# Export-PSNote + +## SYNOPSIS + +Exports PSNotes to a JSON file for backup or sharing. + +## SYNTAX + +### Note (Default) + +``` +Export-PSNote -NoteObject -Path [-Force] [] +``` + +### Catalog + +``` +Export-PSNote -Catalog -Path [-Force] [] +``` + +## DESCRIPTION + +Export-PSNote serializes notes from the PSNotes store into a JSON file. +You can export the entire +store or a filtered subset of notes (for example, by name, alias, tag, or catalog depending on +the parameter set in use). + +The exported file is designed to round-trip with Import-PSNote and can be used for backups, +migration to another machine, or sharing curated note collections. + +If the destination file already exists, use -Force to overwrite it. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Export-PSNote -Catalog 'Default' -Path C:\Export\MyPSNotes.json +``` + +Exports all notes from the 'Default' catalog to a JSON file. + +### EXAMPLE 2 + +```powershell +Get-PSNote -Tag 'AD' | Export-PSNote -Path C:\Export\SharedADNotes.json +``` + +Exports all notes with the tag 'AD' to the file SharedADNotes.json. + +### EXAMPLE 3 + +```powershell +Get-PSNote -Name 'Cred*' -Catalog 'Work' | Export-PSNote -Path C:\Export\WorkCreds.json +``` + +Exports notes that match the name pattern from the 'Work' catalog. + +### EXAMPLE 4 + +```powershell +Get-PSNote -SearchString 'token' | Export-PSNote -Path C:\Export\TokenNotes.json +``` + +Exports notes that match a search string. + +### EXAMPLE 5 + +```powershell +Export-PSNote -Catalog 'Personal' -Path C:\Export\PersonalNotes.json -Force +``` + +Exports the 'Personal' catalog and overwrites the file if it exists. + +## PARAMETERS + +### -Catalog + +The catalog name to export. +When specified, all notes from that catalog are exported. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Catalog + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Force + +Overwrite the output file if it already exists. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -NoteObject + +The PSNote objects you want to export. +Use Get-PSNote to build the object and pass it to the parameter +or use the pipeline to pass them in. + +```yaml +Type: PSNote[] +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Note + Position: Named + IsRequired: true + ValueFromPipeline: true + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Path + +The path to the PSNotes JSON file to export to. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### PSNote[] + + +## OUTPUTS + +### System.Object + + +## NOTES + +- The exported JSON file is intended for use with Import-PSNote. +- Use -Force to overwrite an existing file. +- See also: Import-PSNote, Get-PSNote + + +## RELATED LINKS + + diff --git a/Documentation/Get-CommandSplatting.MD b/Documentation/Get-CommandSplatting.MD deleted file mode 100644 index 0e6ba20..0000000 --- a/Documentation/Get-CommandSplatting.MD +++ /dev/null @@ -1,186 +0,0 @@ -# Get-CommandSplatting - - -## Get-CommandSplatting - -![Get-CommandSplatting01](media/Get-CommandSplatting01.png) - -### Synopsis -Use to output the parameters for a command in splatting format -### Description -Use to output the parameters for a command in splatting format - -### Syntax -```powershell -Get-CommandSplatting [-Command] [[-ParameterSet] ] [-IncludeCommon] [-Copy] [] - -Get-CommandSplatting [-Command] [-ListParameterSets] [-IncludeCommon] [-Copy] [] - -Get-CommandSplatting [-Command] [-All] [-IncludeCommon] [-Copy] [] -``` -### Parameters -#### Command <String> - The command to get the parameters for - - Required? true - Position? 1 - Default value - Accept pipeline input? false - Accept wildcard characters? false -#### ParameterSet <String> - Use to specify a specific parameter set. Use the -ListParameterSets to get a quick - view of all the different Parameter Set names. - - Required? false - Position? 2 - Default value - Accept pipeline input? false - Accept wildcard characters? false -#### ListParameterSets [<SwitchParameter>] - Use to list the different Parameter Sets available for the command. Output is shortened - to only show the names. Use -All to return splatting for all parameter sets. - - Required? false - Position? 2 - Default value False - Accept pipeline input? false - Accept wildcard characters? false -#### All [<SwitchParameter>] - Use to return full splatting for all parameter sets - - Required? false - Position? 2 - Default value False - Accept pipeline input? false - Accept wildcard characters? false -#### IncludeCommon [<SwitchParameter>] - Use to include the PowerShell common parameters in the splatting output. (e.g. Verbose, ErrorAction, etc.) - - Required? false - Position? 3 - Default value False - Accept pipeline input? false - Accept wildcard characters? false -#### Copy [<SwitchParameter>] - - Required? false - Position? 4 - Default value False - Accept pipeline input? false - Accept wildcard characters? false -### Examples -#### Example 1: Get the default parameter set for a command -```powershell -Get-CommandSplatting -Command 'Get-Item' -``` -###### Output -``` -Name : Path -IsDefault : True -SetBlock : - [string[]]$Path = '' - [string]$Filter = '' - [string[]]$Include = '' - [string[]]$Exclude = '' - [Boolean]$Force = $false # Switch - [pscredential]$Credential = '' - [string[]]$Stream = '' -HashBlock : - $Item = @{ - Path = $Path #Required - Filter = $Filter - Include = $Include - Exclude = $Exclude - Force = $Force - Credential = $Credential - Stream = $Stream - } - Get-Item @Item -``` -#### Example 2: List the available parameter sets for a command -```powershell -Get-CommandSplatting -Command 'Get-Item' -ListParameterSets -``` -###### Output -``` -ParameterSet : Path -IsDefault : True -Parameters : Path, Filter, Include, Exclude, Force, Credential, Stream -``` -#### Example 3: Get specific parameter set for a command -```powershell -Get-CommandSplatting -Command 'Get-Item' -ParameterSet LiteralPath -``` -###### Output -``` -ParameterSet : LiteralPath -IsDefault : False -SetBlock : - [string[]]$LiteralPath = '' - [string]$Filter = '' - [string[]]$Include = '' - [string[]]$Exclude = '' - [Boolean]$Force = $false # Switch - [pscredential]$Credential = '' - [string[]]$Stream = '' -HashBlock : - $ItemLiteralPath = @{ - LiteralPath = $LiteralPath #Required - Filter = $Filter - Include = $Include - Exclude = $Exclude - Force = $Force - Credential = $Credential - Stream = $Stream - } - Get-Item @ItemLiteralPath -``` -#### Example 4: Get all parameter sets for a command -```powershell -Get-CommandSplatting -Command 'Get-Item' -All -``` -###### Output -``` -ParameterSet : Path -IsDefault : True -SetBlock : - [string[]]$Path = '' - [string]$Filter = '' - [string[]]$Include = '' - [string[]]$Exclude = '' - [Boolean]$Force = $false # Switch - [pscredential]$Credential = '' - [string[]]$Stream = '' -HashBlock : - $ItemPath = @{ - Path = $Path #Required - Filter = $Filter - Include = $Include - Exclude = $Exclude - Force = $Force - Credential = $Credential - Stream = $Stream - } - Get-Item @ItemPath -ParameterSet : LiteralPath -IsDefault : False -SetBlock : - [string[]]$LiteralPath = '' - [string]$Filter = '' - [string[]]$Include = '' - [string[]]$Exclude = '' - [Boolean]$Force = $false # Switch - [pscredential]$Credential = '' - [string[]]$Stream = '' -HashBlock : - $ItemLiteralPath = @{ - LiteralPath = $LiteralPath #Required - Filter = $Filter - Include = $Include - Exclude = $Exclude - Force = $Force - Credential = $Credential - Stream = $Stream - } - Get-Item @ItemLiteralPath -``` diff --git a/Documentation/Get-CommandSplatting.md b/Documentation/Get-CommandSplatting.md new file mode 100644 index 0000000..407c889 --- /dev/null +++ b/Documentation/Get-CommandSplatting.md @@ -0,0 +1,251 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Get-CommandSplatting +--- + +# Get-CommandSplatting + +## SYNOPSIS + +Generates a splatting template for a PowerShell command. + +## SYNTAX + +### ParameterSet (Default) + +``` +Get-CommandSplatting [-Command] [[-ParameterSet] ] [-IncludeCommon] [-Copy] + [] +``` + +### ListParameterSets + +``` +Get-CommandSplatting [-Command] [-ListParameterSets] [-IncludeCommon] [-Copy] + [] +``` + +### All + +``` +Get-CommandSplatting [-Command] [-All] [-IncludeCommon] [-Copy] [] +``` + +## DESCRIPTION + +Get-CommandSplatting inspects a command’s parameter metadata and produces a ready-to-paste splatting +template. +It returns one or more objects that include: + +- A variable “set block” with typed variable declarations for each parameter +- A hashtable “hash block” formatted for splatting (including required-parameter comments) +- A final example invocation that splats the hashtable into the command + +By default, the cmdlet outputs the default parameter set for the command. +You can list available +parameter sets, generate a specific parameter set, or generate templates for all parameter sets. +Optionally include the PowerShell common parameters and/or copy the first generated template to the +clipboard. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Get-CommandSplatting -Command 'Get-Item' +``` + +Generates a splatting template for the default parameter set of Get-Item. + +### EXAMPLE 2 + +```powershell +Get-CommandSplatting -Command 'Get-Item' -ListParameterSets +``` + +Lists the available parameter sets for Get-Item and shows the parameters included in each set. + +### EXAMPLE 3 + +```powershell +Get-CommandSplatting -Command 'Get-Item' -ParameterSet LiteralPath +``` + +Generates a splatting template for the LiteralPath parameter set. + +### EXAMPLE 4 + +```powershell +Get-CommandSplatting -Command 'Get-Item' -All +``` + +Generates splatting templates for all parameter sets of Get-Item. + +### EXAMPLE 5 + +```powershell +Get-CommandSplatting -Command 'Get-Item' -IncludeCommon -Copy +``` + +Generates the default parameter set template including common parameters and copies the first template to the clipboard. + +## PARAMETERS + +### -All + +Generates splatting templates for all parameter sets for the specified command. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: All + Position: 1 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Command + +The name of the command to generate a splatting template for (cmdlet/function/alias supported by Get-Command). + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 0 + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Copy + +Copies the first generated template (SetBlock + HashBlock) to the clipboard. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 3 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -IncludeCommon + +Includes PowerShell common parameters (for example: Verbose, Debug, ErrorAction) in the generated output. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 2 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -ListParameterSets + +Lists available parameter sets for the specified command, including whether each set is the default and the +parameter names in that set. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: ListParameterSets + Position: 1 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -ParameterSet + +The name of a specific parameter set to generate. +Use -ListParameterSets to discover available names. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: ParameterSet + Position: 1 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### SplatBlock + + +## NOTES + +- Required parameters are annotated in the hashtable output with a "#Required" comment. +- Switch parameters are represented as [Boolean] variables in the set block, defaulting to $false. +- Use -ListParameterSets to discover parameter set names before using -ParameterSet. + + +## RELATED LINKS + + diff --git a/Documentation/Get-PSNote.MD b/Documentation/Get-PSNote.MD deleted file mode 100644 index 5e452e8..0000000 Binary files a/Documentation/Get-PSNote.MD and /dev/null differ diff --git a/Documentation/Get-PSNote.md b/Documentation/Get-PSNote.md new file mode 100644 index 0000000..848c8d3 --- /dev/null +++ b/Documentation/Get-PSNote.md @@ -0,0 +1,262 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Get-PSNote +--- + +# Get-PSNote + +## SYNOPSIS + +Retrieves PSNotes from the note store by listing or searching. + +## SYNTAX + +### Note (Default) + +``` +Get-PSNote [-Name ] [-Tag ] [-Copy] [-Run] [-Catalog ] + [] +``` + +### Search + +``` +Get-PSNote [-Run] [-Catalog ] [-Search ] [] +``` + +## DESCRIPTION + +Get-PSNote returns notes stored in the PSNotes store. +By default, all notes are returned. +You can filter results by name, alias, tag, catalog, or search text depending on the +parameter set in use. + +This cmdlet returns PSNote objects that can be piped into other PSNotes commands such as +Remove-PSNote, Move-PSNote, Export-PSNote, or Set-PSNote. + +Wildcard matching is supported where applicable. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Get-PSNote +``` + +Returns all notes in the store. + +### EXAMPLE 2 + +```powershell +Get-PSNote -Catalog 'Azure' +``` + +Returns all notes in the Azure catalog. + +### EXAMPLE 3 + +```powershell +Get-PSNote -Name 'Get-*' +``` + +Returns notes with names that match the pattern. + +### EXAMPLE 4 + +```powershell +Get-PSNote -Tag 'VM' +``` + +Returns notes tagged with 'VM'. + +### EXAMPLE 5 + +```powershell +Get-PSNote -Search 'backup' +``` + +Searches across note properties for the term 'backup'. + +### EXAMPLE 6 + +```powershell +Get-PSNote -Catalog 'Azure' | Remove-PSNote +``` + +Finds notes in the Azure catalog and removes them. + +## PARAMETERS + +### -Catalog + +Returns notes from the specified catalog. + +```yaml +Type: System.String[] +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Search + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +- Name: Note + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Copy + +When specified, copies the snippet content of the first matching note to the clipboard. +If multiple notes match, you will be prompted to select one. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Note + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Name + +Returns notes that match the specified name. +Wildcards are supported. + +```yaml +Type: System.String +DefaultValue: '*' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Note + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Run + +When specified, executes the snippet content of the first matching note. +If multiple notes match, you will be prompted to select one. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Search + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +- Name: Note + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Search + +Performs a broader search across note properties such as name, alias, and tags. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: +- SearchString +ParameterSets: +- Name: Search + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Tag + +Returns notes that contain one or more specified tags. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Note + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### PSNote + + +## NOTES + +- Returns PSNote objects. +- Wildcards are supported for Name and Alias parameters. +- See also: New-PSNote, Set-PSNote, Remove-PSNote, Move-PSNote, Export-PSNote + + +## RELATED LINKS + + diff --git a/Documentation/Get-PSNoteAlias.MD b/Documentation/Get-PSNoteAlias.MD deleted file mode 100644 index 3f70b1d..0000000 Binary files a/Documentation/Get-PSNoteAlias.MD and /dev/null differ diff --git a/Documentation/Get-PSNoteAlias.md b/Documentation/Get-PSNoteAlias.md new file mode 100644 index 0000000..45d4c9c --- /dev/null +++ b/Documentation/Get-PSNoteAlias.md @@ -0,0 +1,113 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Get-PSNoteAlias +--- + +# Get-PSNoteAlias + +## SYNOPSIS + +Resolves a PSNote by alias and outputs, copies, or executes its content. + +## SYNTAX + +### __AllParameterSets + +``` +Get-PSNoteAlias [-Copy] [-Run] [] +``` + +## DESCRIPTION + +Get-PSNoteAlias locates a PSNote using its alias and performs a quick action against it. + +Depending on the note’s Kind and the parameters supplied, the cmdlet can: + +- Output the snippet content to the console +- Copy the snippet content to the clipboard +- Execute the snippet directly +- Execute a referenced script path + +This command is designed for fast recall of frequently used commands through short, +easy-to-remember aliases. + +## EXAMPLES + +## PARAMETERS + +### -Copy + +Copies the note content to the clipboard instead of writing it to the console. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Run + +Executes the note content. +For snippet notes, the script block is invoked. +For script-path +notes, the referenced script is executed. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### System.String + + +## NOTES + +- Alias values are intended to be unique within the PSNotes store. +- Behavior differs based on the note Kind (for example, Snippet vs ScriptPath). +- Clipboard functionality depends on platform support. +- See also: Get-PSNote, New-PSNote, Set-PSNote + + +## RELATED LINKS + + diff --git a/Documentation/Get-PSNoteMenu.md b/Documentation/Get-PSNoteMenu.md new file mode 100644 index 0000000..e99d169 --- /dev/null +++ b/Documentation/Get-PSNoteMenu.md @@ -0,0 +1,106 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Get-PSNoteMenu +--- + +# Get-PSNoteMenu + +## SYNOPSIS + +Displays an interactive, paged console menu for browsing and selecting PSNotes. + +## SYNTAX + +### __AllParameterSets + +``` +Get-PSNoteMenu [[-InputObject] ] [] +``` + +## DESCRIPTION + +Get-PSNoteMenu presents PSNotes in an interactive terminal-based menu with paging support. +Users can navigate through notes, select one by number, and then choose an action such as: + +- Output the note content +- Copy the note to the clipboard +- Execute the snippet +- Execute a referenced script path + +The menu is designed to provide a streamlined console experience for browsing catalogs, +favorites, or filtered note sets. +When multiple pages are present, page indicators +(for example, "Page 1/3") are shown. + +This cmdlet is intended for interactive use within the current console session. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Get-PSNoteMenu +``` + +Displays all notes in an interactive menu. + +## PARAMETERS + +### -InputObject + +A collection of PSNote objects to display. +Accepts pipeline input from Get-PSNote. + +If not provided, all notes are displayed by default. + +```yaml +Type: System.Object +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 0 + IsRequired: false + ValueFromPipeline: true + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.Object + + +## OUTPUTS + +### PSNote + + +## NOTES + +- Designed for interactive terminal use. +- Supports paging when the number of notes exceeds the configured page size. +- After selecting a note, a secondary action menu is displayed (output, copy, run). +- See also: Get-PSNote, Get-PSNoteAlias, Start-PSNote + + +## RELATED LINKS + + diff --git a/Documentation/Get-RemoteCatalog.md b/Documentation/Get-RemoteCatalog.md new file mode 100644 index 0000000..d7b5ec9 --- /dev/null +++ b/Documentation/Get-RemoteCatalog.md @@ -0,0 +1,112 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Get-RemoteCatalog +--- + +# Get-RemoteCatalog + +## SYNOPSIS + +Gets remote catalogs registered with PSNotes. + +## SYNTAX + +### __AllParameterSets + +``` +Get-RemoteCatalog [[-Catalog] ] [] +``` + +## DESCRIPTION + +Get-RemoteCatalog retrieves remote catalog registrations from the PSNotes configuration. +Remote catalogs +represent external sources (such as a URL) that can be imported into PSNotes or kept registered for future +imports. + +Use -Catalog to return a specific remote catalog by name, or provide a wildcard pattern to match multiple +registrations. +If no remote catalogs are configured, this cmdlet returns an empty array. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Get-RemoteCatalog +``` + +Returns all configured remote catalogs. + +### EXAMPLE 2 + +```powershell +Get-RemoteCatalog -Catalog 'github' +``` + +Returns the remote catalog registration named 'github'. + +### EXAMPLE 3 + +```powershell +Get-RemoteCatalog -Catalog 'git*' +``` + +Returns all remote catalog registrations with names that match the pattern 'git*'. + +## PARAMETERS + +### -Catalog + +The name or wildcard pattern of the remote catalog registration to retrieve. + +The default value is '*' which returns all configured remote catalogs. + +```yaml +Type: System.String +DefaultValue: '*' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 0 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### System.Object + + +## NOTES + +- Remote catalog registrations are stored in the PSNotes configuration (for example, in the note store config file). +- If the PSNotes store is not initialized or no remote catalogs are configured, an empty array is returned. +- See also: Import-RemoteCatalog, Remove-RemoteCatalog + + +## RELATED LINKS + +- [Remove-RemoteCatalog +Import-RemoteCatalog]() diff --git a/Documentation/Import-PSNote.MD b/Documentation/Import-PSNote.MD deleted file mode 100644 index c2bd57b..0000000 Binary files a/Documentation/Import-PSNote.MD and /dev/null differ diff --git a/Documentation/Import-PSNote.md b/Documentation/Import-PSNote.md new file mode 100644 index 0000000..69999f8 --- /dev/null +++ b/Documentation/Import-PSNote.md @@ -0,0 +1,158 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Import-PSNote +--- + +# Import-PSNote + +## SYNOPSIS + +Imports PSNotes from a JSON export file into the local note store. + +## SYNTAX + +### Note (Default) + +``` +Import-PSNote [-Path] [[-Catalog] ] [[-DefaultBehavior] ] + [] +``` + +## DESCRIPTION + +Import-PSNote reads a PSNotes JSON export file and imports the contained notes into the local +PSNotes store. + +Import behavior may merge with existing notes or create new notes depending on the options +provided and the contents of the import file. +Use -Force (if supported) to overwrite existing +notes when conflicts occur. + +This cmdlet is commonly used to restore backups created by Export-PSNote, migrate notes between +machines, or share curated note libraries. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Import-PSNote -Path .\backup.json +``` + +Imports all notes from backup.json into the local store. + +### EXAMPLE 2 + +```powershell +Import-PSNote -Path .\azure-notes.json -Catalog 'Azure' +``` + +Imports notes from azure-notes.json and places them into the Azure catalog (if supported). + +### EXAMPLE 3 + +```powershell +Import-PSNote -Path .\backup.json -DefaultBehavior OverwriteExistingNotes +``` + +Imports notes, overwriting conflicts, and returns the imported note objects. + +## PARAMETERS + +### -Catalog + +Imports notes into the specified catalog (or maps imported notes into that catalog depending on implementation). + +```yaml +Type: System.String +DefaultValue: Default +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 1 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -DefaultBehavior + +Determines how to handle existing notes when conflicts are detected during import. +Valid values: +- Prompt: prompts when conflicts occur +- SkipMigratedNotes: keeps existing notes and skips conflicting imported notes +- OverwriteExistingNotes: overwrites existing notes with imported versions + +```yaml +Type: System.String +DefaultValue: Prompt +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 2 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Path + +The path to the PSNotes JSON file to import. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 0 + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### System.Object + + +## NOTES + +- This cmdlet is designed to round-trip with Export-PSNote. +- If you encounter format/version differences after upgrading PSNotes, run Update-PSNoteStore. +- See also: Export-PSNote, Get-PSNote, Update-PSNoteStore + + +## RELATED LINKS + + diff --git a/Documentation/Import-RemoteCatalog.md b/Documentation/Import-RemoteCatalog.md new file mode 100644 index 0000000..ee35bdc --- /dev/null +++ b/Documentation/Import-RemoteCatalog.md @@ -0,0 +1,202 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Import-RemoteCatalog +--- + +# Import-RemoteCatalog + +## SYNOPSIS + +Registers a remote PSNotes catalog or imports it as a local catalog. + +## SYNTAX + +### __AllParameterSets + +``` +Import-RemoteCatalog [-Name] [-Url] [-AsLocal] [-Force] [-PassThru] + [] +``` + +## DESCRIPTION + +Import-RemoteCatalog adds a remote catalog source to PSNotes or downloads it immediately as a local +catalog. + +By default, the cmdlet registers the remote catalog URL in the PSNotes store under the provided name. +This allows you to keep the catalog “linked” for future imports. + +When -AsLocal is specified, the remote catalog is downloaded immediately and saved as a LOCAL catalog. +In this mode, the URL is not registered as a remote source. +Use -Force to overwrite an existing local +catalog with the same name. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Import-RemoteCatalog -Name 'github' -Url 'https://example.com/psnotes.json' +``` + +Registers the remote catalog URL for later use. + +### EXAMPLE 2 + +```powershell +Import-RemoteCatalog -Name 'github' -Url 'https://example.com/psnotes.json' -AsLocal +``` + +Downloads the remote catalog immediately and creates a local catalog. +The URL is not registered. + +### EXAMPLE 3 + +```powershell +Import-RemoteCatalog -Name 'github' -Url 'https://example.com/psnotes.json' -AsLocal -Force -PassThru +``` + +Overwrites the existing local catalog and returns the created catalog object. + +## PARAMETERS + +### -AsLocal + +Downloads the remote catalog immediately and creates a LOCAL catalog. + +When specified, the remote catalog URL is not registered. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Force + +Only applies to -AsLocal. + +Overwrites the local catalog if it already exists. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Name + +The name to register for the remote catalog (or the name of the local catalog to create when -AsLocal is used). + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 0 + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -PassThru + +Returns the created or registered catalog object. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Url + +The URL of the remote catalog JSON. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 1 + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### System.Object + + +## NOTES + +- -AsLocal creates a local catalog immediately and does not register the URL as a remote catalog. +- Use Get-RemoteCatalog to view registered remote sources. +- See also: Get-RemoteCatalog, Remove-RemoteCatalog + + +## RELATED LINKS + +- [Get-RemoteCatalog +Remove-RemoteCatalog]() diff --git a/Documentation/Initialize-PSNoteStore.md b/Documentation/Initialize-PSNoteStore.md new file mode 100644 index 0000000..44c24cb --- /dev/null +++ b/Documentation/Initialize-PSNoteStore.md @@ -0,0 +1,71 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Initialize-PSNoteStore +--- + +# Initialize-PSNoteStore + +## SYNOPSIS + +Initializes the PSNotes store and required supporting files. + +## SYNTAX + +### __AllParameterSets + +``` +Initialize-PSNoteStore [] +``` + +## DESCRIPTION + +Initialize-PSNoteStore ensures the PSNotes store is present and ready for use. +It creates the +required folder structure and base configuration files when they do not already exist. + +This cmdlet is typically invoked automatically by other PSNotes commands as needed, but it can +also be called directly when setting up PSNotes on a new machine or when repairing an incomplete +store. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Initialize-PSNoteStore +``` + +Initializes the PSNotes store using the default location. + +## PARAMETERS + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### System.Object + + +## NOTES + +- This cmdlet prepares the store layout and configuration required by the PSNotes module. +- Most PSNotes commands will initialize the store automatically when needed. +- See also: Update-PSNoteStore, Get-PSNote + + +## RELATED LINKS + + diff --git a/Documentation/Invoke-PSNote.MD b/Documentation/Invoke-PSNote.MD deleted file mode 100644 index b444795..0000000 Binary files a/Documentation/Invoke-PSNote.MD and /dev/null differ diff --git a/Documentation/Move-PSNote.md b/Documentation/Move-PSNote.md new file mode 100644 index 0000000..4ef9bdb --- /dev/null +++ b/Documentation/Move-PSNote.md @@ -0,0 +1,214 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Move-PSNote +--- + +# Move-PSNote + +## SYNOPSIS + +Moves one or more PSNotes to a different catalog. + +## SYNTAX + +### __AllParameterSets + +``` +Move-PSNote [-InputObject] [-DestinationCatalog] [-Force] [-PassThru] [-WhatIf] + [-Confirm] [] +``` + +## DESCRIPTION + +Move-PSNote changes the catalog assignment of existing PSNotes. +This allows you to reorganize +your note library as it grows without recreating notes. + +You can specify notes directly by name, alias, or other supported parameters, or pipe PSNote +objects from Get-PSNote. + +This cmdlet updates the note metadata and persists the changes to the PSNotes store. +Supports ShouldProcess, enabling the use of -WhatIf and -Confirm. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Get-PSNote -Catalog 'General' | Move-PSNote -DestinationCatalog 'Archive' +``` + +Moves all notes from the General catalog to the Archive catalog. + +### EXAMPLE 2 + +```powershell +Get-PSNote -Tag 'Legacy' | Move-PSNote -DestinationCatalog 'Archive' -Force -PassThru +``` + +Moves all notes tagged 'Legacy' into the Archive catalog without prompting and returns the updated notes. + +## PARAMETERS + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- cf +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -DestinationCatalog + +The destination catalog to move the note(s) into. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 1 + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Force + +Suppresses confirmation prompts when moving notes. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -InputObject + +One or more PSNote objects to move. +Accepts pipeline input from Get-PSNote. + +```yaml +Type: PSNote +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 0 + IsRequired: true + ValueFromPipeline: true + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -PassThru + +Returns the moved PSNote objects. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -WhatIf + +Runs the command in a mode that only reports what would happen without performing the actions. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- wi +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### PSNote + + +## OUTPUTS + +### PSNote + + +## NOTES + +- Supports -WhatIf and -Confirm through ShouldProcess. +- Use Get-PSNote to identify notes before moving them. +- See also: Get-PSNote, Set-PSNote, Remove-PSNote + + +## RELATED LINKS + + diff --git a/Documentation/New-PSNote.MD b/Documentation/New-PSNote.MD deleted file mode 100644 index f743977..0000000 Binary files a/Documentation/New-PSNote.MD and /dev/null differ diff --git a/Documentation/New-PSNote.md b/Documentation/New-PSNote.md new file mode 100644 index 0000000..a6d1d45 --- /dev/null +++ b/Documentation/New-PSNote.md @@ -0,0 +1,379 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: New-PSNote +--- + +# New-PSNote + +## SYNOPSIS + +Creates a new PSNote for storing reusable snippets or script references. + +## SYNTAX + +### Note (Default) + +``` +New-PSNote -Name [-Details ] [-Alias ] [-Tags ] + [-Catalog ] [-Run ] [-Force] [-WhatIf] [-Confirm] [] +``` + +### Snippet + +``` +New-PSNote -Name [-Snippet ] [-Details ] [-Alias ] + [-Tags ] [-Catalog ] [-Run ] [-Force] [-WhatIf] [-Confirm] + [] +``` + +### ScriptBlock + +``` +New-PSNote -Name [-ScriptBlock ] [-Details ] [-Alias ] + [-Tags ] [-Catalog ] [-Run ] [-Force] [-WhatIf] [-Confirm] + [] +``` + +### ScriptPath + +``` +New-PSNote -Name [-ScriptPath ] [-Details ] [-Alias ] + [-Tags ] [-Catalog ] [-Run ] [-Force] [-WhatIf] [-Confirm] + [] +``` + +## DESCRIPTION + +New-PSNote creates a note in the PSNotes store that can represent either: + +- A reusable PowerShell snippet (inline code) +- A script reference (a path to a script file to execute) + +Notes can include metadata such as catalog, alias, and tags to make them easier to organize and +retrieve later. +Once created, notes can be recalled using Get-PSNote, invoked quickly by alias +with Get-PSNoteAlias, or selected interactively using Get-PSNoteMenu. + +This cmdlet supports ShouldProcess, enabling -WhatIf and -Confirm. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +New-PSNote -Name 'List-AzVMs' -Catalog 'Azure' -Alias 'azvms' -Tag 'VM','Azure' -Snippet 'Get-AzVM' +``` + +Creates a snippet note named List-AzVMs in the Azure catalog with an alias and tags. + +### EXAMPLE 2 + +```powershell +New-PSNote -Name 'Build' -Catalog 'Dev' -Alias 'build' -ScriptPath 'C:\Scripts\Build.ps1' +``` + +Creates a script-path note that references a script to run later. + +### EXAMPLE 3 + +```powershell +New-PSNote -Name 'TestNote' -Catalog 'General' -Snippet 'Get-Date' -WhatIf +``` + +Shows what would happen if the note were created without making changes. + +### EXAMPLE 4 + +```powershell +New-PSNote -Name 'List-AzVMs' -Catalog 'Azure' -Snippet 'Get-AzVM' -Force -PassThru +``` + +Overwrites an existing note (if present) and returns the created note object. + +## PARAMETERS + +### -Alias + +An optional short alias used for quick recall (for example, with Get-PSNoteAlias). + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Catalog + +The catalog to create the note in. + +```yaml +Type: System.String +DefaultValue: Default +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- cf +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Details + +Additional details or description about the note. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Force + +Overwrites an existing note with the same name (or alias conflict) where supported. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Name + +The name of the note. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Run + +When specified, executes the snippet content of the note immediately after creation. + +```yaml +Type: System.Boolean +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -ScriptBlock + +A PowerShell script block containing the code to store in the note. + +```yaml +Type: System.Management.Automation.ScriptBlock +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: ScriptBlock + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -ScriptPath + +A script path to store in the note for later execution. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: ScriptPath + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Snippet + +The PowerShell snippet content to store in the note. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Snippet + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Tags + +One or more tags to associate with the note. + +```yaml +Type: System.String[] +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -WhatIf + +Runs the command in a mode that only reports what would happen without performing the actions. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- wi +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### PSNote + + +## NOTES + +- Supports -WhatIf and -Confirm through ShouldProcess. +- Notes may be uniquely identified by name and/or alias depending on store rules. +- Use Set-PSNote to update an existing note. +- See also: Get-PSNote, Set-PSNote, Get-PSNoteAlias, Get-PSNoteMenu + + +## RELATED LINKS + + diff --git a/Documentation/Remove-PSNote.MD b/Documentation/Remove-PSNote.MD deleted file mode 100644 index 838a29d..0000000 Binary files a/Documentation/Remove-PSNote.MD and /dev/null differ diff --git a/Documentation/Remove-PSNote.md b/Documentation/Remove-PSNote.md new file mode 100644 index 0000000..c4495fb --- /dev/null +++ b/Documentation/Remove-PSNote.md @@ -0,0 +1,284 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Remove-PSNote +--- + +# Remove-PSNote + +## SYNOPSIS + +Removes one or more PSNotes from the note store. + +## SYNTAX + +### Note (Default) + +``` +Remove-PSNote [-Name ] [-Tag ] [-Catalog ] [-Force] [-WhatIf] [-Confirm] + [] +``` + +### ByObject + +``` +Remove-PSNote -InputObject [-Force] [-WhatIf] [-Confirm] [] +``` + +### Search + +``` +Remove-PSNote -SearchString [-Catalog ] [-Force] [-WhatIf] [-Confirm] + [] +``` + +## DESCRIPTION + +Remove-PSNote deletes notes from the PSNotes store. +You can target notes by name, alias, +catalog, tag, or by piping PSNote objects from Get-PSNote. + +This cmdlet updates the store immediately and permanently removes the selected notes. +Supports ShouldProcess, enabling the use of -WhatIf and -Confirm for safer operations. + +Use -Force to suppress confirmation prompts where applicable. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Remove-PSNote -Name 'OldNote' +``` + +Removes the note named 'OldNote'. + +### EXAMPLE 2 + +```powershell +Get-PSNote -Catalog 'Archive' | Remove-PSNote +``` + +Removes all notes in the Archive catalog. + +### EXAMPLE 3 + +```powershell +Remove-PSNote -Alias 'azvm' -WhatIf +``` + +Shows what would happen if the note with alias 'azvm' were removed. + +## PARAMETERS + +### -Catalog + +Removes notes from the specified catalog. + +```yaml +Type: System.String[] +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Search + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +- Name: Note + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- cf +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Force + +Suppresses confirmation prompts. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -InputObject + +One or more PSNote objects to remove. +Accepts pipeline input from Get-PSNote. + +```yaml +Type: System.Object +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: ByObject + Position: Named + IsRequired: true + ValueFromPipeline: true + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Name + +The name of the note to remove. +Wildcards may be supported depending on implementation. + +```yaml +Type: System.String +DefaultValue: '*' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Note + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -SearchString + +Performs a broader search across note properties such as name, alias, and tags to identify notes for removal. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Search + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Tag + +Removes notes that contain one or more specified tags. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Note + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -WhatIf + +Runs the command in a mode that only reports what would happen without performing the actions. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- wi +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.Object + + +## OUTPUTS + +### PSNote + + +## NOTES + +- Supports -WhatIf and -Confirm through ShouldProcess. +- Deletions are permanent once committed. +- Use Get-PSNote to preview notes before removing them. +- See also: Get-PSNote, New-PSNote, Set-PSNote, Move-PSNote + + +## RELATED LINKS + + diff --git a/Documentation/Remove-RemoteCatalog.md b/Documentation/Remove-RemoteCatalog.md new file mode 100644 index 0000000..d25e61d --- /dev/null +++ b/Documentation/Remove-RemoteCatalog.md @@ -0,0 +1,234 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Remove-RemoteCatalog +--- + +# Remove-RemoteCatalog + +## SYNOPSIS + +Removes a remote catalog registration from PSNotes. + +## SYNTAX + +### __AllParameterSets + +``` +Remove-RemoteCatalog [-InputObject] [-ConvertToLocal] [-Force] [-PassThru] + [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION + +Remove-RemoteCatalog unregisters one or more remote catalog sources from the PSNotes configuration. + +By default, this cmdlet removes only the remote registration and leaves any previously imported +local catalog content unchanged. + +If -ConvertToLocal is specified, the cached remote catalog is converted into a local catalog +before removing the remote registration. +This allows you to keep the notes while removing the +remote dependency. + +This cmdlet supports pipeline input from Get-RemoteCatalog and implements ShouldProcess, +allowing the use of -WhatIf and -Confirm. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Get-RemoteCatalog +``` + +Lists all registered remote catalogs. + +### EXAMPLE 2 + +```powershell +Get-RemoteCatalog -Catalog 'github' | Remove-RemoteCatalog +``` + +Removes the 'github' remote catalog registration. + +### EXAMPLE 3 + +```powershell +Get-RemoteCatalog -Catalog 'github' | Remove-RemoteCatalog -ConvertToLocal +``` + +Converts the cached remote catalog into a local catalog and removes the remote registration. + +### EXAMPLE 4 + +```powershell +Get-RemoteCatalog | Remove-RemoteCatalog -Force +``` + +Removes all remote catalog registrations without prompting. + +## PARAMETERS + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- cf +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -ConvertToLocal + +Converts the cached remote catalog into a local catalog before removing the remote registration. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Force + +Suppresses confirmation prompts when removing a remote catalog registration. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -InputObject + +A remote catalog object to remove. +Accepts pipeline input from Get-RemoteCatalog. + +```yaml +Type: RemoteCatalogSource +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 0 + IsRequired: true + ValueFromPipeline: true + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -PassThru + +Returns the removed (or converted) catalog object. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -WhatIf + +Runs the command in a mode that only reports what would happen without performing the actions. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- wi +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### RemoteCatalogSource + + +## OUTPUTS + +### System.Object + + +## NOTES + +- Supports -WhatIf and -Confirm through ShouldProcess. +- Removing a remote catalog does not delete local catalogs unless explicitly converted or managed separately. +- See also: Get-RemoteCatalog, Import-RemoteCatalog + + +## RELATED LINKS + +- [Get-RemoteCatalog +Import-RemoteCatalog]() diff --git a/Documentation/Set-PSNote.MD b/Documentation/Set-PSNote.MD deleted file mode 100644 index 9430ae6..0000000 Binary files a/Documentation/Set-PSNote.MD and /dev/null differ diff --git a/Documentation/Set-PSNote.md b/Documentation/Set-PSNote.md new file mode 100644 index 0000000..e4c47a6 --- /dev/null +++ b/Documentation/Set-PSNote.md @@ -0,0 +1,418 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: '' +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Set-PSNote +--- + +# Set-PSNote + +## SYNOPSIS + +Updates an existing PSNote or creates it if it does not already exist. + +## SYNTAX + +### __AllParameterSets + +``` +Set-PSNote [-Name] [[-Catalog] ] [[-Snippet] ] + [[-ScriptBlock] ] [[-ScriptPath] ] [[-Details] ] [[-Alias] ] + [[-Tags] ] [[-Run] ] [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION + +Set-PSNote modifies one or more properties of an existing note in the PSNotes store. +If the +target note does not exist in the specified catalog, a warning is written and the note is +created. + +Only the properties you provide are updated. +Properties you do not specify remain unchanged. +Internally, this cmdlet delegates to New-PSNote with -Force to perform an “upsert”. + +The note Kind is inferred from the content parameter you use: +- Snippet or ScriptBlock sets Kind to 'Snippet' (inline code) +- ScriptPath sets Kind to 'Script' (file reference) + +Set-PSNote supports pipeline input by property name, which makes it easy to bulk update notes +from Get-PSNote output or structured data sources such as CSV. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Set-PSNote -Name 'ADUser' -Tags 'AD','Users','Updated' +``` + +Updates the Tags for the note 'ADUser' in the Default catalog, replacing any existing tags. + +### EXAMPLE 2 + +```powershell +$NewSnippet = '(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__))' +Set-PSNote -Name 'DayOfWeek' -Snippet $NewSnippet +``` + +Updates only the snippet content for the note 'DayOfWeek' while leaving other properties unchanged. + +### EXAMPLE 3 + +```powershell +Set-PSNote -Name 'CpuUsage' -ScriptBlock { + Get-CimInstance Win32_Processor | + Measure-Object -Property LoadPercentage -Average | + Select-Object -ExpandProperty Average +} +``` + +Updates the note 'CpuUsage' with a new multi-line script block. + +### EXAMPLE 4 + +```powershell +Set-PSNote -Name 'CpuUsage' -Details "Returns average CPU usage percentage" -Alias 'cpu' +``` + +Updates only the Details and Alias properties for the note 'CpuUsage'. + +### EXAMPLE 5 + +```powershell +Set-PSNote -Name 'BackupScript' -ScriptPath 'D:\Scripts\Backup-Database.ps1' -Details "Updated backup script location" +``` + +Updates an existing note to reference a different script file, changing Kind to 'Script'. + +### EXAMPLE 6 + +```powershell +Get-PSNote -Tag 'deprecated' | Set-PSNote -Tags 'archived','old' +``` + +Bulk-updates notes by piping objects from Get-PSNote and replacing their Tags. + +### EXAMPLE 7 + +```powershell +[PSCustomObject]@{ + Name = 'MyNote' + Snippet = 'Get-Process | Select-Object -First 10' + Details = 'Top 10 processes' + Tags = @('process','monitoring') +} | Set-PSNote +``` + +Creates or updates a note using pipeline input by property name. + +## PARAMETERS + +### -Alias + +A new alias for the note. + +Aliases may contain letters, numbers, dashes (-), and underscores (_). +The alias is registered +as a global alias that invokes Get-PSNoteAlias. + +Accepts pipeline input by property name. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 6 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Catalog + +The catalog where the note is stored. +Defaults to 'Default'. + +If the note does not exist in the specified catalog, it will be created there. +Accepts pipeline input by property name. + +```yaml +Type: System.String +DefaultValue: Default +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 1 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- cf +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Details + +A description or additional information about the note. +Replaces the existing details. + +Accepts pipeline input by property name. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 5 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Name + +The name of the note to update or create. + +Accepts pipeline input by property name. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 0 + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Run + +Controls whether the note should be executed automatically when retrieved (if supported by your +workflow). +Set to $true to enable, or $false to disable. + +Accepts pipeline input by property name. + +```yaml +Type: System.Boolean +DefaultValue: False +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 8 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -ScriptBlock + +A PowerShell script block containing the code to store in the note. + +Accepts pipeline input by property name. + +```yaml +Type: System.Management.Automation.ScriptBlock +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 3 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -ScriptPath + +A file path to a PowerShell script (.ps1). +When specified, the note references this script and +Kind is set to 'Script'. +The file must exist. + +Accepts pipeline input by property name. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 4 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Snippet + +The snippet text to store in the note. +Replaces the existing snippet content. + +Accepts pipeline input by property name. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 2 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Tags + +A list of tags to associate with the note. +This replaces all existing tags. + +Accepts pipeline input by property name. + +```yaml +Type: System.String[] +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 7 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -WhatIf + +Runs the command in a mode that only reports what would happen without performing the actions. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: +- wi +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.String + + +### System.Management.Automation.ScriptBlock + + +### System.String[] + + +### System.Boolean + + +## OUTPUTS + +### PSNote + + +## NOTES + +- Supports -WhatIf and -Confirm through ShouldProcess. +- This cmdlet is a convenience wrapper around New-PSNote -Force (an “upsert” operation). +- Updates replace existing values rather than merging (for example, Tags are replaced, not appended). + To append tags, retrieve the note first and then set the combined list. +- See also: New-PSNote, Get-PSNote, Remove-PSNote, Get-PSNoteAlias + + +## RELATED LINKS + + diff --git a/Documentation/Update-PSNoteStore.md b/Documentation/Update-PSNoteStore.md new file mode 100644 index 0000000..66de522 --- /dev/null +++ b/Documentation/Update-PSNoteStore.md @@ -0,0 +1,114 @@ +--- +document type: cmdlet +external help file: PSNotes-Help.xml +HelpUri: https://github.com/mdowst/PSNotes +Locale: en-US +Module Name: PSNotes +ms.date: 02/19/2026 +PlatyPS schema version: 2024-05-01 +title: Update-PSNoteStore +--- + +# Update-PSNoteStore + +## SYNOPSIS + +Updates PSNotes catalogs to the latest format. + +## SYNTAX + +### __AllParameterSets + +``` +Update-PSNoteStore [[-DefaultBehavior] ] [] +``` + +## DESCRIPTION + +Update-PSNoteStore scans the PSNotes home directory for JSON catalogs and migrates any catalogs +that are not in the current format. +Migrated content is then imported back into the PSNotes +store using the specified conflict behavior. + +Use -DefaultBehavior to control how note conflicts are handled during import: +- Prompt: prompts when conflicts occur +- SkipMigratedNotes: keeps existing notes and skips conflicting migrated notes +- OverwriteExistingNotes: overwrites existing notes with migrated versions + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Update-PSNoteStore +``` + +Migrates any outdated catalogs and prompts when conflicts occur. + +### EXAMPLE 2 + +```powershell +Update-PSNoteStore -DefaultBehavior SkipMigratedNotes +``` + +Migrates catalogs and skips notes that already exist. + +### EXAMPLE 3 + +```powershell +Update-PSNoteStore -DefaultBehavior OverwriteExistingNotes +``` + +Migrates catalogs and overwrites existing notes when conflicts occur. + +## PARAMETERS + +### -DefaultBehavior + +Determines how to handle existing notes when conflicts are detected during import. + +Valid values: +- Prompt +- SkipMigratedNotes +- OverwriteExistingNotes + +The default is 'Prompt'. + +```yaml +Type: System.String +DefaultValue: Prompt +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: 0 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +- This cmdlet migrates catalogs discovered under $env:PSNOTES_HOME. +- Conflict behavior applies when importing migrated notes back into the store. +- See also: Initialize-PSNoteStore, Import-PSNote, Export-PSNote + + +## RELATED LINKS + +- [](https://github.com/mdowst/PSNotes) diff --git a/Documentation/media/isodate.gif b/Documentation/media/isodate.gif new file mode 100644 index 0000000..af7afd4 Binary files /dev/null and b/Documentation/media/isodate.gif differ diff --git a/Documentation/media/psnote.gif b/Documentation/media/psnote.gif new file mode 100644 index 0000000..47cd167 Binary files /dev/null and b/Documentation/media/psnote.gif differ diff --git a/Documentation/media/ram.gif b/Documentation/media/ram.gif new file mode 100644 index 0000000..494c84b Binary files /dev/null and b/Documentation/media/ram.gif differ diff --git a/Documentation/media/remote.gif b/Documentation/media/remote.gif new file mode 100644 index 0000000..a86c3a7 Binary files /dev/null and b/Documentation/media/remote.gif differ diff --git a/Documentation/media/script.gif b/Documentation/media/script.gif new file mode 100644 index 0000000..2d46018 Binary files /dev/null and b/Documentation/media/script.gif differ diff --git a/Documentation/media/vscode.gif b/Documentation/media/vscode.gif new file mode 100644 index 0000000..4a6675e Binary files /dev/null and b/Documentation/media/vscode.gif differ diff --git a/PSNotes.psd1 b/PSNotes.psd1 deleted file mode 100644 index 74ef0e0..0000000 --- a/PSNotes.psd1 +++ /dev/null @@ -1,133 +0,0 @@ -# -# Module manifest for module 'PSNotes' -# -# Generated by: Matthew Dowst -# -# Generated on: 12/5/2019 -# - -@{ - -# Script module or binary module file associated with this manifest. -RootModule = '.\PSNotes.psm1' - -# Version number of this module. -ModuleVersion = '0.2.0.1' - -# Supported PSEditions -# CompatiblePSEditions = @() - -# ID used to uniquely identify this module -GUID = '040757f4-ee7b-4e93-9883-f6a1930b6966' - -# Author of this module -Author = 'Matthew Dowst' - -# Company or vendor of this module -CompanyName = 'dowst.dev' - -# Copyright statement for this module -Copyright = '(c) 2019 Matthew Dowst. All rights reserved.' - -# Description of the functionality provided by this module -Description = 'PSNotes is a PowerShell module that allows you to create your own custom snippet library, that you can use to reference commands. It is great for long command you run often or commands you don''t run often and need a reminder on. Snippets can either be executed directly, copied to your clipboard, or simply output to the screen for you to do whatever you want with them. When you create a note, you assign an alias to it, so you can have an easy to remember keyword that you can then use to recall it. Notes can also be classified with tags, so you group them in logic collections. You can also easily search for them by tag, name, details, or text within the snippet.' - -# Minimum version of the Windows PowerShell engine required by this module -PowerShellVersion = '2.0' - -# Name of the Windows PowerShell host required by this module -# PowerShellHostName = '' - -# Minimum version of the Windows PowerShell host required by this module -# PowerShellHostVersion = '' - -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' - -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# CLRVersion = '' - -# Processor architecture (None, X86, Amd64) required by this module -ProcessorArchitecture = 'Amd64' - -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() - -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() - -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -ScriptsToProcess = '.\Resources\PSNote_Classes.ps1' - -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() - -# Format files (.ps1xml) to be loaded when importing this module -FormatsToProcess = '.\Resources\PSNotes.format.ps1xml' - -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() - -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Export-PSNote', 'Get-PSNote', 'Get-PSNoteAlias', 'Import-PSNote', 'Invoke-PSNote', 'Copy-PSNote', - 'New-PSNote', 'Remove-PSNote', 'Set-PSNote','Get-CommandSplatting','ConvertTo-Splatting' - -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = @() - -# Variables to export from this module -VariablesToExport = '*' - -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '*' - -# DSC resources to export from this module -# DscResourcesToExport = @() - -# List of all modules packaged with this module -# ModuleList = @() - -# List of all files packaged with this module -# FileList = @() - -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() - - # A URL to the license for this module. - LicenseUri = 'https://github.com/mdowst/PSNotes/blob/master/LICENSE' - - # A URL to the main website for this project. - ProjectUri = 'https://github.com/mdowst/PSNotes' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module - # ReleaseNotes = '' - - # Prerelease string of this module - # Prerelease = '' - - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false - - # External dependent modules of this module - # ExternalModuleDependencies = @() - - } # End of PSData hashtable - - } # End of PrivateData hashtable - -# HelpInfo URI of this module -# HelpInfoURI = '' - -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' - -} - diff --git a/PSNotes.psm1 b/PSNotes.psm1 deleted file mode 100644 index f5c855e..0000000 --- a/PSNotes.psm1 +++ /dev/null @@ -1,56 +0,0 @@ -# Global Variables - -if ($IsLinux) { - $script:UserPSNotesJsonPath = '/home/' -} -else { - $script:UserPSNotesJsonPath = Join-Path $env:APPDATA '\PSNotes\' -} - -if($global:IsPesterTest){ - $script:UserPSNotesJsonPath = Join-Path $UserPSNotesJsonPath 'Pester' - Get-ChildItem -Path $UserPSNotesJsonPath -Filter '*.json' | Remove-Item -Force -} - -$global:UserPSNotesJsonFile = Join-Path $UserPSNotesJsonPath '\PSNotes.json' -[System.Collections.Generic.List[PSNote]] $script:noteObjects = @() - -if (-not $PSScriptRoot) { - $Path = '.\' -} -else { - $Path = $PSScriptRoot -} - -# Import the functions -foreach ($folder in @('private', 'public')) { - $root = Join-Path -Path $Path -ChildPath $folder - if (Test-Path -Path $root) { - Write-Verbose "processing folder $root" - $files = Get-ChildItem -Path $root -Filter *.ps1 -Recurse - - # dot source each file - $files | where-Object { $_.name -NotLike '*.Tests.ps1' } | - ForEach-Object { Write-Verbose $_.name; . $_.FullName } - } -} - -# Load all commands to noteObjects -Initialize-PSNotesJsonFile - -# Check id Set-Clipboard cmdlet is found. If not -if (-not (Get-Command -Name 'Set-Clipboard' -ErrorAction SilentlyContinue)) { - # ClipboardText module is found then set an alias for the Set-Clipboard command - if (Get-Module ClipboardText -ListAvailable) { - if (-not (Get-Alias -Name 'Set-Clipboard' -ErrorAction SilentlyContinue)) { - Set-Alias -Name 'Set-Clipboard' -Value 'Set-ClipboardText' - } - } - else { - $warning = "Cmdlet 'Set-Clipboard' not found. Copy functionality will not work until this is resovled. " + - "`n`t You can install the ClipboardText module from PowerShell Gallery, to add this functionality. " + - "`n`n`t`t Install-Module -Name ClipboardText`n" + - "`n`t More Details: https://www.powershellgallery.com/packages/ClipboardText" - Write-Warning $warning - } -} \ No newline at end of file diff --git a/PSNotes.test.ps1 b/PSNotes.test.ps1 deleted file mode 100644 index 28b9374..0000000 --- a/PSNotes.test.ps1 +++ /dev/null @@ -1,98 +0,0 @@ -BeforeAll { - $global:IsPesterTest = $true - Import-Module .\PSNotes.psd1 -Force -} - -Describe 'Get-PSNote' { - BeforeAll { - $PesterJson = Join-Path $env:TEMP "Pester.Test.json" - $mockJson = '[{"Note":"Date_Tester","Alias":"t_today","Details":"Get todays date","Tags":["date"],"Snippet":"Get-Date 01/01/2001"}]' - $mockJson | Out-File $PesterJson - Import-PSNote -Path $PesterJson -Catalog 'Pester' - } - - It "Get Single Note" { - $testNote = Get-PSNote -Note 'Date_Tester' - $testNote.Note | Should -Be 'Date_Tester' - $testNote.Snippet | Should -Be 'Get-Date 01/01/2001' - $testNote.Details | Should -Be 'Get todays date' - $testNote.Alias | Should -Be 't_today' - $testNote.Tags | Should -Be 'date' - $testNote.file | Should -BeLike '*\PSNotes\Pester\Pester.json' - } - - It "Output Style Check" { - Get-PSNote -Note 'Date_Tester' | Out-String | Should -Be "`r`n----------------------------------------`r`n`r`nNote : Date_Tester`r`nDetails : Get todays date`r`nAlias : t_today`r`nSnippet :`r`n`r`nGet-Date 01/01/2001`r`n`r`n`r`n`r`n" - } - - It "Test Run Parameter" { - Get-PSNote -Note 'Date_Tester' -Run | Should -Be $(Get-Date 01/01/2001) - } -} - -Describe 'Get-PSNoteAlias' { - - It "Test Alias" { - t_today | Should -Be 'Get-Date 01/01/2001' - } - - It "Test Alias Run" { - t_today -run | Should -Be $(Get-Date 01/01/2001) - } - - It "Test Direct Call" { - {Get-PSNoteAlias -ErrorAction Stop} | Should -Throw 'The Get-PSNoteAlias cmdlet is designed to be called using an alias and not directly.' - } - -} - -Describe 'New-PSNote' { - - It "Test Adding Note" { - New-PSNote -Note 'ADTester' -Snippet 'Get-AdUser tester' -Details "Use to return all AD users" -Tags 'AD','Users' - ADTester | Should -Be 'Get-AdUser tester' - } - - It "Test Adding Note with ScriptBlock" { - New-PSNote -Note "SBTester" -ScriptBlock {Get-Date 1/1/2001} -Details "Testing Script Block" -Tags 'Date' -Alias 'TestDate' - TestDate | Should -Be 'Get-Date 1/1/2001' - TestDate -run | Should -Be $(Get-Date 1/1/2001) - } - - It "Test New Alias" { - New-PSNote -Note 'ADTester' -Alias 'TestUser' -Force - TestUser | Should -Be 'Get-AdUser tester' - } - - It "Test New without Force" { - {New-PSNote -Note 'ADTester' -Alias 'NoForce' -ErrorAction Stop} | Should -Throw "The note 'ADTester' already exists. Use -force to overwrite existing properties" - TestUser | Should -Be 'Get-AdUser tester' - } - -} - -Describe 'Set-PSNote' { - - It "Test Set Alias" { - Set-PSNote -Note 'ADTester' -Alias 'SetTest' - SetTest | Should -Be 'Get-AdUser tester' - } - - It "Test Set Details" { - Set-PSNote -Note 'ADTester' -Details "Pester Tester" - SetTest | Should -Be 'Get-AdUser tester' - } - - It "Test Set Note with ScriptBlock" { - Set-PSNote -Note "SBTester" -ScriptBlock {Get-Date 12/31/2001} - TestDate | Should -Be 'Get-Date 12/31/2001' - TestDate -run | Should -Be $(Get-Date 12/31/2001) - } - - It "Test Set Note with Snippet" { - Set-PSNote -Note "SBTester" -Snippet 'Get-Date 7/4/2001' - TestDate | Should -Be 'Get-Date 7/4/2001' - TestDate -run | Should -Be $(Get-Date 7/4/2001) - } -} - diff --git a/Private/Initialize-PSNotesJsonFiles.ps1 b/Private/Initialize-PSNotesJsonFiles.ps1 deleted file mode 100644 index 3e3c1a2..0000000 --- a/Private/Initialize-PSNotesJsonFiles.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -Function Initialize-PSNotesJsonFile{ - # load the Json Files into an Object - Function LoadPSNotesJsonFile{ - param($PSNotesJsonFile) - Write-Verbose "Importing file : $PSNotesJsonFile" - if(-not (Test-Path $PSNotesJsonFile)){ - Write-Warning "File not found : $PSNotesJsonFile" - break - } - - $(Get-Content $PSNotesJsonFile -Raw | ConvertFrom-Json) | Select-Object Note, Snippet, Details, Alias, Tags, @{l='file';e={$PSNotesJsonFile}}| - ForEach-Object{ - $newNote = [PSNote]::New($_) - $remove = $script:noteObjects | Where-Object{$_.Alias -eq $newNote.Alias} - if($remove){ - $script:noteObjects.Remove($remove) | Out-Null - } - $script:noteObjects.Add($newNote) - } - } - - # Create PSNote folder in %APPDATA% to save user's local PSNote.json - if(-not (Test-Path $UserPSNotesJsonPath)){ - New-Item -Type Directory -Path $UserPSNotesJsonPath | Out-Null - } - - # Create PSNote.json in %APPDATA%\PSNotes to save users local settings - if(-not (Test-Path $UserPSNotesJsonFile)){ - $exampleJson = '[{"Note":"NewPSNote","Alias":"Example","Details":"Example of creating a new Note","Tags":["notes"],' + - '"Snippet":"$Snippet = @\u0027\r\n(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__)' + - '\r\n\u0027@\r\nNew-PSNote -Note \u0027DayOfWeek\u0027 -Snippet $Snippet -Details \"Use to name of the day of the week\"' + - ' -Tags \u0027date\u0027 -Alias \u0027today\u0027"}]' - $exampleJson | Out-File $UserPSNotesJsonFile -Encoding UTF8 - } - - # load additional JSON store files - Get-ChildItem -LiteralPath $UserPSNotesJsonPath -Filter "*.json" | Where-Object {$_.FullName -ne $UserPSNotesJsonFile} | - ForEach-Object{ LoadPSNotesJsonFile $_.FullName } - - # load the PSNote.json into $noteObjects - LoadPSNotesJsonFile $UserPSNotesJsonFile - - # load Aliases for commands - $noteObjects | ForEach-Object { - Write-Debug "Alias : $($_.Alias)" - Set-Alias -Name $_.Alias -Value Get-PSNoteAlias -Scope Global -Force - } -} \ No newline at end of file diff --git a/Private/Update-PSNotesJsonFile.ps1 b/Private/Update-PSNotesJsonFile.ps1 deleted file mode 100644 index 474bc94..0000000 --- a/Private/Update-PSNotesJsonFile.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -Function Update-PSNotesJsonFile{ - [cmdletbinding(SupportsShouldProcess=$true,ConfirmImpact='Low')] - param() - if(-not (Test-Path (Split-Path $UserPSNotesJsonFile))){ - New-Item -type directory -Path $(Split-Path $UserPSNotesJsonFile) | Out-Null - } - - $noteObjects | Where-Object {$_.file -eq $UserPSNotesJsonFile} | - Select-Object -Property Note, Alias, Details, Tags, Snippet | ConvertTo-Json | Out-File $UserPSNotesJsonFile -} \ No newline at end of file diff --git a/Private/Write-NoteSnippet.ps1 b/Private/Write-NoteSnippet.ps1 deleted file mode 100644 index d301238..0000000 --- a/Private/Write-NoteSnippet.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -Function Write-NoteSnippet { - <# - .SYNOPSIS - Used by the Copy-PSNote and Invoke-PSNote to display a menu and prompt for selection of a note - - .PARAMETER NoteSelection - An array of PSNote objects to create a menu with - - #> - [cmdletbinding()] - param( - [PSNote[]]$NoteSelection - ) - $i = 0 - $noteMenu = $NoteSelection | ForEach-Object { - $i++ - $_ | Select-Object @{l = 'Nbr'; e = { $i } }, * - } - $promptMenu = $noteMenu | Format-Table Nbr, Note, Alias, Details, Tags -AutoSize | Out-String - - $Prompt = "$($promptMenu)Enter the number to run (or leave blank to cancel) and hit [Enter]" - $Selection = Read-Host -Prompt $Prompt - if ([string]::IsNullOrEmpty($Selection)) { - $null - } - elseif (-not [int]::TryParse($Selection, [ref]$null)) { - Write-Error "The select must a number between 1 and $($NoteSelection.Count)" - } - elseif ([int]$Selection -gt $NoteSelection.Count -or [int]$Selection -lt 1) { - Write-Error "The select must be between 1 and $($NoteSelection.Count)" - } - else { - $NoteSelection[$Selection - 1].Snippet - } -} \ No newline at end of file diff --git a/Public/Copy-PSNote.ps1 b/Public/Copy-PSNote.ps1 deleted file mode 100644 index 9d532ac..0000000 --- a/Public/Copy-PSNote.ps1 +++ /dev/null @@ -1,71 +0,0 @@ -Function Copy-PSNote{ - <# - .SYNOPSIS - Use to display a list of notes in a selectable menu so you can choose which to copy to your clipboard - - .DESCRIPTION - Allows you to search for snippets by name or by tag. You can also search all - properties by using the SearchString parameter. Search results are displayed - in a selectable menu and you are prompted to select which one you want to - add to your clipboard. - - .PARAMETER Note - The note you want to return. Accepts wildcards - - .PARAMETER Tag - The tag of the note(s) you want to return. - - .PARAMETER SearchString - Use to search for text in the note's name, details, snippet, alias, and tags - - .EXAMPLE - Copy-PSNote - - Returns a menu with all notes - - .EXAMPLE - Copy-PSNote -Name 'creds' - - Returns a menu with the note creds - - .EXAMPLE - Copy-PSNote -Name 'cred*' - - Returns a menu with all notes that start with cred - - .EXAMPLE - Copy-PSNote -tag 'AD' - - Returns a menu with all notes with the tag 'AD' - - .EXAMPLE - Copy-PSNote -Name '*user*' -tag 'AD' - - Returns a menu with all notes with user in the name and the tag 'AD' - - .EXAMPLE - Copy-PSNote -SearchString 'day' - - Returns a menu with all notes with the word day in the name, details, snippet text, alias, or tags - - .LINK - https://github.com/mdowst/PSNotes - #> - [cmdletbinding(DefaultParameterSetName="Note")] - param( - [Alias("Name")] - [parameter(Mandatory=$false, ParameterSetName="Note", Position = 0)] - [string]$Note = '*', - [parameter(Mandatory=$false, ParameterSetName="Note")] - [string]$Tag, - [parameter(Mandatory=$false, ParameterSetName="Search", Position = 0)] - [string]$SearchString - ) - - $NoteSelection = @(Get-PSNote @PSBoundParameters) - $noteSnippet = Write-NoteSnippet $NoteSelection - - if(-not [string]::IsNullOrEmpty($noteSnippet)){ - $noteSnippet | Set-Clipboard - } -} \ No newline at end of file diff --git a/Public/Export-PSNote.ps1 b/Public/Export-PSNote.ps1 deleted file mode 100644 index fb1dbba..0000000 --- a/Public/Export-PSNote.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -Function Export-PSNote{ - <# - .SYNOPSIS - Use to export your PSNotes to copy to another machine or share with others - - .DESCRIPTION - Allows you to export your PSNotes to a JSON file, that can then be imported - to another machine or by other users. - - .PARAMETER NoteObject - The PSNote objects you want to export. Use Get-PSNote to build the object and pass it to the parameter - or use a pipeline to pass it. - - .PARAMETER All - Export all PSNotes - - .PARAMETER Path - The path to the PSNotes JSON file to export to. - - .PARAMETER Append - Use to append the output file. Default is to overwrite. - - .EXAMPLE - Export-PSNote -All -Path C:\Export\MyPSNotes.json - - Exportall notes to a JSON file. - - .EXAMPLE - Get-PSNote -tag 'AD' | Export-PSNote -Path C:\Export\SharedADNotes.json - - Exports all notes with the tag 'AD' to the file SharedADNotes.json - - - - .LINK - https://github.com/mdowst/PSNotes - #> - [cmdletbinding(DefaultParameterSetName="Note")] - param( - [parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="Note")] - [PSNote[]]$NoteObject, - [parameter(Mandatory=$false, ParameterSetName="All")] - [switch]$All, - [parameter(Mandatory=$true)] - [string]$Path, - [parameter(Mandatory=$false)] - [switch]$Append - ) - begin{ - [System.Collections.Generic.List[PSNoteExport]] $ExportObjects = @() - Write-Verbose "$($noteObject | FT | Out-String)" - } - process{ - # If All add all objects otherwise only add those passed - if($All){ - $noteObjects | ForEach-Object{ $ExportObjects.Add( [PSNoteExport]::New( $_ ) ) } - } else { - $noteObject | ForEach-Object{ $ExportObjects.Add( [PSNoteExport]::New( $_ ) ) } - } - } - end{ - Write-Verbose "$($ExportObjects | FT | Out-String)" - # if append add append objects before exporting - if($Append){ - if(-not (Test-Path $path)){ - Write-Verbose "File '$path' not found. Will continue with export, but will not append." - } else { - # import existing from JSON, but overwrite any matching Notes with the new value - $(Get-Content $Path -Raw | ConvertFrom-Json) | Select-Object Note, Snippet, Details, Alias, Tags | - Where-Object{ $ExportObjects.Alias -notcontains $_.Alias } | ForEach-Object{ - $ExportObjects.Add([PSNoteExport]::New( $_ )) - } - } - - } - - $ExportObjects | ConvertTo-Json | Out-File $Path -Encoding UTF8NoBOM - } -} \ No newline at end of file diff --git a/Public/Get-CommandSplatting.ps1 b/Public/Get-CommandSplatting.ps1 deleted file mode 100644 index 02f3215..0000000 --- a/Public/Get-CommandSplatting.ps1 +++ /dev/null @@ -1,269 +0,0 @@ -Function Get-CommandSplatting { - <# - .SYNOPSIS - Use to output the parameters for a command in splatting format - - .DESCRIPTION - Use to output the parameters for a command in splatting format - - .PARAMETER Command - The command to get the parameters for - - .PARAMETER ParameterSet - Use to specify a specific parameter set. Use the -ListParameterSets to get a quick - view of all the different Parameter Set names. - - .PARAMETER ListParameterSets - Use to list the different Parameter Sets available for the command. Output is shortened - to only show the names. Use -All to return splatting for all parameter sets. - - .PARAMETER All - Use to return full splatting for all parameter sets - - .PARAMETER IncludeCommon - Use to include the PowerShell common parameters in the splatting output. (e.g. Verbose, ErrorAction, etc.) - - .EXAMPLE - Get-CommandSplatting -Command 'Get-Item' - - Get the default parameter set for a command - - --- Output ---- - Name : Path - IsDefault : True - SetBlock : - [string[]]$Path = '' - [string]$Filter = '' - [string[]]$Include = '' - [string[]]$Exclude = '' - [Boolean]$Force = $false # Switch - [pscredential]$Credential = '' - [string[]]$Stream = '' - HashBlock : - $Item = @{ - Path = $Path #Required - Filter = $Filter - Include = $Include - Exclude = $Exclude - Force = $Force - Credential = $Credential - Stream = $Stream - } - Get-Item @Item - - - .EXAMPLE - Get-CommandSplatting -Command 'Get-Item' -ListParameterSets - - List the available parameter sets for a command - - --- Output ---- - ParameterSet : Path - IsDefault : True - Parameters : Path, Filter, Include, Exclude, Force, Credential, Stream - - ParameterSet : LiteralPath - IsDefault : False - Parameters : LiteralPath, Filter, Include, Exclude, Force, Credential, Stream - - - .EXAMPLE - Get-CommandSplatting -Command 'Get-Item' -ParameterSet LiteralPath - - Get specific parameter set for a command - - --- Output ---- - ParameterSet : LiteralPath - IsDefault : False - SetBlock : - [string[]]$LiteralPath = '' - [string]$Filter = '' - [string[]]$Include = '' - [string[]]$Exclude = '' - [Boolean]$Force = $false # Switch - [pscredential]$Credential = '' - [string[]]$Stream = '' - HashBlock : - $ItemLiteralPath = @{ - LiteralPath = $LiteralPath #Required - Filter = $Filter - Include = $Include - Exclude = $Exclude - Force = $Force - Credential = $Credential - Stream = $Stream - } - Get-Item @ItemLiteralPath - - - .EXAMPLE - Get-CommandSplatting -Command 'Get-Item' -All - - Get all parameter sets for a command - - --- Output ---- - ParameterSet : Path - IsDefault : True - SetBlock : - [string[]]$Path = '' - [string]$Filter = '' - [string[]]$Include = '' - [string[]]$Exclude = '' - [Boolean]$Force = $false # Switch - [pscredential]$Credential = '' - [string[]]$Stream = '' - HashBlock : - $ItemPath = @{ - Path = $Path #Required - Filter = $Filter - Include = $Include - Exclude = $Exclude - Force = $Force - Credential = $Credential - Stream = $Stream - } - Get-Item @ItemPath - ParameterSet : LiteralPath - IsDefault : False - SetBlock : - [string[]]$LiteralPath = '' - [string]$Filter = '' - [string[]]$Include = '' - [string[]]$Exclude = '' - [Boolean]$Force = $false # Switch - [pscredential]$Credential = '' - [string[]]$Stream = '' - HashBlock : - $ItemLiteralPath = @{ - LiteralPath = $LiteralPath #Required - Filter = $Filter - Include = $Include - Exclude = $Exclude - Force = $Force - Credential = $Credential - Stream = $Stream - } - Get-Item @ItemLiteralPath - - - .NOTES - General notes - #> - [CmdletBinding(DefaultParameterSetName = 'ParameterSet')] - param( - [Parameter(Mandatory = $true, Position = 0)] - [string]$Command, - [Parameter(ParameterSetName = 'ParameterSet', Position = 1)] - [string]$ParameterSet, - [Parameter(ParameterSetName = 'ListParameterSets', Position = 1)] - [switch]$ListParameterSets, - [Parameter(ParameterSetName = 'All', Position = 1)] - [switch]$All, - [Parameter(Mandatory = $false, Position = 2)] - [switch]$IncludeCommon, - [Parameter(Mandatory = $false, Position = 3)] - [switch]$Copy - ) - - # Get the command - $commandData = Get-Command $command - - # Get the parameter sets - $ParameterSets = $null - if ($All -eq $true) { - $ParameterSets = @($commandData.ParameterSets) - if ($ParameterSets.Count -eq 0) { - throw "Unable to find parameter sets" - } - } - elseif ($ListParameterSets -eq $true) { - $Output = $commandData.ParameterSets | Select-Object -Property @{l = 'ParameterSet'; e = { $_.Name } }, IsDefault, @{l = 'Parameters'; e = { @($_.Parameters.Name | - Where-Object { $_ -notin [System.Management.Automation.Cmdlet]::CommonParameters }) -join (', ') } - } | - ForEach-Object { - [SplatBlock]@{ - ParameterSet = $_.ParameterSet - IsDefault = $_.IsDefault - HashBlock = $_.Parameters - SetBlock = $null - } - } - } - elseif (-not [string]::IsNullOrEmpty($ParameterSet)) { - $ParameterSets = $commandData.ParameterSets | Where-Object { $_.Name -eq $ParameterSet } - if ($ParameterSets.Count -lt 1 -and $Output.Count -eq 0) { - throw "Unable to find parameter set '$($ParameterSet)'" - } - } - else { - $ParameterSets = @($commandData.ParameterSets | Where-Object { $_.IsDefault -eq $trufe }) - if ($ParameterSets.Count -eq 0) { - $ParameterSets = @($commandData.ParameterSets[0]) - } - if ($ParameterSets.Count -lt 1) { - throw "Unable to find the default parameter set" - } - } - - $hash = $command.Split('-')[-1] - - if ($ListParameterSets -ne $true) { - $Output = foreach ($set in $ParameterSets) { - [System.Collections.Generic.List[PSObject]]$hashBlock = @() - [System.Collections.Generic.List[PSObject]]$setBlock = @() - $hashBlock.Add("`$$($hash)$($set.Name) = @{") - $length = 0 - - $Parameters = $set.Parameters - if ($IncludeCommon -ne $true) { - $Parameters = $set.Parameters | Where-Object { $_.Name -notin - [System.Management.Automation.Cmdlet]::CommonParameters } - } - $Parameters.Name | ForEach-Object { if ($_.Length -gt $length) { $length = $_.length } } - - $LastOrder = $Parameters | Sort-Object Position | Select-Object -ExpandProperty Position -Last 1 - if ($LastOrder -ge 0) { - $SortedParameters = $Parameters | - Select-Object -Property *, @{l = 'Order'; e = { if ($_.Position -lt 0) { $LastOrder + 1 } else { $_.Position } } } | - Sort-Object Order - } - else { - $SortedParameters = $Parameters | Sort-Object Position - } - - Foreach ($p in $SortedParameters) { - $l = $length - $p.Name.Length - - if ($p.ParameterType.Name -eq 'SwitchParameter') { - $setBlock.Add("[Boolean]`$$($p.Name) = `$false # Switch") - } - else { - $setBlock.Add("[$($p.ParameterType)]`$$($p.Name) = ''") - } - - [string]$row = "`t$($p.Name)$(' ' * $l) = `$$($p.Name)" - if ($p.IsMandatory -eq $true) { - $row += "$(' ' * $l) #Required" - } - $hashBlock.Add($row) - } - $hashBlock.Add('}') - $hashBlock.Add("$command @$($hash)$($set.Name)") - [SplatBlock]@{ - Command = $Command - ParameterSet = $set.Name - IsDefault = $set.IsDefault - HashBlock = ($hashBlock -join ("`n")) - SetBlock = ($setBlock -join ("`n")) - } - } - } - - if($Copy -eq $true){ - $Output | Select-Object -First 1 | ForEach-Object{ - "$($_.SetBlock)`n$($_.HashBlock)" | Set-Clipboard - } - } - - $Output -} \ No newline at end of file diff --git a/Public/Get-PSNote.ps1 b/Public/Get-PSNote.ps1 deleted file mode 100644 index 7ff4c83..0000000 --- a/Public/Get-PSNote.ps1 +++ /dev/null @@ -1,116 +0,0 @@ -Function Get-PSNote{ - <# - .SYNOPSIS - Use to search for or list the different PSNotes - - .DESCRIPTION - Allows you to search for snippets by name or by tag. You can also search all - properties by using the SearchString parameter - - .PARAMETER Note - The note you want to return. Accepts wildcards - - .PARAMETER Tag - The tag of the note(s) you want to return. - - .PARAMETER Copy - If specfied the the Snippet will be copied to your clipboard - - .PARAMETER SearchString - Use to search for text in the note's name, details, snippet, alias, and tags - - .EXAMPLE - Get-PSNote - - Returns all notes - - .EXAMPLE - Get-PSNote -Name 'creds' - - Returns the note creds - - .EXAMPLE - Get-PSNote -Name 'cred*' - - Returns all notes that start with cred - - .EXAMPLE - Get-PSNote -tag 'AD' - - Returns all notes with the tag 'AD' - - .EXAMPLE - Get-PSNote -Name '*user*' -tag 'AD' - - Returns all notes with user in the name and the tag 'AD' - - .EXAMPLE - Get-PSNote -SearchString 'day' - - Returns all notes with the word day in the name, details, snippet text, alias, or tags - - .LINK - https://github.com/mdowst/PSNotes - #> - [cmdletbinding(DefaultParameterSetName="Note")] - param( - [parameter(Mandatory=$false, ParameterSetName="Note")] - [string]$Note = '*', - [parameter(Mandatory=$false, ParameterSetName="Note")] - [string]$Tag, - [parameter(Mandatory=$false, ParameterSetName="Note")] - [switch]$Copy, - [parameter(Mandatory=$false, ParameterSetName="Note")] - [switch]$Run, - [parameter(Mandatory=$false, ParameterSetName="Search")] - [string]$SearchString - ) - - - if($SearchString){ - [System.Collections.Generic.List[PSNoteSearch]] $SearchResults = @() - $noteObjects | Where-Object{ $_.Note -like "*$SearchString*" -or $_.Alias -like "*$SearchString*" -or - $_.Details -like "*$SearchString*" -or $_.Snippet -like "*$SearchString*" } | ForEach-Object { $SearchResults.Add($_) } - $noteObjects | Where-Object{ $SearchResults.Note -notcontains $_.Note } | ForEach-Object { - $tagMatch = $false - $_.tag | ForEach-Object { - if($_ -like "*$SearchString*"){ - $tagMatch = $true - } - } - if($tagMatch){ - $SearchResults.Add($_) - } - } - $returned = $SearchResults - } elseif($Tag){ - $returned = $noteObjects | Where-Object{$_.Note -like $note -and $_.Tags -contains $Tag} - } else { - $returned = $noteObjects | Where-Object{$_.Note -like $note} - } - - if($copy){ - if(@($returned).count -gt 1){ - Write-Warning "More than 1 command returned. Only the first one will be written to the clipboard" - } - if(Get-Command -Name 'Set-Clipboard' -ErrorAction SilentlyContinue){ - $returned | Select-Object -First 1 -ExpandProperty Snippet | Set-Clipboard - } else { - Write-Debug "Cmdlet 'Set-Clipboard' not found." - } - } - - if($Run){ - if(@($returned).count -gt 1){ - Write-Warning -Message "$($returned | Select-Object -First 1 -ExpandProperty Snippet)" - Write-Warning -Message "More than 1 command was returned. If you continue Only the first one will be run" -WarningAction Inquire - } - - $Snippet = $returned | Select-Object -First 1 -ExpandProperty Snippet - $ScriptBlock = $executioncontext.invokecommand.NewScriptBlock($Snippet) - Invoke-Command -ScriptBlock $ScriptBlock - } else { - $returned - } - -} diff --git a/Public/Get-PSNoteAlias.ps1 b/Public/Get-PSNoteAlias.ps1 deleted file mode 100644 index d52b6fd..0000000 --- a/Public/Get-PSNoteAlias.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -Function Get-PSNoteAlias{ - <# - .SYNOPSIS - Use display snippet and copy to clipboard using an Alias - - .DESCRIPTION - When the PSNotes module loads, it creates Aliases for all snippets. - Those aliases are mapped to this command and it will return the snippet - and copy it to your clipboard. You cannot call this function directly - as it will not return anything. - - .LINK - https://github.com/mdowst/PSNotes - - - #> - [cmdletbinding()] - param( - [parameter(Mandatory=$false)] - [switch]$Copy, - [parameter(Mandatory=$false)] - [switch]$Run - ) - if($MyInvocation.MyCommand.Name -eq $MyInvocation.InvocationName){ - Write-Error "The Get-PSNoteAlias cmdlet is designed to be called using an alias and not directly." - } else { - $Alias = $MyInvocation.InvocationName - $aliasObject = $noteObjects | Where-Object{$_.Alias -eq $Alias} - if($Run){ - Get-PSNote -Note $aliasObject.Note -Run - } else { - if(Get-Command -Name 'Set-Clipboard' -ErrorAction SilentlyContinue){ - $returned | Select-Object -First 1 -ExpandProperty Snippet | Set-Clipboard - } else { - Write-Debug "Cmdlet 'Set-Clipboard' not found." - } - Return $aliasObject.Snippet - } - } -} - diff --git a/Public/Import-PSNote.ps1 b/Public/Import-PSNote.ps1 deleted file mode 100644 index caba56a..0000000 --- a/Public/Import-PSNote.ps1 +++ /dev/null @@ -1,63 +0,0 @@ -Function Import-PSNote{ - <# - .SYNOPSIS - Use to import a PSNotes JSON fiile - - .DESCRIPTION - Allows you to import shared PSNotes JSON files to your local notes. They can be imported to your personal - store, or they can be imported to a seperate file. - - .PARAMETER NoteObject - The PSNote objects you want to export. Use Get-PSNote to build the object and pass it to the parameter - or use a pipeline to pass it. - - .PARAMETER Path - The path to the PSNotes JSON file to export to. - - .PARAMETER Catalog - Use to output snippets to a seperate file stored in the folder %APPDATA%\PSNotes. - Useful for when you want to share different snippet types. - - .EXAMPLE - Import-PSNote -Path C:\Import\MyPSNotes.json - - Imports the contents of the file MyPSNotes.json and saves it to your personal PSNotes.json file - - .EXAMPLE - Import-PSNote -Path C:\Export\MyPSNotes.json -Catalog 'ADNotes' - - Imports the contents of the file MyPSNotes.json and saves it to the file ADNotes.json in the folder %APPDATA%\PSNotes - - .LINK - https://github.com/mdowst/PSNotes - #> - [cmdletbinding(DefaultParameterSetName="Note")] - param( - [parameter(Mandatory=$true)] - [string]$Path, - [parameter(Mandatory=$false)] - [string]$Catalog - ) - - # If Catalog check name and set path - if($Catalog){ - # confirm the Catalog string is a valid file name - if($Catalog.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ne -1){ - throw "The catalog name '$Catalog' is an invalid file name. Invalid characater found in place $($Catalog.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()))" - } - # Set path the path for the catalog item - $CatalogPath = Join-Path $UserPSNotesJsonPath "$Catalog.json" - } else { - $CatalogPath = $UserPSNotesJsonFile - } - - [System.Collections.Generic.List[PSNote]] $ImportObjects = @() - $(Get-Content $Path -Raw | ConvertFrom-Json) | Select-Object Note, Snippet, Details, Alias, Tags, @{l='file';e={$CatalogPath}}| - ForEach-Object{ $ImportObjects.Add([PSNote]::New($_)) } - - # Append the new notes to the appropriate file - Export-PSNote -NoteObject $ImportObjects -Path $CatalogPath -Append - - # Reinitialize the Json files to reload everything - Initialize-PSNotesJsonFile -} \ No newline at end of file diff --git a/Public/Invoke-PSNote.ps1 b/Public/Invoke-PSNote.ps1 deleted file mode 100644 index b23270b..0000000 --- a/Public/Invoke-PSNote.ps1 +++ /dev/null @@ -1,71 +0,0 @@ -Function Invoke-PSNote{ - <# - .SYNOPSIS - Use to display a list of notes in a selectable menu so you can choose which to run - - .DESCRIPTION - Allows you to search for snippets by name or by tag. You can also search all - properties by using the SearchString parameter. Search results are displayed - in a selectable menu and you are prompted to select which one you want to run. - - .PARAMETER Note - The note you want to run. Accepts wildcards - - .PARAMETER Tag - The tag of the note(s) you want to run. - - .PARAMETER SearchString - Use to search for text in the note's name, details, snippet, alias, and tags - - .EXAMPLE - Invoke-PSNote - - Returns a menu with all notes - - .EXAMPLE - Invoke-PSNote -Name 'creds' - - Returns a menu with the note creds - - .EXAMPLE - Invoke-PSNote -Name 'cred*' - - Returns a menu with all notes that start with cred - - .EXAMPLE - Invoke-PSNote -tag 'AD' - - Returns a menu with all notes with the tag 'AD' - - .EXAMPLE - Invoke-PSNote -Name '*user*' -tag 'AD' - - Returns a menu with all notes with user in the name and the tag 'AD' - - .EXAMPLE - Invoke-PSNote -SearchString 'day' - - Returns a menu with all notes with the word day in the name, details, snippet text, alias, or tags - - .LINK - https://github.com/mdowst/PSNotes - #> - [cmdletbinding(DefaultParameterSetName="Note")] - param( - [Alias("Name")] - [parameter(Mandatory=$false, ParameterSetName="Note", Position = 0)] - [string]$Note = '*', - [parameter(Mandatory=$false, ParameterSetName="Note")] - [string]$Tag, - [parameter(Mandatory=$false, ParameterSetName="Search", Position = 0)] - [string]$SearchString - ) - - $NoteSelection = @(Get-PSNote @PSBoundParameters) - $noteSnippet = Write-NoteSnippet $NoteSelection - - if(-not [string]::IsNullOrEmpty($noteSnippet)){ - $ScriptBlock = $executioncontext.invokecommand.NewScriptBlock($noteSnippet) - Invoke-Command -ScriptBlock $ScriptBlock - } -} \ No newline at end of file diff --git a/Public/New-PSNote.ps1 b/Public/New-PSNote.ps1 deleted file mode 100644 index fabbc8a..0000000 --- a/Public/New-PSNote.ps1 +++ /dev/null @@ -1,136 +0,0 @@ -Function New-PSNote{ - <# - .SYNOPSIS - Use to add or update a PSNote object - - .DESCRIPTION - Allows you to add or update a PSNote object. If note already - exists you must supply the Force switch to overwrite it. - Only values supplied with be updated. - - .PARAMETER Note - The note you want to add/update. - - .PARAMETER Snippet - The text of the snippet to add/update. - - .PARAMETER ScriptBlock - Specifies the snippet to save. Enclose the commands in braces { } to create a script block. - - .PARAMETER Details - The Details of the snippet to add/update. - - .PARAMETER Tag - The tag of the note(s) you want to return. - - .PARAMETER Alias - The Alias to create to copy this snippet to your clipboard. If not - supplied it will use the Note value - - .PARAMETER Tags - A string array of tags to add/update for the Note - - .PARAMETER Force - If Note already exists the Force switch is required to overwrite it - - .EXAMPLE - New-PSNote -Note 'ADUser' -Snippet 'Get-AdUser -Filter *' -Details "Use to return all AD users" -Tags 'AD','Users' - - Creates a new Note for the Get-ADUser cmdlet - - .EXAMPLE - New-PSNote -Note 'CpuUsage' -Tags 'perf' -Alias 'cpu' -ScriptBlock { - Get-WmiObject win32_processor | Measure-Object -property LoadPercentage -Average - } - - Creates a new Note using a script block instead of a snippet string - - .EXAMPLE - $Snippet = '(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__)' - New-PSNote -Note 'DayOfWeek' -Snippet $Snippet -Details "Use to name of the day of the week" -Tags 'date' -Alias 'today' - - Creates a new Note for the to get the current day's abbrevation with the custom Alias of today - - .EXAMPLE - $Snippet = @' - $stringBuilder = New-Object System.Text.StringBuilder - for ($i = 0; $i -lt 10; $i++){ - $stringBuilder.Append("Line $i`r`n") | Out-Null - } - $stringBuilder.ToString() - '@ - New-PSNote -Note 'StringBuilder' -Snippet $Snippet -Details "Use StringBuilder to combine multiple strings" -Tags 'string' - - Creates a new Note with a new mulitple line snippet using a here-string - - .LINK - https://github.com/mdowst/PSNotes - - - #> - [cmdletbinding(SupportsShouldProcess=$true,ConfirmImpact='Low',DefaultParameterSetName="Note")] - param( - [parameter(Mandatory=$true)] - [string]$Note, - [parameter(Mandatory=$false, ParameterSetName="Snippet")] - [string]$Snippet, - [parameter(Mandatory=$false, ParameterSetName="ScriptBlock")] - [ScriptBlock]$ScriptBlock, - [parameter(Mandatory=$false)] - [string]$Details, - [parameter(Mandatory=$false)] - [string]$Alias, - [parameter(Mandatory=$false)] - [string[]]$Tags, - [parameter(Mandatory=$false)] - [switch]$Force - ) - Function Test-NoteAlias{ - param($Alias) - - $AliasCheck = [regex]::Matches($Alias,"[^0-9a-zA-Z\-_]") - if($AliasCheck.Success){ - throw "'$Alias' is not a valid alias. Alias's can only contain letters, numbers, dashes(-), and underscores (_)." - } - } - - if(-not [string]::IsNullOrEmpty($ScriptBlock)){ - $Snippet = $ScriptBlock.ToString() - } - - $newNote = $noteObjects | Where-Object{$_.Note -eq $Note} - if($newNote -and -not $force){ - Write-Error "The note '$Note' already exists. Use -force to overwrite existing properties" - break - } elseif($newNote -and $force){ - $noteObjects | Where-Object{$_.Note -eq $Note} | ForEach-Object{ - if(-not [string]::IsNullOrEmpty($Snippet)){ - $_.Snippet = $Snippet - } - if(-not [string]::IsNullOrEmpty($Details)){ - $_.Details = $Details - } - if(-not [string]::IsNullOrEmpty($Alias)){ - Test-NoteAlias $Alias - $_.Alias = $Alias - } - if(-not [string]::IsNullOrEmpty($Tags)){ - $_.Tags = $Tags - } - $_.File = $UserPSNotesJsonFile - } - } else { - if([string]::IsNullOrEmpty($Alias)){ - $Alias = $Note - } - - Test-NoteAlias $Alias - - $newNote = [PSNote]::New($Note, $Snippet, $Details, $Alias, $Tags) - $noteObjects.Add($newNote) - } - - Set-Alias -Name $newNote.Alias -Value Get-PSNoteAlias -Scope Global - - Update-PSNotesJsonFile -} \ No newline at end of file diff --git a/Public/Remove-PSNote.ps1 b/Public/Remove-PSNote.ps1 deleted file mode 100644 index 57dbaab..0000000 --- a/Public/Remove-PSNote.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -Function Remove-PSNote{ - <# - .SYNOPSIS - Use to remove a Note from you personal store - - .DESCRIPTION - Allows you to remove a snippets by name. - - .PARAMETER Note - The note you want to remove. Has to match exactly - - - .EXAMPLE - Remove-PSNote -Note 'creds' - - Removes the Note creds - - .EXAMPLE - Get-PSNote -Name 'creds' | Remove-PSNote - - Removes the Note creds using pipeline - - .EXAMPLE - Remove-PSNote -Note 'creds' -confirm:$false - - Removes the Note creds without prompting - - .LINK - https://github.com/mdowst/PSNotes - #> - [cmdletbinding(SupportsShouldProcess=$true,ConfirmImpact='High')] - param( - [parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$True)] - [string]$Note, - [parameter(Mandatory=$false)] - [switch]$Force - ) - - $remove = $noteObjects | Where-Object{$_.Note -eq $note} - Write-Verbose "Note : $($note | Out-String)" - Write-Verbose "remove : $($remove | Out-String)" - - - if($remove){ - if($PSCmdlet.ShouldProcess( - ("Removing note '{0}'" -f $remove.Note), - ("Would you like to remove {0}?" -f $remove.Note), - "Confirm removal" - )){ - if($noteObjects.Remove($remove)){ - Update-PSNotesJsonFile - } - } - } - - - - $remove - -} \ No newline at end of file diff --git a/Public/Set-PSNote.ps1 b/Public/Set-PSNote.ps1 deleted file mode 100644 index 1b937c7..0000000 --- a/Public/Set-PSNote.ps1 +++ /dev/null @@ -1,71 +0,0 @@ -Function Set-PSNote{ - <# - .SYNOPSIS - Use to add or update a PSNote object - - .DESCRIPTION - Allows you to add or update a PSNote object. If note already - exists you must supply the Force switch to overwrite it. - Only values supplied with be updated. - - .PARAMETER Note - The note you want to add/update. - - .PARAMETER Snippet - The text of the snippet to add/update. - - .PARAMETER ScriptBlock - Specifies the snippet to save. Enclose the commands in braces { } to create a script block - - .PARAMETER Details - The Details of the snippet to add/update. - - .PARAMETER Tag - The tag of the note(s) you want to return. - - .PARAMETER Alias - The Alias to create to copy this snippet to your clipboard. If not - supplied it will use the Note value - - .PARAMETER Tags - A string array of tags to add/update for the Note - - .EXAMPLE - Set-PSNote -Note 'ADUser' -Tags 'AD','Users' - - Set the tags AD and User for the note ADUser - - .EXAMPLE - $Snippet = '(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__)' - Set-PSNote -Note 'DayOfWeek' -Snippet $Snippet - - Updates the snippet for the note DayOfWeek - - .LINK - https://github.com/mdowst/PSNotes - - - #> - [cmdletbinding(SupportsShouldProcess=$true,ConfirmImpact='Low',DefaultParameterSetName="Note")] - param( - [parameter(Mandatory=$true)] - [string]$Note, - [parameter(Mandatory=$false, ParameterSetName="Snippet")] - [string]$Snippet, - [parameter(Mandatory=$false, ParameterSetName="ScriptBlock")] - [ScriptBlock]$ScriptBlock, - [parameter(Mandatory=$false)] - [string]$Details, - [parameter(Mandatory=$false)] - [string]$Alias, - [parameter(Mandatory=$false)] - [string[]]$Tags - ) - - $check = $noteObjects | Where-Object{$_.Note -eq $Note} - if(-not $check){ - Write-Warning "The note '$Note' does not exists. An attempt will be made to create it." - } - - New-PSNote @PSBoundParameters -Force -} \ No newline at end of file diff --git a/Publish/PublishToPSGallery.ps1 b/Publish/PublishToPSGallery.ps1 deleted file mode 100644 index 22845b9..0000000 --- a/Publish/PublishToPSGallery.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -# Script variables -$IncludeDirectories = 'Private','Public','Resources' -$ModulesFolder = (Split-Path $PSScriptRoot) -$NugetAPIKey = Get-Content (Join-Path $PSScriptRoot 'APIKey.json') - -# Get the module manifest -$psd1File = Get-ChildItem -path $ModulesFolder -Filter "*.psd1" | Select-Object -ExpandProperty FullName -$psd1 = Test-ModuleManifest $psd1File - -# Revise the new version -$Revision = $psd1.Version.Revision + 1 -[System.Version]$newVersion = [System.Version]::new($psd1.Version.Major, $psd1.Version.Minor, $psd1.Version.MinorRevision, $Revision) - -Write-Verbose "New version '$version'" - -Update-ModuleManifest -Path $psd1File -ModuleVersion $newVersion - -# create the release folder -$releaseFolder = Join-Path $PSScriptRoot "\PSNotes\$($newVersion.ToString())" -If (-not(Test-Path $releaseFolder)){ - New-Item -type Directory -Path $releaseFolder | Out-Null -} - -# Copy files to release folder -Get-ChildItem $ModulesFolder -Filter '*.ps*1' | Where-Object { $_.Name -notlike '*test*' } | - Copy-Item -Destination $releaseFolder - -foreach($folder in $IncludeDirectories){ - $modFolder = Join-Path $ModulesFolder $folder - $destFolder = Join-Path $releaseFolder $folder - If (-not(Test-Path $destFolder)){ - New-Item -type Directory -Path $destFolder | Out-Null - } - - Get-ChildItem $modFolder -File | Copy-Item -Destination $destFolder -} - - -# Publish to powershell gallery -Publish-Module -Path $releaseFolder -NugetAPIKey $NugetAPIKey -Verbose \ No newline at end of file diff --git a/README.md b/README.md index b0e0e8f..aba2b96 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,354 @@ -# PSNotes - -PSNotes is a PowerShell module that allows you to create your own custom snippet library, that you can use to reference commands you run often. Or ones you don't run often and need a reminder on. Snippets can either be executed directly, copied to your clipboard, or simply output to the display for you to do whatever you want with them. When you create a note, you assign an alias to it, so you can have an easy to remember keyword that you can then use to recall it. Notes can also be classified with tags, so you can easily search for them. - -* [Key Features](#key-features) -* [Getting Started](#getting-started) - * [Install Instructions](#install-instructions) - * [Output and Run Notes](#output-and-Run-Notes) - * [Search Notes](#search-notes) - * [Creating Notes](#Creating-Notes) - * [Updating Notes](#updating-notes) - * [Sharing Notes](#sharing-notes) - -# Key Features - -### Recall a command using a specific alias keyword -When you create a new note, you can define an alias that you can later use to display or run it. -![UnixTime Demo](Documentation/media/UnixTime.gif) - -Perfect for long commands you need to run often. -![AzCon Demo](Documentation/media/AzCon.gif) - -### Easily search your notes -You can assign tags to your notes to make searching easier. -![Get-PSNote Demo](Documentation/media/ADUserTag.gif) - -### Quickly add your own notes -Add new snippets at any time -![New-PSNote Demo](Documentation/media/newnote.gif) - -Add new snippets as string or by using a script block -![New-PSNote ScriptBlock Demo](Documentation/media/ScriptBlock.gif) - -### Share your notes with others -The import and export functionality allows you to share notes between machines and people. -![Export/Import Demo](Documentation/media/ImportExport.gif) - -# Getting Started -## Install Instructions -PowerShell v5+ and PowerShell Core v6+ -```powershell -Install-Module PSNotes -``` - -Note: At of the time of publishing Set-Clipboard is not supported in PowerShell Core. To use the copy to clipboard functionality of this module, it is recommended that you also install the ClipboardText module. - - -```powershell -Install-Module -Name ClipboardText -``` -[top](#psnotes) -## Output and Run Notes -When you create a note in the PSNotes module you assign an alias to it. You can use this alias at any time to output, copy, or run a note. Simply type the name of the alias and hit enter to output it to your PowerShell console. You can also add the `-copy` switch to have the note copied to your clipboard or use the `-run` to execute the note directly. - -###### Example 1: Output the note to the console -This example gets the note/code snippet with the alias "MyNote" and outputs it to the console. -```powershell -MyNote -``` - -###### Example 2: Output the note to the console -This example gets the note/code snippet with the alias "MyNote" and outputs it to the console and copies it to your local clipboard. -```powershell -MyNote -copy -``` - -###### Example 3: Execute the note directly -This example gets the note/code snippet with the alias "MyNote" and executes the command in your local session. -```powershell -MyNote -run -``` -[top](#psnotes) -## Search Notes -Don't worry if you can't remember the alias you assigned to a note. You can use `Get-PSNote` to search your notes by name, tags, and keywords. - -###### Example 1: Get all notes -This example gets all the notes currently loaded in your profile. -```powershell -Get-PSNote -``` - -###### Example 2: Get notes with a name that begins with string -This example gets all the notes that start with cred. -```powershell -Get-PSNote -Name 'cred*' -``` - -###### Example 3: Get notes with a name that contains a string -This example gets all the notes that have a name with the word "user" in it. -```powershell -Get-PSNote -Name '*user*' -``` - -###### Example 4: Get notes by tag -This example gets all the notes that have the tag "AD" assigned to them. -```powershell -Get-PSNote -Tag 'AD' -``` - -###### Example 4: Get notes that includes a search string -This example gets all the notes with the word "day" in the name, details, snippet text, alias, or tags. -```powershell -Get-PSNote -SearchString 'day' -``` -[top](#psnotes) -## Creating Notes -You can create your own notes at any time using `New-PSNote`. Keep in mind that the snippet must be passed as string, so it is recommended to wrap them in single quotes and here-strings to prevent them from being executed when you are creating a note. - -###### Example 1: Create a new note -This example creates a new note for the Get-ADUser cmdlet. Since the `-Alias` parameter is not supplied the Note value will be assigned as the alias. -```powershell -New-PSNote -Note 'ADUser' -Snippet 'Get-ADUser -Filter *' -Details "Use to return all AD users" -Tags 'AD','Users' -``` - -###### Example 2: Create a new note with a custom alias -This example creates a new note with a custom alias. -```powershell -$Snippet = '(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__)' -New-PSNote -Note 'DayOfWeek' -Snippet $Snippet -Details "Use to name of the day of the week" -Tags 'date' -Alias 'today' -``` - -###### Example 3: Create a new note using script block -This example creates a new note for the Get-WmiObject using a script block instead of a string. This make multiple line scripts easier to enter and gives you the ability to use auto-complete when entering it. -```powershell -New-PSNote -Note 'CpuUsage' -Tags 'perf' -Alias 'cpu' -ScriptBlock { - Get-WmiObject win32_processor | Measure-Object -property LoadPercentage -Average -} -``` - -###### Example 4: Create a new note with both single and double quotes in it -This example shows one way you can create a new note for a snippet that contains both single and double quotes. Notice in the snippet itself the single quotes are doubled. This escapes them and tells PowerShell it is not the end of the string. -```powershell -New-PSNote -Note 'SvcAccounts' -Snippet 'Get-ADUser -Filter ''Name -like "*SvcAccount"''' -Details "Use to return all AD Service Accounts" -Tags 'AD','Users' -``` - -###### Example 5: Create multiple line note -When creating a note for a multiple line snippet, it is recommended that you use a here-string with single quotes to prevent expressions from being evaluated when you run the `New-PSNote` command. -```powershell -$Snippet = @' -$stringBuilder = New-Object System.Text.StringBuilder -for ($i = 0; $i -lt 10; $i++){ - $stringBuilder.Append("Line $i`r`n") | Out-Null -} -$stringBuilder.ToString() -'@ -New-PSNote -Note 'StringBuilder' -Snippet $Snippet -Details "Use StringBuilder to combine multiple strings" -Tags 'string' -``` -[top](#psnotes) -## Updating Notes -You can update a note at any time using `Set-PSNote`. With `Set-PSNote` you can update the Snippet, Details, Tags, or Alias of any note. In addition, notes can be deleted using `Remove-PSNote`. - -###### Example 1: Set new tags -This example shows how to add the tags "AD" and "User" to the note ADUser -```powershell -Set-PSNote -Note 'ADUser' -Tags 'AD','Users' -``` - -###### Example 2: Update Snippet -This example shows how to update the snippet for the note DayOfWeek -```powershell -$Snippet = '(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__)' -Set-PSNote -Note 'DayOfWeek' -Snippet $Snippet -``` - -###### Example 3: Delete a note -This example shows how to delete a note named creds. This command does not accept wildcards, so the name of the note must match exactly. -```powershell -Remove-PSNote -Note 'creds' -``` -[top](#psnotes) -## Sharing Notes -Not only does PSNotes allow you to create your own custom notes. It allows you to share them between computers and users. You can create a list of notes export them and share them with your team. Notes are stored in easy to read and edit JSON files in case you want to make manual edits. - -Note: PSNotes stores your notes in your local AppData folder using the path %appdata%\PSNotes. By default, it places them in the file PSNotes.json. When you run the `Import-PSNote` cmdlet you can choose a catalog name. Doing so will cause the imported notes to be stored in a file with that catalogs name. - -###### Example 1: Export all notes -This example exports all notes to a JSON file. -```powershell -Export-PSNote -All -Path C:\Export\MyPSNotes.json -``` - -###### Example 2: Export a selection of notes -This example exports the notes with the tag AD to a JSON file. -```powershell -Get-PSNote -tag 'AD' | Export-PSNote -Path C:\Export\SharedADNotes.json -``` - -###### Example 3: Import to personal store -This example imports the contents of the file MyPSNotes.json and saves it to your personal PSNotes.json file. -```powershell -Import-PSNote -Path C:\Import\MyPSNotes.json -``` - -###### Example 4: Import to custom catalog file -This example imports the contents of the file SharedADNotes.json and saves it to the file ADNotes.json in the folder %APPDATA%\PSNotes -```powershell -Import-PSNote -Path C:\Export\SharedADNotes.json -Catalog 'ADNotes' -``` -[top](#psnotes) \ No newline at end of file +# PSNotes + +PSNotes is a PowerShell module that lets you: + +* Store reusable snippets and scripts +* Assign memorable aliases +* Tag and categorize them +* Execute, copy, or preview instantly +* Organize them into local or remote catalogs +* Browse them from a terminal menu + +It’s not just a snippet tool. A structured engine for reusable automation patterns. + +PSNotes isn’t about storing code. +It’s about lowering cognitive load. + +The best automation engineers don’t memorize commands. +They build systems to remember for them. + +## Why PSNotes? + +If you're an automation engineer, you've done this: + +* Saved commands in random text files +* Dug through Slack to find “that fix from last month” +* Scrolled your history hoping it’s still there +* Searched through scripts scattered everywhere +* Rebuilt a command you *know* you already wrote once + +PSNotes replaces tribal memory with structured recall. + +--- +# Quick Links +* [Key Features](#key-features) +* [Installation](#installation) +* [Get Started](#get-started) +* [Roadmap](#roadmap) +* [Architecture](#architecture) +--- + +# Key Features + +### Interactive Console + +> Browse, search, and execute from a menu directly in your terminal. + +![psnote Demo](Documentation/media/psnote.gif) + +> Works from Windows Terminal, VSCode, pwsh, or any PowerShell host. + +![vscode Demo](Documentation/media/vscode.gif) + +--- + +### Run Snippets Instantly + +> When you create a new note, you can define an alias that you can later use to display or run it. + +![UnixTime Demo](Documentation/media/UnixTime.gif) + +--- + +### Execute Script Paths + +> Notes can either be saved as snippets or point directly to a script file. + +```powershell +New-PSNote -Name "Patch All" -Alias patch -ScriptPath 'C:\Scripts\Patch-All.ps1' +``` + +![Script Demo](Documentation/media/script.gif) + +--- + +### Catalogs (Local + Remote) + +> Catalogs can be imported from remote URLs. Letting you access your notes from everywhere. + + +```powershell +Import-RemoteCatalog -Url https://... +Get-PSNote -Catalog TeamShared +``` + +![Remote Demo](Documentation/media/remote.gif) + + +--- + +# Installation + +```powershell +Install-Module PSNotes +``` + +Or from source: + +```powershell +# clone repo +git clone https://github.com/mdowst/PSNotes +cd PSNotes +# build solution +. .\tools\build.ps1 +# import module +Import-Module .\bin\PSNotes\\PSNotes.psd1 +``` + +--- + +# Get Started + +## Create Notes + +This example creates a new note for the `Get-AzVM` cmdlet. The `-Run` parameter tells `PSNotes` to automatically execute the snippet when called via the alias. + +```powershell +New-PSNote -Name "List VMs" -Alias vmlist -Snippet "Get-AzVM" -Run $true +``` + +--- + +## Search + +Don't worry if you can't remember the alias you assigned to a note. You can use `Get-PSNote` to search your notes by name, tags, and keywords. + +```powershell +Get-PSNote -Search azure +Get-PSNote -Tag network +``` + +--- + +## Run + +Snippets can be run directly from the `Get-PSNote` search. + +```powershell +Get-PSNote -Name vmlist -Run +``` + +or simply: + +```powershell +vmlist +``` + +--- + +## Copy to Clipboard + +Copying to your clipboard works the same as running. + +```powershell +Get-PSNote vmlist -Copy +``` + +or simply: + +```powershell +vmlist -copy +``` + +--- + +## Export / Import + +Not only does PSNotes allow you to create your own custom notes. It allows you to share them between computers and users. You can create a list of notes export them and share them with your team. Notes are stored in easy to read and edit JSON files in case you want to make manual edits. + +```powershell +Export-PSNote -Catalog Personal -Path .\backup.json +Import-PSNoteCatalog -Path .\backup.json +``` + +You can also import remote catalogs from any accessible URL. These can easily be hosted on GitHub or Gist.\ +Remote catalogs will resync with the source URL everytime the module is loaded.\ +A local cache is maintained incase the URL is unaccessable.\ + +Import with `-AsLocal` will create a local copy that you can update and edit. It does not maintain a sync with the remote list. + +```powershell +Import-RemoteCatalog -Name 'MyRemote' -Url https://gist.githubusercontent.com/example/stuff.json +Import-RemoteCatalog -Name 'CopyLocal' -Url https://gist.githubusercontent.com/example/stuff.json -AsLocal +``` + +Note: PSNotes stores your notes in your local AppData folder using the path `%appdata%\PSNotes` or `home\PSNotes`. + +--- + +## Updating / Migrating + +PSNotes catalogs are versioned. As the internal schema evolves, older catalogs may need to be migrated to the latest format. + +If PSNotes detects older catalogs it will direct you to run the `Update-PSNoteStore` command ensures your catalogs stay compatible without manual intervention. + +```powershell +Update-PSNoteStore +``` + +If a migrated note conflicts with an existing one, the import process will prompt for what actions you want to take. (Overwrite, skip, rename) + +Yes. Migration operates on catalog files and re-imports them through the same structured store logic used by the module. Your notes are preserved and updated to the current schema. + +--- + +# Roadmap + +Planned enhancements: + +* Git-backed catalogs +* Signed remote catalogs +* Cloud sync +* Enhanced TUI navigation +* Keyboard shortcuts +* VSCode Extension +* PSReadLine backup and searching + +--- + +# Architecture + +PSNotes is designed as a structured automation memory system, not just a flat snippet file. + +At its core, PSNotes is built around a versioned note store, catalog separation, and a consistent execution pipeline. + +## Core Components + +### Note Store + +The **Note Store** is the central persistence layer. + +* Backed by JSON +* Versioned using `StoreVersion` +* Automatically migrated when formats evolve +* Managed through structured commands (not direct file edits) + +All catalogs live inside the PSNotes home directory and are loaded through the store layer. + +This allows: + +* Safe schema evolution +* Predictable imports/exports +* Conflict handling +* Future metadata expansion + +### Catalogs + +A catalog is a logical collection of notes. + +Catalogs provide: + +* Isolation (Personal, Team, Lab, etc.) +* Portability (export/import as JSON) +* Remote distribution +* Structured organization at scale + +Catalogs are first-class citizens, not just tags. + +### Notes + +Each note is a structured object containing: + +* `Note` (name) +* `Alias` +* `Snippet` (or script path) +* `Tags` +* `Details` +* `Run` +* `Kind` +* `StoreVersion` + +Notes are not raw text. They are typed automation artifacts. + +This enables: + +* Parameterized execution +* Splatting patterns +* Future structured search +* Metadata-driven filtering +* Execution control + +### Execution Model + +PSNotes turns notes into executable commands. + +When invoked: + +* The store resolves the note +* Execution behavior is determined by `Kind` + * Snippet + * Script path +* The `Run` property controls preview vs execution +* Aliases behave like native commands + +This makes notes feel like real PowerShell functions without writing modules. + +### Remote Catalog Layer + +Remote catalogs allow structured sharing. + +You can: + +* Register remote catalogs +* Cache them locally +* Convert them to local catalogs +* Remove registrations cleanly + +Because catalogs are versioned and schema-driven, remote catalogs can evolve safely. + +## Versioning & Migration + +The store format is versioned. + +When the schema changes: + +* `Update-PSNoteStore` scans for outdated catalogs +* Migrates them to the current format +* Re-imports them using structured conflict rules + +This allows PSNotes to grow without breaking your operational memory. + +## Search & Filtering Engine + +PSNotes supports structured retrieval via: + +* Alias lookup +* Tag filtering +* Catalog filtering +* Text search + +Because notes are stored as structured objects, future enhancements can include: + +* Full-text snippet search +* Regex-based search +* Cross-catalog queries +* Metadata indexing +* Fuzzy matching + +The architecture supports this evolution. + +## Design Principles + +PSNotes was built around a few core ideas: +* First and foremost **Portable over siloed** +* Structured over ad-hoc +* Versioned over fragile +* Composable over static +* Searchable over memorized + +--- + +# License + +[MIT](LICENSE) + diff --git a/Resources/PSNote_Classes.ps1 b/Resources/PSNote_Classes.ps1 deleted file mode 100644 index 512a2cc..0000000 --- a/Resources/PSNote_Classes.ps1 +++ /dev/null @@ -1,109 +0,0 @@ -# Create the PSNote class -class PSNote { - [string]$Note - [string]$Snippet - [string]$Details - [string]$Alias - [string[]]$Tags - [string]$file - - PSNote( - [string]$Note, - [string]$Snippet, - [string]$Details, - [string]$Alias, - [string[]]$Tags - ){ - $this.Note = $Note - $this.Snippet = $Snippet - $this.Details = $Details - $this.Alias = $Alias - $this.Tags = $Tags - $this.File = $script:UserPSNotesJsonFile - - if([string]::IsNullOrEmpty($Alias)){ - $this.Alias = $Note - } - } - - PSNote( - [object]$object - ){ - $this.Note = $object.Note - $this.Snippet = $object.Snippet - $this.Details = $object.Details - $this.Alias = $object.Alias - $this.Tags = $object.Tags - $this.File = $object.File - - if([string]::IsNullOrEmpty($this.Alias)){ - $this.Alias = $object.Note - } - - } -} - -class PSNoteSearch { - [string]$Note - [string]$Snippet - [string]$Details - [string]$Alias - [string[]]$Tags - [string]$file - - PSNoteSearch( - [object]$object - ){ - $this.Note = $object.Note - $this.Snippet = $object.Snippet - $this.Details = $object.Details - $this.Alias = $object.Alias - $this.Tags = $object.Tags - $this.File = $object.File - - if([string]::IsNullOrEmpty($this.Alias)){ - $this.Alias = $object.Note - } - - } -} - -class PSNoteExport { - [string]$Note - [string]$Snippet - [string]$Details - [string]$Alias - [string[]]$Tags - - PSNoteExport( - [object]$object - ){ - $this.Note = $object.Note - $this.Snippet = $object.Snippet - $this.Details = $object.Details - $this.Alias = $object.Alias - $this.Tags = $object.Tags - - if([string]::IsNullOrEmpty($this.Alias)){ - $this.Alias = $object.Note - } - - } -} - -class SplatBlock { - [string]$Command - [string]$ParameterSet - [Boolean]$IsDefault - [string]$HashBlock - [string]$SetBlock - - PSNoteExport( - [object]$object - ){ - $this.Command = $object.Command - $this.IsDefault = $object.IsDefault - $this.HashBlock = $object.HashBlock - $this.SetBlock = $object.SetBlock - } -} \ No newline at end of file diff --git a/src/Classes/NoteStore.class.ps1 b/src/Classes/NoteStore.class.ps1 new file mode 100644 index 0000000..087c46f --- /dev/null +++ b/src/Classes/NoteStore.class.ps1 @@ -0,0 +1,1251 @@ +enum PSNoteKind { + Snippet + Script +} + +# Create the PSNote class +class PSNote { + [string]$Name + [string]$Snippet + [string]$Details + [string]$Alias + [string[]]$Tags + [string]$Catalog + [bool]$Run = $false + [PSNoteKind]$Kind = [PSNoteKind]::Snippet + + PSNote( + [string]$Name, + [string]$Snippet, + [string]$Details, + [string]$Alias, + [string[]]$Tags + ) { + $this.Name = $Name + $this.Snippet = $Snippet + $this.Details = $Details + $this.Alias = $Alias + $this.Tags = $Tags + $this.Catalog = 'Default' + + $this.Kind = [PSNoteKind]::Snippet + } + + PSNote( + [string]$Name, + [string]$Snippet, + [string]$Details, + [string]$Alias, + [string[]]$Tags, + [string]$Catalog, + [bool]$Run + ) { + $this.Name = $Name + $this.Snippet = $Snippet + $this.Details = $Details + $this.Alias = $Alias + $this.Tags = $Tags + $this.Catalog = $Catalog + $this.Run = $Run + + $this.Kind = [PSNoteKind]::Snippet + } + + PSNote( + [string]$Name, + [PSNoteKind]$Kind, + [string]$Snippet, + [string]$Details, + [string]$Alias, + [string[]]$Tags, + [string]$Catalog, + [bool]$Run + ) { + $this.Name = $Name + $this.Kind = $Kind + $this.Snippet = $Snippet + $this.Details = $Details + $this.Alias = $Alias + $this.Tags = $Tags + $this.Catalog = $Catalog + $this.Run = $Run + + } + + PSNote([object]$object) { + $this.Name = $this.GetObjectProperty($object, 'Name') + if ([string]::IsNullOrWhiteSpace($this.Name)) { + $this.Name = $this.GetObjectProperty($object, 'Note') + } + $this.Details = $this.GetObjectProperty($object, 'Details') + $this.Alias = $this.GetObjectProperty($object, 'Alias') + $this.Tags = $this.GetObjectProperty($object, 'Tags') + $this.Catalog = $this.GetObjectProperty($object, 'Catalog') + $this.Snippet = $this.GetObjectProperty($object, 'Snippet') + + # Run (existing behavior) + $objRun = $this.GetObjectProperty($object, 'Run') + $tryRun = $false + if ([bool]::TryParse($objRun, [ref]$tryRun)) { $this.Run = $tryRun } else { $this.Run = $false } + + # --- Kind (new, but tolerate missing/invalid) --- + $kindText = $null + if ($null -ne $object.PSObject.Properties['Kind']) { + $kindText = [string]$object.Kind + } + + if ([string]::IsNullOrWhiteSpace($kindText)) { + $this.Kind = [PSNoteKind]::Snippet + } + else { + try { $this.Kind = [PSNoteKind]::$kindText } + catch { $this.Kind = [PSNoteKind]::Snippet } + } + } + + [object] GetObjectProperty([object]$object, [string]$propertyName) { + if ($null -ne $object.PSObject.Properties[$propertyName]) { + return $object.$propertyName + } + return $null + } + + [string] GetKey() { + return "$($this.Catalog)::$($this.Alias)" + } + + # Optional helper: what do we show in menus? + [string] GetDisplayText() { + $out = switch ($this.Kind) { + Script { "$($this.Alias) (Script)" } + default { "$($this.Alias)" } + } + return $out + } +} + +class NoteCatalog { + static [int] $CurrentStoreVersion = 1 + + [string] $Path + [string] $Catalog + [int] $StoreVersion + [System.Collections.Generic.List[PSNote]] $Notes + [bool] $IsRemote = $false + + NoteCatalog() { + [NoteStore]::InitializeEnvironment() + $this.Path = [NoteCatalog]::ResolvePath('Default', $env:PSNOTES_HOME) + $this.Catalog = 'Default' + $this.StoreVersion = [NoteCatalog]::CurrentStoreVersion + $this.Notes = [System.Collections.Generic.List[PSNote]]::new() + $this.Open() + } + + NoteCatalog([string] $Catalog) { + [NoteStore]::InitializeEnvironment() + if (Test-Path $Catalog) { + $this.Path = $Catalog + } + else { + $this.Path = [NoteCatalog]::ResolvePath($Catalog, $env:PSNOTES_HOME) + } + $this.Catalog = [System.IO.Path]::GetFileNameWithoutExtension($this.Path) + $this.StoreVersion = [NoteCatalog]::CurrentStoreVersion + $this.Notes = [System.Collections.Generic.List[PSNote]]::new() + $this.Open() + } + + NoteCatalog([bool] $blank) { + $this.Path = [NoteCatalog]::ResolvePath('Default', $env:PSNOTES_HOME) + $this.Catalog = [System.IO.Path]::GetFileNameWithoutExtension($this.Path) + $this.StoreVersion = [NoteCatalog]::CurrentStoreVersion + $this.Notes = [System.Collections.Generic.List[PSNote]]::new() + } + + static [string] ResolvePath() { + return [NoteCatalog]::ResolvePath('Default', $env:PSNOTES_HOME) + } + + static [string] ResolvePath([string] $catalog) { + return [NoteCatalog]::ResolvePath($catalog, $env:PSNOTES_HOME) + } + + static [string] ResolvePath([string] $catalog = 'Default', [string] $rootPath = $env:PSNOTES_HOME) { + if ([string]::IsNullOrWhiteSpace($rootPath)) { + $rootPath = Join-Path $env:APPDATA 'PSNotes' + } + if (-not (Test-Path $rootPath)) { + $null = New-Item -Path $rootPath -ItemType Directory -Force + } + + $fileName = if ($catalog -match '\.json$') { $catalog } else { "$catalog.json" } + return (Join-Path $rootPath $fileName) + } + + [void] Open() { + $json = [NoteCatalog]::ReadUtf8NoBom($this.Path) + + if (-not [string]::IsNullOrWhiteSpace($Json)) { + $data = $Json | ConvertFrom-Json -ErrorAction Stop + $dataStoreVersion = $data.psobject.Properties | Where-Object { $_.Name -eq 'StoreVersion' } | Select-Object -ExpandProperty Value + $catalogName = $data.psobject.Properties | Where-Object { $_.Name -eq 'Catalog' } | Select-Object -ExpandProperty Value + if ([string]::IsNullOrWhiteSpace($catalogName)) { + $catalogName = [System.IO.Path]::GetFileNameWithoutExtension($this.Path) + } + $this.Catalog = $catalogName + # Migration / backward-compat: + # If older store was just an array of notes, wrap it. + if ($dataStoreVersion -ne $this.StoreVersion) { + Write-Warning "Note catalog store version mismatch in $($this.Catalog). Expected: $($this.StoreVersion), Found: $($dataStoreVersion)`n Notes from $($this.Catalog) will not be loaded.`n`n Run 'Update-PSNoteStore' to migrate legacy store catalogs.`n" + return + } + + $data.Notes | ForEach-Object { + $note = [PSNote]::New($_) + if ([string]::IsNullOrWhiteSpace($note.Catalog)) { + $note.Catalog = $this.Catalog + } + $this.Notes.Add($note) + } + } + + if ($null -eq $this.Catalog) { + $this.Catalog = [System.IO.Path]::GetFileNameWithoutExtension($this.Path) + } + # Future: if ($store.StoreVersion -lt CurrentStoreVersion) { $store.Migrate() } + } + + static [NoteCatalog] Open([string] $catalogPath) { + $store = [NoteCatalog]::new($true) + $store.Path = $catalogPath + $store.Open() + + return $store + } + + static [bool] VersionCheck([string] $catalogPath) { + $currentVersion = $false + $json = [NoteCatalog]::ReadUtf8NoBom($catalogPath) + + if (-not [string]::IsNullOrWhiteSpace($Json)) { + $data = $Json | ConvertFrom-Json -ErrorAction Stop + $dataStoreVersion = $data.psobject.Properties | Where-Object { $_.Name -eq 'StoreVersion' } | Select-Object -ExpandProperty Value + + # If store version matches current, return true + if ($dataStoreVersion -eq [NoteCatalog]::CurrentStoreVersion) { + $currentVersion = $true + } + } + return $currentVersion + } + + static [hashtable] ValidateNotes([string] $catalogPath) { + $result = @{ + IsValid = $true + StoreVersion = $null + Errors = [System.Collections.Generic.List[string]]::new() + Warnings = [System.Collections.Generic.List[string]]::new() + } + + try { + $json = [NoteCatalog]::ReadUtf8NoBom($catalogPath) + + if ([string]::IsNullOrWhiteSpace($json)) { + $result.IsValid = $false + $result.Errors.Add("File is empty or does not exist") + return $result + } + + $data = $json | ConvertFrom-Json -ErrorAction Stop + + # Check the store version + $dataStoreVersion = $data.psobject.Properties | Where-Object { $_.Name -eq 'StoreVersion' } | Select-Object -ExpandProperty Value + if ($dataStoreVersion -eq [NoteCatalog]::CurrentStoreVersion) { + $result.StoreVersion = 'Current' + } + elseif (-not [string]::IsNullOrWhiteSpace($dataStoreVersion)) { + $result.StoreVersion = $dataStoreVersion + $result.Warnings.Add("Note catalog store version mismatch. Expected: $([NoteCatalog]::CurrentStoreVersion), Found: $dataStoreVersion") + } + else { + $result.StoreVersion = 'Legacy' + } + # Determine if this is old format (array) or new format (object with Notes property) + $legacyNotes = $data.psobject.Properties | Where-Object { $_.Name -eq 'Note' } | Select-Object -ExpandProperty Value + $currentNotes = $data.psobject.Properties | Where-Object { $_.Name -eq 'Notes' } | Select-Object -ExpandProperty Value + $vnotes = if ($data -is [array] -or $null -ne $legacyNotes) { + $data + $result.Warnings.Add("Legacy note catalog format detected (array). Consider migrating to new format.") + } + elseif ($null -ne $currentNotes) { + $data.Notes + } + else { + $result.IsValid = $false + $result.Errors.Add("Invalid JSON structure: expected array or object with 'Notes' property") + return $result + } + + if ($null -eq $vnotes -or @($vnotes).Count -eq 0) { + $result.IsValid = $false + $result.Errors.Add("No notes found in catalog") + return $result + } + + $index = 0 + foreach ($note in $vnotes) { + $noteErrors = [System.Collections.Generic.List[string]]::new() + + # Check for Name property (or legacy Note) + $noteValue = $null + if ($null -ne $note.PSObject.Properties['Name']) { + $noteValue = [string]$note.Name + } + elseif ($null -ne $note.PSObject.Properties['Note']) { + $noteValue = [string]$note.Note + } + if ([string]::IsNullOrWhiteSpace($noteValue)) { + $noteErrors.Add("Note[$index]: Missing or empty 'Name' property") + } + + # Check for Snippet property + $snippetValue = $null + if ($null -ne $note.PSObject.Properties['Snippet']) { + $snippetValue = [string]$note.Snippet + } + if ([string]::IsNullOrWhiteSpace($snippetValue)) { + $noteErrors.Add("Note[$index]: Missing or empty 'Snippet' property") + } + + # Additional validation: warn if Alias is missing (will default to Name) + if ($null -eq $note.PSObject.Properties['Alias'] -or [string]::IsNullOrWhiteSpace([string]$note.Alias)) { + if (-not [string]::IsNullOrWhiteSpace($noteValue)) { + $result.Warnings.Add("Note[$index] '$noteValue': Missing 'Alias' property (will default to Name)") + } + } + + if ($noteErrors.Count -gt 0) { + $result.IsValid = $false + foreach ($err in $noteErrors) { + $result.Errors.Add($err) + } + } + + $index++ + } + } + catch { + $result.IsValid = $false + $result.Errors.Add("Failed to parse JSON: $($_.Exception.Message)") + } + + return $result + } + + static [NoteCatalog] Migrate([string] $catalogPath, [bool] $backup = $true) { + $migrate = [NoteCatalog]::new($true) + $migrate.Path = $catalogPath + $migrate.Catalog = [System.IO.Path]::GetFileNameWithoutExtension($catalogPath) + + # Read the old format JSON + $json = [NoteCatalog]::ReadUtf8NoBom($catalogPath) + if ([string]::IsNullOrWhiteSpace($json)) { + return $migrate + } + + try { + $oldData = $json | ConvertFrom-Json -ErrorAction Stop + $fileName = [System.IO.Path]::GetFileName($catalogPath) + $backupDir = Join-Path $env:PSNOTES_HOME 'backups' + $backupStamp = (Get-Date).ToString('yyyyMMdd_HHmmssfff') + $backupPath = Join-Path $backupDir "$fileName.pre-migration.$backupStamp.bak" + $tempPath = Join-Path $backupDir "$fileName.$backupStamp.tmp" + $migrate.Path = $tempPath + # Backup the original file before migration + if (-not (Test-Path $backupDir)) { + $null = New-Item -Path $backupDir -ItemType Directory -Force + } + [System.IO.File]::Copy($catalogPath, $backupPath, $true) + [System.IO.File]::Copy($catalogPath, $tempPath, $true) + + # Convert old format (array) to new format (object with StoreVersion, Catalog, Notes) + # Old format: [{ Note, Snippet, Details, Alias, Tags }, ...] + # New format: { StoreVersion, Catalog, Notes: [...] } + $oldData | ForEach-Object { + $note = [PSNote]::New($_) + if ([string]::IsNullOrWhiteSpace($note.Catalog)) { + $note.Catalog = $migrate.Catalog + } + $migrate.Notes.Add($note) + } + + if ($backup) { + # Save in new format using atomic backup + [NoteCatalog]::AtomicSaveWithBackup($tempPath, $json, 'migrationv1-') + + # Confirm that $catalogPath and $backupPath are the same and if so delete $catalogPath + if ((Get-FileHash -Path $catalogPath).Hash -eq (Get-FileHash -Path $backupPath).Hash) { + [System.IO.File]::Delete($catalogPath) + } + + Write-Verbose "Successfully migrated $fileName to new format. Backup saved to $backupPath" + } + $migrate.Save() + } + catch { + throw "Failed to migrate catalog at $catalogPath : $_" + } + + return $migrate + } + + [void] RemoveNote([string] $note, [bool] $save = $true) { + $remove = $this.Notes | Where-Object { $_.Name -eq $note } + if ($remove) { + $this.Notes.Remove($remove) | Out-Null + if ($save) { + $this.Save() + } + } + else { + Write-Warning "Note '$note' not found in catalog. No action taken." + } + } + + [string] ToJson() { + $obj = [pscustomobject]@{ + StoreVersion = $this.StoreVersion + Catalog = $this.Catalog + Notes = @($this.Notes | Select-Object -Property * -ExcludeProperty Catalog) + } + return ($obj | ConvertTo-Json -Depth 10) + } + + [void] Save() { + # Always write current version + $this.StoreVersion = [NoteCatalog]::CurrentStoreVersion + + $json = $this.ToJson() + [NoteCatalog]::AtomicSaveWithBackup($this.Path, $json) + } + + static [System.IO.FileStream] AcquireLock([string] $path, [int] $timeoutMs = 5000, [int] $retryDelayMs = 50) { + $sw = [System.Diagnostics.Stopwatch]::StartNew() + $last = $null + + while ($sw.ElapsedMilliseconds -lt $timeoutMs) { + try { + # Lock the store file itself; write-through this stream in Save() + return [System.IO.File]::Open( + $path, + [System.IO.FileMode]::OpenOrCreate, + [System.IO.FileAccess]::ReadWrite, + [System.IO.FileShare]::None + ) + } + catch { + $last = $_ + Start-Sleep -Milliseconds $retryDelayMs + } + } + + throw "Timed out acquiring lock for note store file: $path. Last error: $($last.Exception.Message)" + } + + static [string] ReadUtf8NoBom([string] $path) { + if (-not (Test-Path $path)) { return $null } + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + $fs = [System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + try { + $sr = [System.IO.StreamReader]::new($fs, $utf8NoBom, $true) + try { return $sr.ReadToEnd() } finally { $sr.Dispose() } + } + finally { $fs.Dispose() } + } + + static [void] AtomicSaveWithBackup([string] $path, [string] $content) { + [NoteCatalog]::AtomicSaveWithBackup($path, $content, '') + } + + static [void] AtomicSaveWithBackup([string] $path, [string] $content, [string] $prefix) { + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + $directory = $ENV:PSNOTES_HOME + $fileName = [System.IO.Path]::GetFileName($path) + $tempPath = Join-Path $directory "$fileName.tmp" + $backupDir = Join-Path $directory 'backups' + $backupStamp = (Get-Date).ToString('yyyyMMdd_HHmmssfff') + $backupPath = Join-Path $backupDir "$prefix$fileName.$backupStamp.bak" + + # Ensure directory exists + if (-not (Test-Path $directory)) { + $null = New-Item -Path $directory -ItemType Directory -Force + } + # Ensure backup directory exists + if (-not (Test-Path $backupDir)) { + $null = New-Item -Path $backupDir -ItemType Directory -Force + } + + try { + # Step 1: Write to temp file + [System.IO.File]::WriteAllText($tempPath, $content, $utf8NoBom) + + # Step 2: Acquire lock on the target file (or create if doesn't exist) + $lockStream = [NoteCatalog]::AcquireLock($path, 5000, 50) + try { + $lockStream.Close() + $lockStream.Dispose() + + # Step 3: Create backup if original exists + if (Test-Path $path) { + # Copy current file to timestamped backup + [System.IO.File]::Copy($path, $backupPath, $true) + + # Keep only the last 10 backups for this file + $backupPattern = "$fileName.*.bak" + $oldBackups = Get-ChildItem -Path $backupDir -Filter $backupPattern -File | + Sort-Object -Property LastWriteTime -Descending | + Select-Object -Skip 10 + foreach ($old in $oldBackups) { + try { [System.IO.File]::Delete($old.FullName) } + catch { Write-Error "Failed to delete old backup file: $($_.Exception.Message)" } + } + } + + # Step 4: Replace original with temp (atomic operation on NTFS/most filesystems) + [System.IO.File]::Copy($tempPath, $path, $true) + + # Step 5: Clean up temp file + if (Test-Path $tempPath) { + [System.IO.File]::Delete($tempPath) + } + } + catch { + # If anything goes wrong, ensure lock is released + if ($null -ne $lockStream) { + try { $lockStream.Dispose() } + catch { Write-Error "Failed to dispose lock stream: $_" } + } + throw + } + } + catch { + # Clean up temp file if it exists + if (Test-Path $tempPath) { + try { [System.IO.File]::Delete($tempPath) } + catch { Write-Error "Failed to delete temp file: $_" } + } + throw "Failed to save note store atomically: $_" + } + } +} + +class RemoteCatalogSource { + [string] $Name + [string] $Url + [string] $CacheFile # relative to PSNOTES_HOME (recommended) + [string] $LastSync # ISO 8601 string + [string] $ETag + [string] $LastModified + [bool] $Enabled = $true + + RemoteCatalogSource() {} + + RemoteCatalogSource([string] $name, [string] $url, [string] $cacheFile) { + $this.Name = $name + $this.Url = $url + $this.CacheFile = $cacheFile + $this.Enabled = $true + } +} + +class NoteConfigStore { + static [int] $CurrentVersion = 1 + + [string] $Path + [int] $Version + [System.Collections.Generic.List[RemoteCatalogSource]] $RemoteCatalogs + + NoteConfigStore() { + $configPath = Join-Path $env:PSNOTES_HOME 'config' + if (-not (Test-Path $configPath)) { $null = New-Item -Path $configPath -ItemType Directory -Force } + + $this.Path = (Join-Path $configPath 'psnoteconfigstore.json') + $this.Version = [NoteConfigStore]::CurrentVersion + $this.RemoteCatalogs = [System.Collections.Generic.List[RemoteCatalogSource]]::new() + $this.Open() + } + + [void] Open() { + if (-not (Test-Path $this.Path)) { return } + + try { + $json = [NoteCatalog]::ReadUtf8NoBom($this.Path) + if ([string]::IsNullOrWhiteSpace($json)) { return } + + $data = $json | ConvertFrom-Json -ErrorAction Stop + + if ($data.RemoteCatalogs) { + foreach ($rc in $data.RemoteCatalogs) { + $r = [RemoteCatalogSource]::new() + $r.Name = [string]$rc.Name + $r.Url = [string]$rc.Url + $r.CacheFile = [string]$rc.CacheFile + $r.LastSync = [string]$rc.LastSync + $r.ETag = [string]$rc.ETag + $r.LastModified = [string]$rc.LastModified + + $enabled = $true + if ($null -ne $rc.PSObject.Properties['Enabled']) { + [void][bool]::TryParse([string]$rc.Enabled, [ref]$enabled) + } + $r.Enabled = $enabled + + if (-not [string]::IsNullOrWhiteSpace($r.Url)) { + $this.RemoteCatalogs.Add($r) | Out-Null + } + } + } + } + catch { + # fail safe + return + } + } + + [void] Save() { + $json = ($this | ConvertTo-Json -Depth 8) + [NoteCatalog]::AtomicSaveWithBackup($this.Path, $json) + } +} + +class NoteMetadataStore { + static [int] $CurrentVersion = 1 + + [string] $Path + [int] $Version + [System.Collections.Generic.HashSet[string]] $Favorites + + NoteMetadataStore() { + $metaPath = Join-Path $env:PSNOTES_HOME 'config' + if (-not (Test-Path $metaPath)) { + $null = New-Item -Path $metaPath -ItemType Directory -Force + } + $this.Path = (Join-Path $metaPath 'psnotemetadatastore.json') + $this.Version = [NoteMetadataStore]::CurrentVersion + $this.Favorites = [System.Collections.Generic.HashSet[string]]::new() + $this.Open() + } + + NoteMetadataStore([string] $path) { + $this.Path = $path + $this.Version = [NoteMetadataStore]::CurrentVersion + $this.Favorites = [System.Collections.Generic.HashSet[string]]::new() + $this.Open() + } + + [void] Open() { + if (-not (Test-Path $this.Path)) { return } + + try { + $json = [NoteCatalog]::ReadUtf8NoBom($this.Path) + if ([string]::IsNullOrWhiteSpace($json)) { return } + + $data = $json | ConvertFrom-Json -ErrorAction Stop + if ($data.Favorites) { + foreach ($k in $data.Favorites) { + if (-not [string]::IsNullOrWhiteSpace([string]$k)) { + $null = $this.Favorites.Add([string]$k) + } + } + } + } + catch { + # If metadata is corrupt, fail safe (don't crash UI). You can add logging later. + return + } + } + + [string] ToJson() { + return ($this | ConvertTo-Json -Depth 5) + } + + [void] Save() { + $json = $this.ToJson() + [NoteCatalog]::AtomicSaveWithBackup($this.Path, $json) + } +} + +class NoteStore { + static [int] $CurrentStoreVersion = 1 + + [System.Collections.Generic.List[NoteCatalog]] $Catalogs + [System.Collections.Generic.List[PSNote]] $Notes + [NoteMetadataStore] $Metadata + [NoteConfigStore] $Config + + NoteStore() { + [NoteStore]::InitializeEnvironment() + $this.Notes = [System.Collections.Generic.List[PSNote]]::new() + $this.Catalogs = [System.Collections.Generic.List[NoteCatalog]]::new() + $this.Metadata = [NoteMetadataStore]::new() + $this.Config = [NoteConfigStore]::new() + $this.LoadLocalCatalogs() + $this.LoadRemoteCatalogs() + $this.InitializeAliases() + } + + [void] LoadLocalCatalogs() { + $catalogFiles = Get-ChildItem -Path $env:PSNOTES_HOME -Filter '*.json' -File -ErrorAction SilentlyContinue + if ($catalogFiles) { + foreach ($file in $catalogFiles) { + try { + $this.LoadCatalog($file.BaseName) + } + catch { + Write-Warning "Failed to load note catalog from file '$($file.FullName)': $($_.Exception.Message)" + } + } + } + } + + static [void] InitializeEnvironment() { + if ([string]::IsNullOrEmpty($env:PSNOTES_HOME)) { + if (Get-Variable -Name IsLinux -Scope Global -ValueOnly -ErrorAction SilentlyContinue) { + $env:PSNOTES_HOME = '/home/PSNotes' + } + else { + $env:PSNOTES_HOME = Join-Path $env:APPDATA 'PSNotes' + } + } + } + + hidden [NoteCatalog] GetCatalogObject([string] $catalogName) { + return $this.Catalogs | + Where-Object { $_.Catalog -eq $catalogName } | + Select-Object -First 1 + } + + [void] LoadCatalog([string] $catalogName) { + $catalog = [NoteCatalog]::new($catalogName) + $this.LoadCatalog($catalog) + } + + [void] LoadCatalog([NoteCatalog] $catalog) { + $catalog.Notes | ForEach-Object { + $newNote = $_ + $dup = if (-not [string]::IsNullOrWhiteSpace($newNote.Alias)) { + $this.Notes | Where-Object { $_.Alias -eq $newNote.Alias } + } + else { + $this.Notes | Where-Object { $_.Name -eq $newNote.Name } + } + if ($dup -and $dup.Catalog -ne $newNote.Catalog) { + Write-Warning "Duplicate Alias found: $($newNote.Alias). Skipping note: $($newNote.Name)" + } + elseif (-not $dup) { + $this.Notes.Add($newNote) + } + } + if (-not ($this.Catalogs | Where-Object { $_.Catalog -eq $catalog.Catalog })) { + $this.Catalogs.Add($catalog) + } + } + + [void] InitializeAliases() { + $this.Notes | ForEach-Object { + Write-Debug "Alias : $($_.Alias)" + if ([string]::IsNullOrWhiteSpace($_.Alias)) { + Write-Verbose "Note '$( $_.Name )' has an empty Alias. Skipping alias creation." + } + else { + Set-Alias -Name $_.Alias -Value Get-PSNoteAlias -Scope Global -Force + } + } + } + + [void] Save() { + $this.Catalogs | ForEach-Object { + $_.Save() + } + } + + [void] AddNote([PSNote] $note) { + # Block adding notes into remote catalogs + $cat = $this.GetCatalogObject($note.Catalog) + if ($cat -and $cat.IsRemote) { + Write-Warning "Cannot add notes to remote catalog '$($note.Catalog)'. Remote catalogs are read-only." + return + } + + $this.Notes.Add($note) | Out-Null + + if (-not ($this.Catalogs | Where-Object { $_.Catalog -eq $note.Catalog })) { + $newCatalog = [NoteCatalog]::new($note.Catalog) + $this.Catalogs.Add($newCatalog) + } + + $catalogUpdates = $this.Catalogs | Where-Object { $_.Catalog -eq $note.Catalog } | ForEach-Object { + $_.Notes.Add($note) | Out-Null + $_ + } + $catalogUpdates | ForEach-Object { + $_.Save() + $this.LoadCatalog($_) + } + } + + [void] RemoveNote([string] $note, [string] $catalog) { + $this.RemoveNote($note, $catalog, $true) + } + + [void] RemoveNote([string] $note, [string] $catalog, [bool] $reload) { + # Block removing notes from remote catalogs + $cat = $this.GetCatalogObject($catalog) + if ($cat -and $cat.IsRemote) { + Write-Warning "Cannot remove notes from remote catalog '$catalog'. Remote catalogs are read-only." + return + } + + $remove = $this.Notes | Where-Object { $_.Name -eq $note -and $_.Catalog -eq $catalog } + if ($remove) { + $this.Notes.Remove($remove) | Out-Null + $catalogUpdates = $this.Catalogs | Where-Object { $_.Catalog -eq $remove.Catalog } | ForEach-Object { + $_.Notes.Remove($remove) | Out-Null + $_ + } + if ( $reload ) { + # TODO: $this.Metadata.RemoveFavorite($remove) + $catalogUpdates | ForEach-Object { + $_.Save() + $this.LoadCatalog($_) + } + } + } + else { + Write-Warning "Note '$note' not found in catalog '$catalog'. No action taken." + } + } + + [void] UpdateNote([PSNote] $note) { + + $noteName = if ($null -ne $note.PSObject.Properties['Name']) { + [string]$note.Name + } + elseif ($null -ne $note.PSObject.Properties['Note']) { + [string]$note.Note + } + + # BUGFIX: this was checking 'Kind' when you meant 'Catalog' + $noteCatalog = if ($null -ne $note.PSObject.Properties['Catalog']) { + [string]$note.Catalog + } + + $update = $this.Notes | Where-Object { $_.Name -eq $noteName } | Select-Object -First 1 + + if (-not $update) { + Write-Warning "Note '$($noteName)' not found in catalog '$($noteCatalog)'. No action taken." + return + } + + # Block updating remote notes (based on where the existing note lives) + $existingCatalog = $this.GetCatalogObject($update.Catalog) + if ($existingCatalog -and $existingCatalog.IsRemote) { + Write-Warning "Cannot update note '$($update.Name)' because it belongs to remote catalog '$($update.Catalog)'. Remote notes are read-only." + return + } + + # Also block moving/updating into a remote catalog + $targetCatalog = $this.GetCatalogObject($note.Catalog) + if ($targetCatalog -and $targetCatalog.IsRemote) { + Write-Warning "Cannot move note into remote catalog '$($note.Catalog)'. Remote catalogs are read-only." + return + } + + if ($update.Catalog -eq $noteCatalog) { + $this.RemoveNote($noteName, $noteCatalog, $false) + $this.AddNote($note) + } + else { + Write-Warning "Note '$($noteName)' exists in catalog '$($update.Catalog)'. Cannot update note in different catalog at this time '$($noteCatalog)'. No action taken." + } + } + + [void] MoveNote([PSNote] $Note, [string] $DestinationCatalog, [bool] $Force) { + + if (-not $Note) { throw "Note is required." } + if ([string]::IsNullOrWhiteSpace($Note.Catalog)) { throw "Input note must have a Catalog." } + if ([string]::IsNullOrWhiteSpace($DestinationCatalog)) { throw "DestinationCatalog is required." } + + $sourceCatalogName = [string]$Note.Catalog + + if ($DestinationCatalog -eq $sourceCatalogName) { + throw "DestinationCatalog is the same as SourceCatalog ('$sourceCatalogName'). Nothing to do." + } + + $srcCatalog = $this.GetCatalogObject($sourceCatalogName) + if (-not $srcCatalog) { throw "Source catalog '$sourceCatalogName' not found." } + if ($srcCatalog.IsRemote) { throw "Cannot move notes from remote catalog '$sourceCatalogName'. Remote catalogs are read-only." } + + $dstCatalog = $this.GetCatalogObject($DestinationCatalog) + if ($dstCatalog -and $dstCatalog.IsRemote) { + throw "Cannot move notes into remote catalog '$DestinationCatalog'. Remote catalogs are read-only." + } + + # Create destination catalog if missing (local) + if (-not $dstCatalog) { + $dstCatalog = [NoteCatalog]::new($DestinationCatalog) + $dstCatalog.IsRemote = $false + $this.Catalogs.Add($dstCatalog) | Out-Null + } + + # Minimal identity: prefer Alias, else Note + $keyAlias = [string]$Note.Alias + $keyName = [string]$Note.Name + + $sourceMatch = if (-not [string]::IsNullOrWhiteSpace($keyAlias)) { + $srcCatalog.Notes | Where-Object { $_.Alias -eq $keyAlias } | Select-Object -First 1 + } + else { + $srcCatalog.Notes | Where-Object { $_.Name -eq $keyName } | Select-Object -First 1 + } + + if (-not $sourceMatch) { + throw "The provided note does not exist in source catalog '$sourceCatalogName' (by Alias/Note)." + } + + # Destination conflict (Alias wins, else Note) + $destConflict = $null + if (-not [string]::IsNullOrWhiteSpace($keyAlias)) { + $destConflict = $dstCatalog.Notes | Where-Object { $_.Alias -eq $keyAlias } | Select-Object -First 1 + } + if (-not $destConflict -and -not [string]::IsNullOrWhiteSpace($keyName)) { + $destConflict = $dstCatalog.Notes | Where-Object { $_.Name -eq $keyName } | Select-Object -First 1 + } + + if ($destConflict -and -not $Force) { + throw "A note already exists in '$DestinationCatalog' with the same Alias/Note. Use -Force to overwrite." + } + + # Preserve favorite status across the move (key includes Catalog) + $wasFavorite = $this.IsFavorite($sourceMatch) + if ($wasFavorite) { $this.RemoveFavorite($sourceMatch) } + + # Remove from source + $null = $srcCatalog.Notes.Remove($sourceMatch) + + # Overwrite destination if forced + if ($destConflict -and $Force) { + $null = $dstCatalog.Notes.Remove($destConflict) + } + + # Move (mutate the same object) + $sourceMatch.Catalog = $DestinationCatalog + $dstCatalog.Notes.Add($sourceMatch) | Out-Null + + # Persist both catalogs + $srcCatalog.Save() + $dstCatalog.Save() + + if ($wasFavorite) { $this.AddFavorite($sourceMatch) } + + # Refresh in-memory views (mirrors your Add/Remove patterns) + $this.LoadCatalog($srcCatalog) + $this.LoadCatalog($dstCatalog) + $this.InitializeAliases() + } + + hidden static [string] GetRemoteCacheRoot() { + $root = Join-Path $env:PSNOTES_HOME 'remote' + if (-not (Test-Path $root)) { $null = New-Item -Path $root -ItemType Directory -Force } + return $root + } + + hidden static [string] GetRemoteCacheFileName([string] $url) { + $sha = [System.Security.Cryptography.SHA256]::Create() + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($url) + $hash = ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' + return "$hash.json" + } + finally { $sha.Dispose() } + } + + [RemoteCatalogSource] RegisterRemoteCatalog([string] $name, [string] $Url) { + if (-not $this.Config) { $this.Config = [NoteConfigStore]::new() } + if (-not $this.Config.RemoteCatalogs) { + $this.Config.RemoteCatalogs = [System.Collections.Generic.List[RemoteCatalogSource]]::new() + } + + $existing = $this.Config.RemoteCatalogs | Where-Object { $_.Url -eq $Url } | Select-Object -First 1 + if ($existing) { return $existing } + + $cacheRoot = [NoteStore]::GetRemoteCacheRoot() + $cacheFile = Join-Path $cacheRoot ([NoteStore]::GetRemoteCacheFileName($Url)) + $rel = [System.IO.Path]::GetRelativePath($env:PSNOTES_HOME, $cacheFile) + + $entry = [RemoteCatalogSource]::new($name, $Url, $rel) + $this.Config.RemoteCatalogs.Add($entry) | Out-Null + $this.Config.Save() + $this.LoadRemoteCatalogs() # Optional: load immediately after registering + return $entry + } + + [void] SyncRemoteCatalogs() { + if (-not $this.Config -or -not $this.Config.RemoteCatalogs) { return } + + foreach ($rc in $this.Config.RemoteCatalogs) { + if (-not $rc.Enabled) { continue } + if ([string]::IsNullOrWhiteSpace($rc.Url)) { continue } + if ([string]::IsNullOrWhiteSpace($rc.CacheFile)) { continue } + + $cachePath = Join-Path $env:PSNOTES_HOME $rc.CacheFile + $cacheDir = Split-Path $cachePath -Parent + if (-not (Test-Path $cacheDir)) { $null = New-Item -Path $cacheDir -ItemType Directory -Force } + + try { + $headers = @{} + if (-not [string]::IsNullOrWhiteSpace($rc.ETag)) { $headers['If-None-Match'] = $rc.ETag } + if (-not [string]::IsNullOrWhiteSpace($rc.LastModified)) { $headers['If-Modified-Since'] = $rc.LastModified } + Write-Debug "Syncing remote catalog from $($rc.Url) with headers: $($headers | Out-String)" + # Note: If the remote server supports ETag or Last-Modified, this will save bandwidth and be faster. If not, it will just download every time. + # This is disabled for now because GIST was not playing nice with conditional requests, but you can enable it if your server supports it well. + $resp = Invoke-WebRequest -Uri $rc.Url -UseBasicParsing -ErrorAction Stop + + $resp.Content | Set-Content -Path $cachePath -Encoding UTF8 + + $rc.LastSync = (Get-Date).ToUniversalTime().ToString('o') + if ($resp.Headers.ETag) { $rc.ETag = $resp.Headers.ETag } + if ($resp.Headers.'Last-Modified') { $rc.LastModified = $resp.Headers.'Last-Modified' } + } + catch { + if (Test-Path $cachePath) { + Write-Warning "Remote catalog unreachable ($($rc.Url)). Using cached version: $cachePath" + } + else { + Write-Warning "Remote catalog unreachable ($($rc.Url)) and no cached copy exists. Skipping." + } + } + } + + $this.Config.Save() + } + + [void] LoadRemoteCatalogs() { + if (-not $this.Config -or -not $this.Config.RemoteCatalogs) { return } + + # Ensure cache is fresh first + $this.SyncRemoteCatalogs() + + foreach ($rc in $this.Config.RemoteCatalogs) { + if (-not $rc.Enabled) { continue } + + $cachePath = Join-Path $env:PSNOTES_HOME $rc.CacheFile + if (-not (Test-Path $cachePath)) { continue } + + try { + # NOTE: This assumes the remote JSON is in NoteCatalog format (or legacy array that ValidateNotes already tolerates) + # If you want to support "bundle of catalogs", we can extend this to detect .Catalogs and split. + if (-not [NoteCatalog]::VersionCheck($cachePath)) { + # version mismatch will warn later when opened; you can decide whether to block + } + + $remoteCatalog = [NoteCatalog]::new($cachePath) + $remoteCatalog.Catalog = $rc.Name # Override catalog name with the friendly name from config + $remoteCatalog.IsRemote = $true + $remoteCatalog.Notes | ForEach-Object { $_.Catalog = $remoteCatalog.Catalog } # Ensure notes point to the remote catalog name + # Important: Load AFTER locals. Your existing duplicate behavior makes locals win. + $this.LoadCatalog($remoteCatalog) + } + catch { + Write-Warning "Failed to load remote catalog cache '$cachePath': $($_.Exception.Message)" + } + } + } + + hidden [RemoteCatalogSource] RemoveRemoteCatalog([string] $Url, [bool] $ConvertToLocal, [bool] $Force) { + if (-not $this.Config) { $this.Config = [NoteConfigStore]::new() } + + if (-not $this.Config.RemoteCatalogs -or $this.Config.RemoteCatalogs.Count -eq 0) { + throw "No remote catalogs are registered." + } + + $match = $this.Config.RemoteCatalogs | + Where-Object { $_.Url -eq $Url } | + Select-Object -First 1 + + if (-not $match) { + throw "Remote catalog not found: $Url" + } + + $cachePath = $null + if (-not [string]::IsNullOrWhiteSpace($match.CacheFile)) { + $cachePath = Join-Path $env:PSNOTES_HOME $match.CacheFile + } + + if ($ConvertToLocal) { + + if (-not $cachePath -or -not (Test-Path $cachePath)) { + throw "Cannot convert to local because no cached catalog exists for '$Url'." + } + + # Open cached remote catalog + $remoteCatalog = [NoteCatalog]::Open($cachePath) + + # Prefer the registered friendly remote name when creating the local catalog. + # This keeps local file naming consistent with how remotes are presented/loaded. + $localCatalogName = if (-not [string]::IsNullOrWhiteSpace($match.Name)) { + $match.Name + } + else { + $remoteCatalog.Catalog + } + + # Determine local destination path using your static resolver + $localPath = [NoteCatalog]::ResolvePath($localCatalogName) + + if ((Test-Path $localPath) -and -not $Force) { + throw "Local catalog already exists: '$($remoteCatalog.Catalog)'. Use -Force to overwrite." + } + + # Create a new local catalog object + $localCatalog = [NoteCatalog]::new($localCatalogName) + $localCatalog.IsRemote = $false + $localCatalog.Notes = [System.Collections.Generic.List[PSNote]]::new() + + foreach ($n in @($remoteCatalog.Notes)) { + $n.Catalog = $localCatalog.Catalog + $localCatalog.Notes.Add($n) | Out-Null + } + + $localCatalog.Save() + + # Load local version into store + $this.LoadCatalog($localCatalog) + } + + # Remove from config + $null = $this.Config.RemoteCatalogs.Remove($match) + $this.Config.Save() + + # Remove remote catalog object from in-memory store + if ($cachePath) { + $loadedRemote = $this.Catalogs | + Where-Object { $_.Path -eq $cachePath } | + Select-Object -First 1 + + if ($loadedRemote) { + $null = $this.Catalogs.Remove($loadedRemote) + + # Rebuild Notes collection cleanly + $this.Notes = [System.Collections.Generic.List[PSNote]]::new() + foreach ($c in $this.Catalogs) { + foreach ($n in @($c.Notes)) { + $this.Notes.Add($n) | Out-Null + } + } + + $this.InitializeAliases() + } + } + + return $match + } + + hidden [NoteCatalog] ImportRemoteCatalogAsLocal([string] $Name, [string] $Url, [bool] $Force) { + if ([string]::IsNullOrWhiteSpace($Name)) { throw "Name is required." } + if ([string]::IsNullOrWhiteSpace($Url)) { throw "Url is required." } + + # Download the remote JSON + $resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop + $json = [string]$resp.Content + if ([string]::IsNullOrWhiteSpace($json)) { + throw "Remote catalog returned empty content: $Url" + } + + # Save to a temp file so we can reuse existing NoteCatalog.Open() parsing/validation + $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("psnotes_remote_{0}.json" -f ([guid]::NewGuid().ToString('N'))) + try { + $json | Set-Content -Path $tmp -Encoding UTF8 -ErrorAction Stop + + $remoteCatalog = [NoteCatalog]::Open($tmp) + + # Use the provided Name as the local catalog name (canonical) + $localName = $Name + $localPath = [NoteCatalog]::ResolvePath($localName) + + if ((Test-Path $localPath) -and -not $Force) { + throw "Local catalog '$localName' already exists. Use -Force to overwrite." + } + + $localCatalog = [NoteCatalog]::new($localName) + $localCatalog.IsRemote = $false + $localCatalog.Notes = [System.Collections.Generic.List[PSNote]]::new() + + foreach ($n in @($remoteCatalog.Notes)) { + # Normalize to the local catalog name + $n.Catalog = $localName + $localCatalog.Notes.Add($n) | Out-Null + } + + $localCatalog.Save() + $this.LoadCatalog($localCatalog) + $this.InitializeAliases() + + return $localCatalog + } + finally { + if (Test-Path $tmp) { Remove-Item -Path $tmp -Force -ErrorAction SilentlyContinue } + } + } + + [string] GetNoteKey([PSNote] $note) { + return "$($note.Catalog)::$($note.Name)::$($note.Alias)" + } + + [bool] IsFavorite([PSNote] $note) { + if (-not $this.Metadata) { return $false } + return $this.Metadata.Favorites.Contains($this.GetNoteKey($note)) + } + + [void] AddFavorite([PSNote] $note) { + if (-not $this.Metadata) { return } + $key = $this.GetNoteKey($note) + if ($this.Metadata.Favorites.Add($key)) { $this.Metadata.Save() } + } + + [void] RemoveFavorite([PSNote] $note) { + if (-not $this.Metadata) { return } + $key = $this.GetNoteKey($note) + if ($this.Metadata.Favorites.Remove($key)) { $this.Metadata.Save() } + } + + [bool] ToggleFavorite([PSNote] $note) { + if (-not $this.Metadata) { return $false } + + $key = $this.GetNoteKey($note) + if ($this.Metadata.Favorites.Contains($key)) { + $null = $this.Metadata.Favorites.Remove($key) + $this.Metadata.Save() + return $false + } + else { + $null = $this.Metadata.Favorites.Add($key) + $this.Metadata.Save() + return $true + } + } + + [System.Collections.Generic.List[PSNote]] GetFavorites() { + $list = [System.Collections.Generic.List[PSNote]]::new() + if (-not $this.Metadata) { return $list } + + foreach ($n in $this.Notes) { + if ($this.Metadata.Favorites.Contains($this.GetNoteKey($n))) { + $list.Add($n) | Out-Null + } + } + return $list + } +} \ No newline at end of file diff --git a/src/Classes/SplatBlock.class.ps1 b/src/Classes/SplatBlock.class.ps1 new file mode 100644 index 0000000..fa5e84d --- /dev/null +++ b/src/Classes/SplatBlock.class.ps1 @@ -0,0 +1,16 @@ +class SplatBlock { + [string]$Command + [string]$ParameterSet + [Boolean]$IsDefault + [string]$HashBlock + [string]$SetBlock + + SplatBlock( + [object]$object + ){ + $this.Command = $object.Command + $this.IsDefault = $object.IsDefault + $this.HashBlock = $object.HashBlock + $this.SetBlock = $object.SetBlock + } +} \ No newline at end of file diff --git a/Resources/PSNotes.format.ps1xml b/src/PSNotes.format.ps1xml similarity index 50% rename from Resources/PSNotes.format.ps1xml rename to src/PSNotes.format.ps1xml index e046f7d..7f0762f 100644 --- a/Resources/PSNotes.format.ps1xml +++ b/src/PSNotes.format.ps1xml @@ -1,192 +1,144 @@ - - - - - PSNote - - PSNote - - - - - - + + + + + + PSNote + + PSNote + + + + + + - "$('-' * 40)`n`n" + - "Note : $($_.Note)`n" + - "Details : $($_.Details)`n" + - "Alias : $($_.Alias)`n" + - "Snippet :`n`n" + - "$($_.Snippet)`n`n" - - - - - - - - - SplatBlock - - SplatBlock - - - - - - + "$('-' * 40)`n`n" + + "Note : $($_.Note)`n" + + "Catalog : $($_.Catalog)`n" + + "Details : $($_.Details)`n" + + "Alias : $($_.Alias)`n" + + "Snippet :`n`n" + + "$($_.Snippet)`n`n" + + + + + + + + + SplatBlock + + SplatBlock + + + + + + - if($_.SetBlock){ - "ParameterSet : $($_.ParameterSet)`n" + - "IsDefault : $($_.IsDefault)`n" + - "SetBlock :" + - "$($_.SetBlock.Split("`n") | Foreach-Object{ "`n $($_.Trim())"})" + - "`nHashBlock :" + - "$($_.HashBlock.Split("`n") | Foreach-Object{ "`n $($_)"})" - } - elseif($_.ParameterSet){ - "ParameterSet : $($_.ParameterSet)`n" + - "IsDefault : $($_.IsDefault)`n" + - "Parameters : $($_.HashBlock)`n" - } - else{ - "$($_.HashBlock)" - } - - - - - - - - - PSNoteSearch - - PSNoteSearch - - - - - - 25 - - - - 15 - - - - 15 - - - - 15 - - - - - - - - - - Note - - - Details - - - Alias - - - Tags - - - Snippet - - - - - - - - PSNote - - PSNote - - - - - - 25 - - - - 15 - - - - 15 - - - - 15 - - - - - - - - - - Note - - - Details - - - Alias - - - Tags - - - Snippet - - - - - - - - PSNote - - PSNote - - - - - - - Note - - - Details - - - Alias - - - Tags - - - Snippet - - - - - - - - \ No newline at end of file + if ($_.SetBlock) { + "ParameterSet : $($_.ParameterSet)`n" + + "IsDefault : $($_.IsDefault)`n" + + "SetBlock :" + + "$($_.SetBlock.Split("`n") | ForEach-Object { "`n $($_.Trim())" })" + + "`nHashBlock :" + + "$($_.HashBlock.Split("`n") | ForEach-Object { "`n $($_)" })" + } + elseif ($_.ParameterSet) { + "ParameterSet : $($_.ParameterSet)`n" + + "IsDefault : $($_.IsDefault)`n" + + "Parameters : $($_.HashBlock)`n" + } + else { + "$($_.HashBlock)" + } + + + + + + + + + PSNote + + PSNote + + + + + left + 25 + + + left + 15 + + + left + 15 + + + left + 15 + + + left + + + + + + + Note + + + Catalog + + + Alias + + + Tags + + + Snippet + + + + + + + + PSNote + + PSNote + + + + + + + Note + + + Catalog + + + Alias + + + Tags + + + Snippet + + + + + + + + diff --git a/src/PSNotes.psd1 b/src/PSNotes.psd1 new file mode 100644 index 0000000..7ef7544 --- /dev/null +++ b/src/PSNotes.psd1 @@ -0,0 +1,148 @@ +# +# Module manifest for module 'PSNotes' +# +# Generated by: Matthew Dowst +# +# Generated on: 6/2/2022 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = '.\PSNotes.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0' + + # Supported PSEditions + CompatiblePSEditions = @('Desktop','Core') + + # ID used to uniquely identify this module + GUID = '040757f4-ee7b-4e93-9883-f6a1930b6966' + + # Author of this module + Author = 'Matthew Dowst' + + # Company or vendor of this module + CompanyName = 'dowst.dev' + + # Copyright statement for this module + Copyright = '(c) 2026 Matthew Dowst. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PSNotes is a PowerShell module that provides a structured, versioned snippet and script library for reusable automation patterns. Create notes with aliases, tags, and metadata to quickly execute, copy, or preview commands. Organize notes into local or remote catalogs, search by name, tag, details, or snippet content, and turn frequently used automation into first-class commands.' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '5.1' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + ProcessorArchitecture = 'None' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = '' + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + FormatsToProcess = 'PSNotes.format.ps1xml' + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = '*' + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = '*' + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'PowerShell' + 'Snippets' + 'Automation' + 'DevOps' + 'SRE' + 'Catalog' + 'Productivity' + 'CLI' + 'Terminal' + ) + + # A URL to the license for this module. + LicenseUri = 'https://github.com/mdowst/PSNotes/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/mdowst/PSNotes' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = @' +- Added interactive console via Start-PSNote +- Added catalog support (local and remote) +- Added script path execution support +- Added versioned store with migration via Update-PSNoteStore +- Improved search and tagging capabilities +'@ + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + +} + diff --git a/src/Private/Import-PSNoteCatalog.ps1 b/src/Private/Import-PSNoteCatalog.ps1 new file mode 100644 index 0000000..41e3c56 --- /dev/null +++ b/src/Private/Import-PSNoteCatalog.ps1 @@ -0,0 +1,127 @@ +Function Import-PSNoteCatalog { + <# + .SYNOPSIS + Use to import a PSNotes JSON fiile + + .DESCRIPTION + Allows you to import shared PSNotes JSON files to your local notes. They can be imported to your personal + store, or they can be imported to a seperate file. + + .PARAMETER NoteObject + The PSNote objects you want to export. Use Get-PSNote to build the object and pass it to the parameter + or use a pipeline to pass it. + + .PARAMETER Path + The path to the PSNotes JSON file to export to. + + .PARAMETER Catalog + Use to output snippets to a seperate file stored in the folder %APPDATA%\PSNotes. + Useful for when you want to share different snippet types. + + .EXAMPLE + Import-PSNote -Path C:\Import\MyPSNotes.json + + Imports the contents of the file MyPSNotes.json and saves it to your personal PSNotes.json file + + .EXAMPLE + Import-PSNote -Path C:\Export\MyPSNotes.json -Catalog 'ADNotes' + + Imports the contents of the file MyPSNotes.json and saves it to the file ADNotes.json in the folder %APPDATA%\PSNotes + + .LINK + https://github.com/mdowst/PSNotes + #> + [cmdletbinding()] + param( + [parameter(Mandatory = $true)] + [NoteCatalog]$ImportedCatalog, + [parameter(Mandatory = $true)] + [string]$DestinationCatalog, + [ValidateSet('Prompt', 'SkipMigratedNotes', 'OverwriteExistingNotes')] + [parameter(Mandatory = $false)] + [string]$DefaultBehavior = 'Prompt' + ) + Test-PSNotesInitalize + + $excludeCollection = [System.Collections.Generic.List[PSNote]]::new() + do { + $dup = $script:_noteStore.Notes | Where-Object { $_.Alias -in $ImportedCatalog.Notes.Alias -and $_.Alias -notin $excludeCollection.Alias } | Select-Object -First 1 + if ($dup) { + $mn = $ImportedCatalog.Notes | Where-Object { $_.Alias -eq $dup.Alias } | Select-Object -First 1 + Write-Warning "Duplicate note alias found during migration: $($mn.Alias)." + $lines = @( + "Existing Note" + "-------------" + "Catalog : $($dup.Catalog)" + "Note : $($dup.Name)" + "Alias : $($dup.Alias)" + "Snippet : $($dup.Snippet.Trim().Split("`n")[0])" + ) + $buffer = $lines | ForEach-Object { $_.Length } | Sort-Object -Descending | Select-Object -First 1 + $lines[2] = "Catalog : `e[38;2;0;128;255m$($dup.Catalog)`e[0m" + $lines[0] = $lines[0].PadRight($buffer + 10) + "Migrated Note" + $lines[1] = $lines[1].PadRight($buffer + 10) + "-------------" + $lines[2] = $lines[2].PadRight($buffer + 31) + "Catalog : `e[38;2;255;255;0m$($mn.Catalog)`e[0m" + $lines[3] = $lines[3].PadRight($buffer + 10) + "Note : $($mn.Name)" + $lines[4] = $lines[4].PadRight($buffer + 10) + "Alias : $($mn.Alias)" + $lines[5] = $lines[5].PadRight($buffer + 10) + "Snippet : $($mn.Snippet.Trim().Split("`n")[0])" + + $lines += "Select an action:" + $lines += "1. Skip migrating this note." + $lines += " (Item in `e[38;2;255;255;0m$($mn.Catalog)`e[0m will be deleted.)" + $lines += "2. Overwrite existing note with migrated note." + $lines += " (Item in `e[38;2;0;128;255m$($dup.Catalog)`e[0m will be deleted.)" + $lines += "3. Set new alias." + $lines += "4. View details of each note." + $lines += "Enter choice (1, 2, 3, or 4) and hit [Enter]" + $prompt = ($lines -join ("`n")) + if (-not [string]::IsNullOrEmpty($additionalNote)) { + $prompt = $additionalNote + "`n" + $prompt + } + # Determine choice based on DefaultBehavior + if ($DefaultBehavior -eq 'SkipMigratedNotes') { + $choice = '1' + } + elseif ($DefaultBehavior -eq 'OverwriteExistingNotes') { + $choice = '2' + } + else { + $choice = Read-Host $prompt + } + + if ($choice -eq '1') { + Write-Verbose "Skipping migration of note with alias: $($mn.Alias)" + $excludeCollection.Add($mn) + #$ImportedCatalog.RemoveNote($mn.Note, $false) + } + elseif ($choice -eq '2') { + Write-Verbose "Overwriting existing note with alias: $($mn.Alias)" + $script:_noteStore.RemoveNote($dup.Name, $dup.Catalog, $true ) + } + elseif ($choice -eq '3') { + $newAlias = Read-Host "Enter new alias for the migrated note" + if (-not ($script:_noteStore.Notes | Where-Object { $_.Alias -eq $newAlias })) { + $mn.Alias = $newAlias + } + else { + $additionalNote = "`e[38;2;255;255;0mAlias $newAlias already exists. Please choose another.`e[0m" + } + } + elseif ($choice -eq '4') { + $additionalNote = "`e[38;2;0;128;255mExisting Note`n$( ($dup | Format-List * -Force | Out-String).Trim() )`e[0m" + $additionalNote = $additionalNote + "`n-------------------------`n" + $additionalNote = $additionalNote + "`e[38;2;255;255;0mMigrated Note`n$( ($mn | Format-List * -Force | Out-String).Trim() )`e[0m" + } + else { + $additionalNote = "`e[38;2;255;0;0mInvalid choice. Please enter 1, 2, 3, or 4.`e[0m" + } + } + } while ($dup) + + $currentCatalog = [NoteCatalog]::New($DestinationCatalog) + $ImportedCatalog.Notes | Where-Object { $_.Alias -notin $excludeCollection.Alias -and $_.Alias -notin $currentCatalog.Notes.Alias } | ForEach-Object { $currentCatalog.Notes.Add($_) } + $currentCatalog.Save() + $script:_noteStore.LoadCatalog($DestinationCatalog) + + Write-Verbose "PSNoteStore update complete." +} \ No newline at end of file diff --git a/src/Private/Initialize-PSNotesRemoteJsonFile.ps1 b/src/Private/Initialize-PSNotesRemoteJsonFile.ps1 new file mode 100644 index 0000000..ad87d62 --- /dev/null +++ b/src/Private/Initialize-PSNotesRemoteJsonFile.ps1 @@ -0,0 +1,24 @@ +Function Initialize-PSNoteStoreRemoteJsonFile{ + + # Create PSNote.json in %APPDATA%\PSNotes to save users local settings + if(-not (Test-Path $env:PSNotesRemoteJsonFile)){ + Out-File $env:PSNotesRemoteJsonFile -Encoding UTF8 + } + + # Sync the remote JSON files + $uri = 'https://gist.githubusercontent.com/mdowst/7198756f760ad0de0f635aaef5c4d338/raw/aa078383267e1d1613169e1a42f5c35ab70ffa7d/RemotePSNote.json' + + $outFile = Join-Path $env:PSNOTES_HOME (Split-Path $uri -Leaf) + if([System.IO.Path]::GetExtension($outFile) -ne '.json'){ + $outFile += '.json' + } + + try{ + Invoke-WebRequest -Uri $uri -OutFile $outFile -ErrorAction Stop + } + catch{ + # TODO: Write custom error message + Write-Error "Something went wrong downloading the remote PSNotes JSON file from '$uri'. `nError Details: $($_.Exception.Message)" + } + $download +} \ No newline at end of file diff --git a/src/Private/Invoke-PSNote.ps1 b/src/Private/Invoke-PSNote.ps1 new file mode 100644 index 0000000..e349cbe --- /dev/null +++ b/src/Private/Invoke-PSNote.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + Invokes a PSNote by executing its script or snippet. + +.DESCRIPTION + Internal function that executes a PSNote based on its Kind property. + For Script notes, it runs the referenced script file. + For Snippet notes, it executes the snippet as a script block. + +.PARAMETER Note + The PSNote object to invoke. Must be of Kind 'Script' or 'Snippet'. + +.EXAMPLE +Invoke-PSNote -Note $note + +Executes the provided note. + +.NOTES + This is an internal function. Validates that the note store is initialized + before execution. + +.OUTPUTS + System.Object + Returns any output from the executed script or snippet. +#> +Function Invoke-PSNote { + + [cmdletbinding(DefaultParameterSetName = "Note")] + param( + [parameter(Mandatory = $true, ParameterSetName = "Note", Position = 0)] + [PSNote]$Note + ) + Test-PSNotesInitalize + + switch ($Note.Kind) { + Script { + if ([string]::IsNullOrWhiteSpace($Note.Snippet)) { + throw "Cannot invoke Script note '$($Note.Name)': Path is empty." + } + if (-not (Test-Path -LiteralPath $Note.Snippet)) { + throw "Cannot invoke Script note '$($Note.Name)': Script file not found at path: $($Note.Snippet)" + } + & $Note.Snippet + } + Snippet { + if ([string]::IsNullOrWhiteSpace($Note.Snippet)) { + throw "Cannot invoke Snippet note '$($Note.Name)': Snippet is empty." + } + $scriptBlock = [ScriptBlock]::Create($Note.Snippet) + Invoke-Command -ScriptBlock $scriptBlock + } + default { + throw "Cannot invoke note '$($Note.Name)': Unknown Kind: $($Note.Kind)" + } + } + +} \ No newline at end of file diff --git a/src/Private/Test-PSNotesInitalize.ps1 b/src/Private/Test-PSNotesInitalize.ps1 new file mode 100644 index 0000000..b1b6a28 --- /dev/null +++ b/src/Private/Test-PSNotesInitalize.ps1 @@ -0,0 +1,9 @@ +Function Test-PSNotesInitalize{ + [CmdletBinding()] + param() + + if([string]::IsNullOrEmpty($env:PSNOTES_HOME)){ + Write-Verbose '$env:PSNOTES_HOME is not set.' + Initialize-PSNoteStore + } +} \ No newline at end of file diff --git a/src/Private/Write-NoteSnippet.ps1 b/src/Private/Write-NoteSnippet.ps1 new file mode 100644 index 0000000..80ad621 --- /dev/null +++ b/src/Private/Write-NoteSnippet.ps1 @@ -0,0 +1,51 @@ +Function Write-NoteSnippet { + <# + .SYNOPSIS + Used by the Copy-PSNote and Invoke-PSNote to display a menu and prompt for selection of a note + + .PARAMETER NoteSelection + An array of PSNote objects to create a menu with + + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + [cmdletbinding()] + param( + [PSNote[]]$NoteSelection + ) + $i = 0 + $noteMenu = $NoteSelection | ForEach-Object { + $i++ + $_ | Select-Object @{l = 'Nbr'; e = { $i } }, * + } + $promptMenu = $noteMenu | Format-Table Nbr, Note, Alias, Details, Tags -AutoSize | Out-String + + $Prompt = "Enter the number to run (or leave blank to cancel) and hit [Enter]" + $promptError = $false + do { + Write-Host $promptMenu + if ( $promptError ) { + Write-Host "The select must a number between 1 and $($NoteSelection.Count)" -ForegroundColor Red + } + $Selection = Read-Host -Prompt $Prompt + if ([string]::IsNullOrEmpty($Selection)) { + $promptError = $false + } + elseif (-not [int]::TryParse($Selection, [ref]$null)) { + $promptError = $true + } + elseif ([int]$Selection -gt $NoteSelection.Count -or [int]$Selection -lt 1) { + $promptError = $true + } + else { + $promptError = $false + } + + }while ($promptError) + + if ([string]::IsNullOrEmpty($Selection)) { + $null + } + else { + $NoteSelection[$Selection - 1] + } +} \ No newline at end of file diff --git a/src/Public/Export-PSNote.ps1 b/src/Public/Export-PSNote.ps1 new file mode 100644 index 0000000..0b7b3ad --- /dev/null +++ b/src/Public/Export-PSNote.ps1 @@ -0,0 +1,109 @@ +<# +.SYNOPSIS +Exports PSNotes to a JSON file for backup or sharing. + +.DESCRIPTION +Export-PSNote serializes notes from the PSNotes store into a JSON file. You can export the entire +store or a filtered subset of notes (for example, by name, alias, tag, or catalog depending on +the parameter set in use). + +The exported file is designed to round-trip with Import-PSNote and can be used for backups, +migration to another machine, or sharing curated note collections. + +If the destination file already exists, use -Force to overwrite it. + +.FUNCTIONALITY +PSNotes Data Exchange + +.ROLE +Public + +.COMPONENT +Serialization + +.PARAMETER NoteObject +The PSNote objects you want to export. Use Get-PSNote to build the object and pass it to the parameter +or use the pipeline to pass them in. + +.PARAMETER Catalog +The catalog name to export. When specified, all notes from that catalog are exported. + +.PARAMETER Path +The path to the PSNotes JSON file to export to. + +.PARAMETER Force +Overwrite the output file if it already exists. + +.EXAMPLE +Export-PSNote -Catalog 'Default' -Path C:\Export\MyPSNotes.json + +Exports all notes from the 'Default' catalog to a JSON file. + +.EXAMPLE +Get-PSNote -Tag 'AD' | Export-PSNote -Path C:\Export\SharedADNotes.json + +Exports all notes with the tag 'AD' to the file SharedADNotes.json. + +.EXAMPLE +Get-PSNote -Name 'Cred*' -Catalog 'Work' | Export-PSNote -Path C:\Export\WorkCreds.json + +Exports notes that match the name pattern from the 'Work' catalog. + +.EXAMPLE +Get-PSNote -SearchString 'token' | Export-PSNote -Path C:\Export\TokenNotes.json + +Exports notes that match a search string. + +.EXAMPLE +Export-PSNote -Catalog 'Personal' -Path C:\Export\PersonalNotes.json -Force + +Exports the 'Personal' catalog and overwrites the file if it exists. + +.OUTPUTS +System.Object + +.NOTES +- The exported JSON file is intended for use with Import-PSNote. +- Use -Force to overwrite an existing file. +- See also: Import-PSNote, Get-PSNote +#> +Function Export-PSNote { + [cmdletbinding(DefaultParameterSetName = "Note")] + param( + [parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Note")] + [PSNote[]]$NoteObject, + [parameter(Mandatory = $true, ParameterSetName = "Catalog")] + [string]$Catalog, + [parameter(Mandatory = $true)] + [string]$Path, + [parameter(Mandatory = $false)] + [switch]$Force + ) + begin { + Test-PSNotesInitalize + + Write-Debug "$($noteObject | Format-Table | Out-String)" + # If Catalog is specified, add all objects from that catalog, otherwise only add those passed + if ($PSCmdlet.ParameterSetName -eq 'Catalog') { + $ExportObjects = [NoteCatalog]::new($Catalog) + } + else { + $ExportObjects = [NoteCatalog]::new($false) + $ExportObjects.Catalog = 'Export' + } + } + process { + # If Catalog is specified, add all objects from that catalog, otherwise only add those passed + $noteObject | ForEach-Object { $ExportObjects.Notes.Add( $_ ) } + } + end { + Write-Debug "$($ExportObjects | Format-Table | Out-String)" + + if ((Test-Path $Path) -and -not $Force) { + Write-Error "File already exists at '$Path'. Use -Force to overwrite." + } + else { + $ExportObjects | Select-Object -Property * -ExcludeProperty Path | ConvertTo-Json -Depth 5 | Out-File $Path -Encoding UTF8NoBOM + } + } +} \ No newline at end of file diff --git a/src/Public/Get-PSNote.ps1 b/src/Public/Get-PSNote.ps1 new file mode 100644 index 0000000..5eb6405 --- /dev/null +++ b/src/Public/Get-PSNote.ps1 @@ -0,0 +1,154 @@ +<# +.SYNOPSIS +Retrieves PSNotes from the note store by listing or searching. + +.DESCRIPTION +Get-PSNote returns notes stored in the PSNotes store. By default, all notes are returned. +You can filter results by name, alias, tag, catalog, or search text depending on the +parameter set in use. + +This cmdlet returns PSNote objects that can be piped into other PSNotes commands such as +Remove-PSNote, Move-PSNote, Export-PSNote, or Set-PSNote. + +Wildcard matching is supported where applicable. + +.FUNCTIONALITY +PSNotes Notes + +.ROLE +Public + +.COMPONENT +Notes + +.PARAMETER Name +Returns notes that match the specified name. Wildcards are supported. + +.PARAMETER Tag +Returns notes that contain one or more specified tags. + +.PARAMETER Catalog +Returns notes from the specified catalog. + +.PARAMETER Search +Performs a broader search across note properties such as name, alias, and tags. + +.PARAMETER Copy +When specified, copies the snippet content of the first matching note to the clipboard. If multiple notes match, you will be prompted to select one. + +.PARAMETER Run +When specified, executes the snippet content of the first matching note. If multiple notes match, you will be prompted to select one. + +.EXAMPLE +PS> Get-PSNote + +Returns all notes in the store. + +.EXAMPLE +PS> Get-PSNote -Catalog 'Azure' + +Returns all notes in the Azure catalog. + +.EXAMPLE +PS> Get-PSNote -Name 'Get-*' + +Returns notes with names that match the pattern. + +.EXAMPLE +PS> Get-PSNote -Tag 'VM' + +Returns notes tagged with 'VM'. + +.EXAMPLE +PS> Get-PSNote -Search 'backup' + +Searches across note properties for the term 'backup'. + +.EXAMPLE +PS> Get-PSNote -Catalog 'Azure' | Remove-PSNote + +Finds notes in the Azure catalog and removes them. + +.OUTPUTS +PSNote + +.NOTES +- Returns PSNote objects. +- Wildcards are supported for Name and Alias parameters. +- See also: New-PSNote, Set-PSNote, Remove-PSNote, Move-PSNote, Export-PSNote +#> +Function Get-PSNote{ + [cmdletbinding(DefaultParameterSetName="Note")] + param( + [parameter(Mandatory=$false, ParameterSetName="Note")] + [string]$Name = '*', + [parameter(Mandatory=$false, ParameterSetName="Note")] + [string]$Tag, + [parameter(Mandatory=$false, ParameterSetName="Note")] + [switch]$Copy, + [parameter(Mandatory=$false, ParameterSetName="Note")] + [parameter(Mandatory=$false, ParameterSetName="Search")] + [switch]$Run, + [parameter(Mandatory=$false, ParameterSetName="Note")] + [parameter(Mandatory=$false, ParameterSetName="Search")] + [string[]]$Catalog, + [parameter(Mandatory=$false, ParameterSetName="Search")] + [Alias("SearchString")] + [string]$Search + ) + Test-PSNotesInitalize + + $notes = $script:_noteStore.Notes + + if($Catalog){ + $notes = $notes | Where-Object { + $noteCatalog = $_.Catalog + foreach($pattern in $Catalog){ + if($noteCatalog -like $pattern){ return $true } + } + return $false + } + } + + if($Search){ + $returned = $notes | Where-Object { + $_.Name -like "*$Search*" -or + $_.Alias -like "*$Search*" -or + $_.Details -like "*$Search*" -or + $_.Snippet -like "*$Search*" -or + ($_.Tags | Where-Object { $_ -like "*$Search*" } | Select-Object -First 1) + } + } elseif($Tag){ + $returned = $notes | Where-Object{$_.Name -like $Name -and $_.Tags -contains $Tag} + } else { + $returned = $notes | Where-Object{$_.Name -like $Name} + } + + if($copy -or $Run){ + if(@($returned).count -gt 1){ + Write-Warning "More than 1 command returned. Select one to continue." + $sel = Write-NoteSnippet -NoteSelection $returned + $returned = $sel + } + + if(-not $returned){ + Write-Warning "No note found to run." + return + } + } + + if($copy){ + if(Get-Command -Name 'Set-Clipboard' -ErrorAction SilentlyContinue){ + $returned | Select-Object -First 1 -ExpandProperty Snippet | Set-Clipboard + } else { + Write-Debug "Cmdlet 'Set-Clipboard' not found." + } + } + + if($Run){ + Invoke-PSNote -Note $returned + } else { + $returned + } + +} diff --git a/src/Public/Get-PSNoteAlias.ps1 b/src/Public/Get-PSNoteAlias.ps1 new file mode 100644 index 0000000..944b221 --- /dev/null +++ b/src/Public/Get-PSNoteAlias.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS +Resolves a PSNote by alias and outputs, copies, or executes its content. + +.DESCRIPTION +Get-PSNoteAlias locates a PSNote using its alias and performs a quick action against it. + +Depending on the note’s Kind and the parameters supplied, the cmdlet can: + +- Output the snippet content to the console +- Copy the snippet content to the clipboard +- Execute the snippet directly +- Execute a referenced script path + +This command is designed for fast recall of frequently used commands through short, +easy-to-remember aliases. + +.FUNCTIONALITY +PSNotes Notes + +.ROLE +Public + +.COMPONENT +Notes + +.PARAMETER Copy +Copies the note content to the clipboard instead of writing it to the console. + +.PARAMETER Run +Executes the note content. For snippet notes, the script block is invoked. For script-path +notes, the referenced script is executed. + +.OUTPUTS +System.String + +.NOTES +- Alias values are intended to be unique within the PSNotes store. +- Behavior differs based on the note Kind (for example, Snippet vs ScriptPath). +- Clipboard functionality depends on platform support. +- See also: Get-PSNote, New-PSNote, Set-PSNote +#> +Function Get-PSNoteAlias{ + [cmdletbinding()] + param( + [parameter(Mandatory=$false)] + [switch]$Copy, + [parameter(Mandatory=$false)] + [switch]$Run + ) + Test-PSNotesInitalize + # The function is designed to be called via an alias, so we check if the invocation name matches the command name. + if($MyInvocation.MyCommand.Name -eq $MyInvocation.InvocationName){ + Write-Error "The Get-PSNoteAlias cmdlet is designed to be called using an alias and not directly." + } else { + $Alias = $MyInvocation.InvocationName + $aliasObject = $script:_noteStore.Notes | Where-Object{$_.Alias -eq $Alias} + if(($Run -or $aliasObject.Run) -and -not $Copy) { + Invoke-PSNote -Note $aliasObject + } else { + if(Get-Command -Name 'Set-Clipboard' -ErrorAction SilentlyContinue){ + $aliasObject | Select-Object -First 1 -ExpandProperty Snippet | Set-Clipboard + } else { + Write-Debug "Cmdlet 'Set-Clipboard' not found." + } + Return $aliasObject.Snippet + } + } +} + diff --git a/src/Public/Get-PSNoteMenu.ps1 b/src/Public/Get-PSNoteMenu.ps1 new file mode 100644 index 0000000..6e2cb55 --- /dev/null +++ b/src/Public/Get-PSNoteMenu.ps1 @@ -0,0 +1,266 @@ +<# +.SYNOPSIS +Displays an interactive, paged console menu for browsing and selecting PSNotes. + +.DESCRIPTION +Get-PSNoteMenu presents PSNotes in an interactive terminal-based menu with paging support. +Users can navigate through notes, select one by number, and then choose an action such as: + +- Output the note content +- Copy the note to the clipboard +- Execute the snippet +- Execute a referenced script path + +The menu is designed to provide a streamlined console experience for browsing catalogs, +favorites, or filtered note sets. When multiple pages are present, page indicators +(for example, "Page 1/3") are shown. + +This cmdlet is intended for interactive use within the current console session. + +.FUNCTIONALITY +PSNotes Interactive UI + +.ROLE +Interactive + +.COMPONENT +UI + +.PARAMETER InputObject +A collection of PSNote objects to display. Accepts pipeline input from Get-PSNote. + +If not provided, all notes are displayed by default. + +.EXAMPLE +PS> Get-PSNoteMenu + +Displays all notes in an interactive menu. + +.OUTPUTS +PSNote + +.NOTES +- Designed for interactive terminal use. +- Supports paging when the number of notes exceeds the configured page size. +- After selecting a note, a secondary action menu is displayed (output, copy, run). +- See also: Get-PSNote, Get-PSNoteAlias, Start-PSNote +#> +function Get-PSNoteMenu { + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param( + [Parameter(ValueFromPipeline)] + $InputObject + ) + + begin { $notes = @() } + + process { + if ($PSBoundParameters.ContainsKey('InputObject') -and $null -ne $InputObject) { + $notes += , $InputObject + } + } + + end { + if (-not $notes -or $notes.Count -eq 0) { $notes = @(Get-PSNote) } + if (-not $notes -or $notes.Count -eq 0) { Write-Host "No notes found."; return } + + # --- Screen sizing (Terminal-friendly) --- + $winWidth = 120 + $winHeight = 30 + try { + $raw = $Host.UI.RawUI + $w = [int]$raw.WindowSize.Width + $h = [int]$raw.WindowSize.Height + if ($w -le 0) { $w = [int]$raw.BufferSize.Width } + if ($h -le 0) { $h = [int]$raw.BufferSize.Height } + if ($w -gt 0) { $winWidth = $w } + if ($h -gt 0) { $winHeight = $h } + } + catch { + $winWidth = 120 + $winHeight = 30 + } + + $winWidth = [Math]::Max(20, [int]$winWidth) + $winHeight = [Math]::Max(10, [int]$winHeight) + + # Reserve lines for header/pager + prompt + $reservedLines = 4 + $rowsPerPage = [Math]::Max(1, $winHeight - $reservedLines) + + $count = $notes.Count + $numWidth = ($count.ToString()).Length + $gap = 2 + + # Build display items + $display = for ($i = 0; $i -lt $count; $i++) { + $n = $notes[$i] + + $catalog = if ($n.PSObject.Properties.Match('Catalog').Count -gt 0) { $n.Catalog } else { $null } + if ([string]::IsNullOrWhiteSpace($catalog)) { $catalog = 'Default' } + + $label = if ($n.PSObject.Properties.Match('Alias').Count -gt 0) { $n.Alias } else { $null } + if ([string]::IsNullOrWhiteSpace($label)) { + if ($n.PSObject.Properties.Match('Name').Count -gt 0) { + $label = $n.Name + } + elseif ($n.PSObject.Properties.Match('Note').Count -gt 0) { + $label = $n.Note + } + } + if ([string]::IsNullOrWhiteSpace($label)) { $label = '' } + + [pscustomobject]@{ + Index = ($i + 1) + Text = ("{0," + $numWidth + "}) [{1}] {2}") -f ($i + 1), $catalog, $label + } + } + + # Max *length* (not max *string*) + $maxTextLen = ($display | ForEach-Object { $_.Text.Length } | Measure-Object -Maximum).Maximum + if (-not $maxTextLen -or $maxTextLen -lt 1) { $maxTextLen = 10 } + + $colWidth = [int][Math]::Min([Math]::Max(10, $maxTextLen + $gap), $winWidth) + $cols = [int][Math]::Floor($winWidth / [double]$colWidth) + if ($cols -lt 1) { $cols = 1 } + + $itemsPerPage = [Math]::Max(1, $rowsPerPage * $cols) + + $totalPages = [int][Math]::Ceiling($count / [double]$itemsPerPage) + if ($totalPages -lt 1) { $totalPages = 1 } + + $pageIndex = 0 # 0-based + + while ($true) { + Clear-Host + + $pageStart = $pageIndex * $itemsPerPage + $pageEnd = [Math]::Min($pageStart + $itemsPerPage - 1, $count - 1) + + $pageItems = $display[$pageStart..$pageEnd] + $pageCount = $pageItems.Count + $pageRows = [int][Math]::Ceiling($pageCount / [double]$cols) + + # Page header ONLY if more than one page + if ($totalPages -gt 1) { + Write-Host ("Page {0}/{1}" -f ($pageIndex + 1), $totalPages) + Write-Host "" + } + + for ($r = 0; $r -lt $pageRows; $r++) { + $lineParts = for ($c = 0; $c -lt $cols; $c++) { + $idx = ($r * $cols) + $c + if ($idx -ge $pageCount) { continue } + + $cell = $pageItems[$idx].Text + if ($c -lt ($cols - 1)) { $cell = $cell.PadRight($colWidth) } + $cell + } + Write-Host ($lineParts -join '') + } + + $isLastPage = ($pageIndex -ge ($totalPages - 1)) + + if ($totalPages -eq 1 -or $isLastPage) { + $prompt = "Enter # to select, or 'q' to quit" + } + else { + $prompt = "Enter # to select, Enter for next page, or 'q' to quit" + } + + $resp = Read-Host $prompt + + if ([string]::IsNullOrWhiteSpace($resp)) { + if (-not $isLastPage) { $pageIndex++ } + continue + } + + if ($resp -match '^(q|quit|exit)$') { return } + + $chosen = 0 + if (-not [int]::TryParse($resp, [ref]$chosen)) { + Write-Host "Please enter a number, Enter, or 'q'." -ForegroundColor Yellow + Start-Sleep -Milliseconds 700 + continue + } + + if ($chosen -lt 1 -or $chosen -gt $count) { + Write-Host "Invalid selection: $chosen" -ForegroundColor Yellow + Start-Sleep -Milliseconds 700 + continue + } + + $selected = $notes[$chosen - 1] + + # --- SECOND MENU: Copy / Run / Output --- + Clear-Host + + $selCatalog = if ($selected.PSObject.Properties.Match('Catalog').Count -gt 0) { $selected.Catalog } else { 'Default' } + if ([string]::IsNullOrWhiteSpace($selCatalog)) { $selCatalog = 'Default' } + + $selLabel = if ($selected.PSObject.Properties.Match('Alias').Count -gt 0) { $selected.Alias } else { $null } + if ([string]::IsNullOrWhiteSpace($selLabel)) { + if ($selected.PSObject.Properties.Match('Name').Count -gt 0) { + $selLabel = $selected.Name + } + elseif ($selected.PSObject.Properties.Match('Note').Count -gt 0) { + $selLabel = $selected.Note + } + else { + $selLabel = '' + } + } + + Write-Host "$(($selected | Out-String).TrimEnd())" + Write-Host "`n----------------------------------------" + Write-Host "[C] Copy to clipboard" + Write-Host "[R] Run" + Write-Host "[ENTER] Exit" + Write-Host "" + + $action = (Read-Host "Choose action").Trim() + + switch -Regex ($action) { + '^(c|copy)$' { + # Prefer Snippet, else fall back to Note (or empty) + $text = $null + if ($selected.PSObject.Properties.Match('Snippet').Count -gt 0) { $text = $selected.Snippet } + if ([string]::IsNullOrWhiteSpace($text) -and $selected.PSObject.Properties.Match('Name').Count -gt 0) { + $text = $selected.Name + } + elseif ([string]::IsNullOrWhiteSpace($text) -and $selected.PSObject.Properties.Match('Note').Count -gt 0) { + $text = $selected.Note + } + if ($null -eq $text) { $text = '' } + + if (Get-Command Set-Clipboard -ErrorAction SilentlyContinue) { + $text | Set-Clipboard + Write-Host "Copied to clipboard." -ForegroundColor Green + } else { + Write-Host "Set-Clipboard not available in this session." -ForegroundColor Yellow + } + + Start-Sleep -Milliseconds 700 + return + } + + '^(r|run)$' { + try { + # Execute in the current scope (like the rest of PSNotes patterns) + Invoke-PSNote -Note $selected -ErrorAction Stop + } + catch { + Write-Host ("Failed to run snippet: {0}" -f $_.Exception.Message) -ForegroundColor Red + } + + return + } + + default { + return $selected + } + } + } + } +} \ No newline at end of file diff --git a/src/Public/Import-PSNote.ps1 b/src/Public/Import-PSNote.ps1 new file mode 100644 index 0000000..9a15afc --- /dev/null +++ b/src/Public/Import-PSNote.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS +Imports PSNotes from a JSON export file into the local note store. + +.DESCRIPTION +Import-PSNote reads a PSNotes JSON export file and imports the contained notes into the local +PSNotes store. + +Import behavior may merge with existing notes or create new notes depending on the options +provided and the contents of the import file. Use -Force (if supported) to overwrite existing +notes when conflicts occur. + +This cmdlet is commonly used to restore backups created by Export-PSNote, migrate notes between +machines, or share curated note libraries. + +.FUNCTIONALITY +PSNotes Data Exchange + +.ROLE +Public + +.COMPONENT +Serialization + +.PARAMETER Path +The path to the PSNotes JSON file to import. + +.PARAMETER Catalog +Imports notes into the specified catalog (or maps imported notes into that catalog depending on implementation). + +.PARAMETER DefaultBehavior +Determines how to handle existing notes when conflicts are detected during import. +Valid values: +- Prompt: prompts when conflicts occur +- SkipMigratedNotes: keeps existing notes and skips conflicting imported notes +- OverwriteExistingNotes: overwrites existing notes with imported versions + +.EXAMPLE +PS> Import-PSNote -Path .\backup.json + +Imports all notes from backup.json into the local store. + +.EXAMPLE +PS> Import-PSNote -Path .\azure-notes.json -Catalog 'Azure' + +Imports notes from azure-notes.json and places them into the Azure catalog (if supported). + +.EXAMPLE +PS> Import-PSNote -Path .\backup.json -DefaultBehavior OverwriteExistingNotes + +Imports notes, overwriting conflicts, and returns the imported note objects. + +.OUTPUTS +System.Object + +.NOTES +- This cmdlet is designed to round-trip with Export-PSNote. +- If you encounter format/version differences after upgrading PSNotes, run Update-PSNoteStore. +- See also: Export-PSNote, Get-PSNote, Update-PSNoteStore +#> +Function Import-PSNote { + [cmdletbinding(DefaultParameterSetName = "Note")] + param( + [parameter(Mandatory = $true)] + [string]$Path, + [parameter(Mandatory = $false)] + [string]$Catalog = 'Default', + [ValidateSet('Prompt', 'SkipMigratedNotes', 'OverwriteExistingNotes')] + [parameter(Mandatory = $false)] + [string]$DefaultBehavior = 'Prompt' + ) + Test-PSNotesInitalize + + + $validation = [NoteCatalog]::ValidateNotes($Path) + + if(-not $validation.IsValid) { + Write-Error "The provided PSNotes JSON file is not in the correct format. Please ensure the file is a valid PSNotes catalog. Validation Errors: $($validation.Errors -join '; ')" + return + } + elseif($validation.StoreVersion -eq 'Current') { + $importedCatalog = [NoteCatalog]::Open($Path) + } + else { + $importedCatalog = [NoteCatalog]::Migrate($Path, $false) + } + + Import-PSNoteCatalog -ImportedCatalog $importedCatalog -DestinationCatalog $Catalog -DefaultBehavior $DefaultBehavior + + $script:_noteStore.InitializeAliases() + Write-Verbose "PSNoteStore update complete." +} \ No newline at end of file diff --git a/src/Public/Initialize-PSNoteStore.ps1 b/src/Public/Initialize-PSNoteStore.ps1 new file mode 100644 index 0000000..71d90ed --- /dev/null +++ b/src/Public/Initialize-PSNoteStore.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS +Initializes the PSNotes store and required supporting files. + +.DESCRIPTION +Initialize-PSNoteStore ensures the PSNotes store is present and ready for use. It creates the +required folder structure and base configuration files when they do not already exist. + +This cmdlet is typically invoked automatically by other PSNotes commands as needed, but it can +also be called directly when setting up PSNotes on a new machine or when repairing an incomplete +store. + +.FUNCTIONALITY +PSNotes Store Maintenance + +.ROLE +Maintenance + +.COMPONENT +Store + +.EXAMPLE +PS> Initialize-PSNoteStore + +Initializes the PSNotes store using the default location. + +.OUTPUTS +System.Object + +.NOTES +- This cmdlet prepares the store layout and configuration required by the PSNotes module. +- Most PSNotes commands will initialize the store automatically when needed. +- See also: Update-PSNoteStore, Get-PSNote +#> +Function Initialize-PSNoteStore { + [CmdletBinding()] + param() + + # Load all commands to noteObjects + #Initialize-PSNoteStoreRemoteJsonFile + $script:_noteStore = [NoteStore]::new() + Write-Verbose "User PSNotes Path: $env:PSNOTES_HOME" + $script:_noteStore.InitializeAliases() + + # Check id Set-Clipboard cmdlet is found. If not + if (-not (Get-Command -Name 'Set-Clipboard' -ErrorAction SilentlyContinue)) { + # ClipboardText module is found then set an alias for the Set-Clipboard command + if (Get-Module ClipboardText -ListAvailable) { + if (-not (Get-Alias -Name 'Set-Clipboard' -ErrorAction SilentlyContinue)) { + Set-Alias -Name 'Set-Clipboard' -Value 'Set-ClipboardText' + } + } + else { + $warning = "Cmdlet 'Set-Clipboard' not found. Copy functionality will not work until this is resolved. " + + "`n`t You can install the ClipboardText module from PowerShell Gallery, to add this functionality. " + + "`n`n`t`t Install-Module -Name ClipboardText`n" + + "`n`t More Details: https://www.powershellgallery.com/packages/ClipboardText" + Write-Warning $warning + } + } +} \ No newline at end of file diff --git a/src/Public/Move-PSNote.ps1 b/src/Public/Move-PSNote.ps1 new file mode 100644 index 0000000..4b9469a --- /dev/null +++ b/src/Public/Move-PSNote.ps1 @@ -0,0 +1,77 @@ +<# +.SYNOPSIS +Moves one or more PSNotes to a different catalog. + +.DESCRIPTION +Move-PSNote changes the catalog assignment of existing PSNotes. This allows you to reorganize +your note library as it grows without recreating notes. + +You can specify notes directly by name, alias, or other supported parameters, or pipe PSNote +objects from Get-PSNote. + +This cmdlet updates the note metadata and persists the changes to the PSNotes store. +Supports ShouldProcess, enabling the use of -WhatIf and -Confirm. + +.FUNCTIONALITY +PSNotes Notes + +.ROLE +Public + +.COMPONENT +Notes + +.PARAMETER InputObject +One or more PSNote objects to move. Accepts pipeline input from Get-PSNote. + +.PARAMETER DestinationCatalog +The destination catalog to move the note(s) into. + +.PARAMETER Force +Suppresses confirmation prompts when moving notes. + +.PARAMETER PassThru +Returns the moved PSNote objects. + + +.EXAMPLE +PS> Get-PSNote -Catalog 'General' | Move-PSNote -DestinationCatalog 'Archive' + +Moves all notes from the General catalog to the Archive catalog. + +.EXAMPLE +PS> Get-PSNote -Tag 'Legacy' | Move-PSNote -DestinationCatalog 'Archive' -Force -PassThru + +Moves all notes tagged 'Legacy' into the Archive catalog without prompting and returns the updated notes. + +.OUTPUTS +PSNote + +.NOTES +- Supports -WhatIf and -Confirm through ShouldProcess. +- Use Get-PSNote to identify notes before moving them. +- See also: Get-PSNote, Set-PSNote, Remove-PSNote +#> +function Move-PSNote { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [PSNote] $InputObject, + + [Parameter(Mandatory)] + [string] $DestinationCatalog, + + [switch] $Force, + + [switch] $PassThru + ) + + process { + $label = if ($InputObject.Alias) { $InputObject.Alias } else { $InputObject.Name } + + if ($PSCmdlet.ShouldProcess("[$($InputObject.Catalog)] $label", "Move to '$DestinationCatalog'")) { + $moved = $script:_noteStore.MoveNote($InputObject, $DestinationCatalog, [bool]$Force) + if ($PassThru) { $moved } + } + } +} diff --git a/src/Public/New-PSNote.ps1 b/src/Public/New-PSNote.ps1 new file mode 100644 index 0000000..7db6d31 --- /dev/null +++ b/src/Public/New-PSNote.ps1 @@ -0,0 +1,189 @@ +<# +.SYNOPSIS +Creates a new PSNote for storing reusable snippets or script references. + +.DESCRIPTION +New-PSNote creates a note in the PSNotes store that can represent either: + +- A reusable PowerShell snippet (inline code) +- A script reference (a path to a script file to execute) + +Notes can include metadata such as catalog, alias, and tags to make them easier to organize and +retrieve later. Once created, notes can be recalled using Get-PSNote, invoked quickly by alias +with Get-PSNoteAlias, or selected interactively using Get-PSNoteMenu. + +This cmdlet supports ShouldProcess, enabling -WhatIf and -Confirm. + +.FUNCTIONALITY +PSNotes Notes + +.ROLE +Public + +.COMPONENT +Notes + +.PARAMETER Name +The name of the note. + +.PARAMETER Catalog +The catalog to create the note in. + +.PARAMETER Alias +An optional short alias used for quick recall (for example, with Get-PSNoteAlias). + +.PARAMETER Tags +One or more tags to associate with the note. + +.PARAMETER Snippet +The PowerShell snippet content to store in the note. + +.PARAMETER ScriptPath +A script path to store in the note for later execution. + +.PARAMETER ScriptBlock +A PowerShell script block containing the code to store in the note. + +.PARAMETER Details +Additional details or description about the note. + +.PARAMETER Force +Overwrites an existing note with the same name (or alias conflict) where supported. + +.PARAMETER Run +When specified, executes the snippet content of the note immediately after creation. + +.EXAMPLE +PS> New-PSNote -Name 'List-AzVMs' -Catalog 'Azure' -Alias 'azvms' -Tag 'VM','Azure' -Snippet 'Get-AzVM' + +Creates a snippet note named List-AzVMs in the Azure catalog with an alias and tags. + +.EXAMPLE +PS> New-PSNote -Name 'Build' -Catalog 'Dev' -Alias 'build' -ScriptPath 'C:\Scripts\Build.ps1' + +Creates a script-path note that references a script to run later. + +.EXAMPLE +PS> New-PSNote -Name 'TestNote' -Catalog 'General' -Snippet 'Get-Date' -WhatIf + +Shows what would happen if the note were created without making changes. + +.EXAMPLE +PS> New-PSNote -Name 'List-AzVMs' -Catalog 'Azure' -Snippet 'Get-AzVM' -Force -PassThru + +Overwrites an existing note (if present) and returns the created note object. + +.OUTPUTS +PSNote + +.NOTES +- Supports -WhatIf and -Confirm through ShouldProcess. +- Notes may be uniquely identified by name and/or alias depending on store rules. +- Use Set-PSNote to update an existing note. +- See also: Get-PSNote, Set-PSNote, Get-PSNoteAlias, Get-PSNoteMenu +#> +Function New-PSNote { + [cmdletbinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low', DefaultParameterSetName = "Note")] + param( + [parameter(Mandatory = $true)] + [string]$Name, + [parameter(Mandatory = $false, ParameterSetName = "Snippet")] + [string]$Snippet, + [parameter(Mandatory = $false, ParameterSetName = "ScriptBlock")] + [ScriptBlock]$ScriptBlock, + [parameter(Mandatory = $false, ParameterSetName = "ScriptPath")] + [string]$ScriptPath, + [parameter(Mandatory = $false)] + [string]$Details, + [parameter(Mandatory = $false)] + [string]$Alias, + [parameter(Mandatory = $false)] + [string[]]$Tags, + [parameter(Mandatory = $false)] + [string]$Catalog = 'Default', + [parameter(Mandatory = $false)] + [bool]$Run = $false, + [parameter(Mandatory = $false)] + [switch]$Force + ) + Test-PSNotesInitalize + + Function Test-NoteAlias { + param($Alias) + + $AliasCheck = [regex]::Matches($Alias, "[^0-9a-zA-Z\-_]") + if ($AliasCheck.Success) { + throw "'$Alias' is not a valid alias. Alias's can only contain letters, numbers, dashes(-), and underscores (_)." + } + } + + # Determine the Kind based on parameter set + $Kind = [PSNoteKind]::Snippet + + if (-not [string]::IsNullOrEmpty($ScriptPath)) { + if (-not (Test-Path -Path $ScriptPath)) { + throw "Script file not found: $ScriptPath" + return + } + $Snippet = $ScriptPath + $Kind = [PSNoteKind]::Script + # Default $Run to $true for ScriptPath if not explicitly passed + if (-not $PSBoundParameters.ContainsKey('Run')) { + $Run = $true + } + } + elseif (-not [string]::IsNullOrEmpty($ScriptBlock)) { + $Snippet = $ScriptBlock.ToString() + } + + $newNote = $script:_noteStore.Notes | Where-Object { $_.Name -eq $Name -and $_.Catalog -eq $Catalog } + if ($newNote -and -not $force) { + Write-Error "The note '$Name' already exists. Use -force to overwrite existing properties" + break + } + elseif ($newNote -and $force) { + $toUpdate = $script:_noteStore.Notes | Where-Object { $_.Name -eq $Name -and $_.Catalog -eq $Catalog } | ForEach-Object { + $tu = [PSNote]::new($_) + $PSBoundParameters.GetEnumerator() | ForEach-Object { + if ($_.Key -eq 'ScriptBlock') { + $tu.Snippet = $_.Value.ToString() + } + elseif ($_.Key -eq 'ScriptPath') { + $tu.Snippet = $Snippet + $tu.Kind = [PSNoteKind]::Script + } + elseif ($_.Key -eq 'Alias') { + Test-NoteAlias $_.Value + $tu.Alias = $_.Value + } + elseif ($_.Key -ne 'Force' -and $_.Key -ne 'Note') { + # Skip Force and Note as we don't want to update those + $tu.$($_.Key) = $_.Value + } + } + $tu + } + $toUpdate | ForEach-Object { + Write-Verbose "Updating Note: $($_.Name)" + $script:_noteStore.UpdateNote($_) + if (-not [string]::IsNullOrEmpty($_.Alias)) { + Set-Alias -Name $_.Alias -Value Get-PSNoteAlias -Scope Global -Force + } + } + } + else { + if ([string]::IsNullOrEmpty($Alias)) { + $Alias = '' + } + else { + Test-NoteAlias $Alias + } + + $newNote = [PSNote]::New($Name, $Kind, $Snippet, $Details, $Alias, $Tags, $Catalog, $Run) + $script:_noteStore.AddNote($newNote) + } + + if (-not [string]::IsNullOrEmpty($newNote.Alias)) { + Set-Alias -Name $newNote.Alias -Value Get-PSNoteAlias -Scope Global -Force + } +} \ No newline at end of file diff --git a/src/Public/Remote/Get-RemoteCatalog.ps1 b/src/Public/Remote/Get-RemoteCatalog.ps1 new file mode 100644 index 0000000..69c4ba3 --- /dev/null +++ b/src/Public/Remote/Get-RemoteCatalog.ps1 @@ -0,0 +1,65 @@ +<# +.SYNOPSIS +Gets remote catalogs registered with PSNotes. + +.DESCRIPTION +Get-RemoteCatalog retrieves remote catalog registrations from the PSNotes configuration. Remote catalogs +represent external sources (such as a URL) that can be imported into PSNotes or kept registered for future +imports. + +Use -Catalog to return a specific remote catalog by name, or provide a wildcard pattern to match multiple +registrations. If no remote catalogs are configured, this cmdlet returns an empty array. + +.FUNCTIONALITY +PSNotes Remote Catalogs + +.ROLE +Public + +.COMPONENT +RemoteCatalogs + +.PARAMETER Catalog +The name or wildcard pattern of the remote catalog registration to retrieve. + +The default value is '*' which returns all configured remote catalogs. + +.EXAMPLE +PS> Get-RemoteCatalog + +Returns all configured remote catalogs. + +.EXAMPLE +PS> Get-RemoteCatalog -Catalog 'github' + +Returns the remote catalog registration named 'github'. + +.EXAMPLE +PS> Get-RemoteCatalog -Catalog 'git*' + +Returns all remote catalog registrations with names that match the pattern 'git*'. + +.OUTPUTS +System.Object + +.NOTES +- Remote catalog registrations are stored in the PSNotes configuration (for example, in the note store config file). +- If the PSNotes store is not initialized or no remote catalogs are configured, an empty array is returned. +- See also: Import-RemoteCatalog, Remove-RemoteCatalog + +.LINK + Remove-RemoteCatalog + Import-RemoteCatalog +#> +function Get-RemoteCatalog { + [CmdletBinding()] + param( + [parameter(Mandatory=$false)] + [string]$Catalog = '*' + ) + + if (-not $script:_noteStore.Config -or -not $script:_noteStore.Config.RemoteCatalogs) { return @() } + + $remote = $script:_noteStore.Config.RemoteCatalogs | Where-Object { $_.Name -like $Catalog } + return $remote +} diff --git a/src/Public/Remote/Import-RemoteCatalog.ps1 b/src/Public/Remote/Import-RemoteCatalog.ps1 new file mode 100644 index 0000000..c427397 --- /dev/null +++ b/src/Public/Remote/Import-RemoteCatalog.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS +Registers a remote PSNotes catalog or imports it as a local catalog. + +.DESCRIPTION +Import-RemoteCatalog adds a remote catalog source to PSNotes or downloads it immediately as a local +catalog. + +By default, the cmdlet registers the remote catalog URL in the PSNotes store under the provided name. +This allows you to keep the catalog “linked” for future imports. + +When -AsLocal is specified, the remote catalog is downloaded immediately and saved as a LOCAL catalog. +In this mode, the URL is not registered as a remote source. Use -Force to overwrite an existing local +catalog with the same name. + +.FUNCTIONALITY +PSNotes Remote Catalogs + +.ROLE +Public + +.COMPONENT +RemoteCatalogs + +.PARAMETER Name +The name to register for the remote catalog (or the name of the local catalog to create when -AsLocal is used). + +.PARAMETER Url +The URL of the remote catalog JSON. + +.PARAMETER AsLocal +Downloads the remote catalog immediately and creates a LOCAL catalog. + +When specified, the remote catalog URL is not registered. + +.PARAMETER Force +Only applies to -AsLocal. + +Overwrites the local catalog if it already exists. + +.PARAMETER PassThru +Returns the created or registered catalog object. + +.EXAMPLE +PS> Import-RemoteCatalog -Name 'github' -Url 'https://example.com/psnotes.json' + +Registers the remote catalog URL for later use. + +.EXAMPLE +PS> Import-RemoteCatalog -Name 'github' -Url 'https://example.com/psnotes.json' -AsLocal + +Downloads the remote catalog immediately and creates a local catalog. The URL is not registered. + +.EXAMPLE +PS> Import-RemoteCatalog -Name 'github' -Url 'https://example.com/psnotes.json' -AsLocal -Force -PassThru + +Overwrites the existing local catalog and returns the created catalog object. + +.OUTPUTS +System.Object + +.NOTES +- -AsLocal creates a local catalog immediately and does not register the URL as a remote catalog. +- Use Get-RemoteCatalog to view registered remote sources. +- See also: Get-RemoteCatalog, Remove-RemoteCatalog + +.LINK + Get-RemoteCatalog + Remove-RemoteCatalog +#> +function Import-RemoteCatalog { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Name, + + [Parameter(Mandatory)] + [string] $Url, + + # If set: downloads immediately and creates a LOCAL catalog. Does NOT register the URL. + [switch] $AsLocal, + + # Only applies to -AsLocal. Overwrite local catalog if it exists. + [switch] $Force, + + [switch] $PassThru + ) + + if ($AsLocal) { + $local = $script:_noteStore.ImportRemoteCatalogAsLocal($Name, $Url, [bool]$Force) + if ($PassThru) { return $local } + return + } + + $script:_noteStore.RegisterRemoteCatalog($Name, $Url) | ForEach-Object { + if ($PassThru) { $_ } + } +} diff --git a/src/Public/Remote/Remove-RemoteCatalog.ps1 b/src/Public/Remote/Remove-RemoteCatalog.ps1 new file mode 100644 index 0000000..d60ebc5 --- /dev/null +++ b/src/Public/Remote/Remove-RemoteCatalog.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS +Removes a remote catalog registration from PSNotes. + +.DESCRIPTION +Remove-RemoteCatalog unregisters one or more remote catalog sources from the PSNotes configuration. + +By default, this cmdlet removes only the remote registration and leaves any previously imported +local catalog content unchanged. + +If -ConvertToLocal is specified, the cached remote catalog is converted into a local catalog +before removing the remote registration. This allows you to keep the notes while removing the +remote dependency. + +This cmdlet supports pipeline input from Get-RemoteCatalog and implements ShouldProcess, +allowing the use of -WhatIf and -Confirm. + +.FUNCTIONALITY +PSNotes Remote Catalogs + +.ROLE +Public + +.COMPONENT +RemoteCatalogs + +.PARAMETER InputObject +A remote catalog object to remove. Accepts pipeline input from Get-RemoteCatalog. + +.PARAMETER ConvertToLocal +Converts the cached remote catalog into a local catalog before removing the remote registration. + +.PARAMETER Force +Suppresses confirmation prompts when removing a remote catalog registration. + +.PARAMETER PassThru +Returns the removed (or converted) catalog object. + +.EXAMPLE +PS> Get-RemoteCatalog + +Lists all registered remote catalogs. + +.EXAMPLE +PS> Get-RemoteCatalog -Catalog 'github' | Remove-RemoteCatalog + +Removes the 'github' remote catalog registration. + +.EXAMPLE +PS> Get-RemoteCatalog -Catalog 'github' | Remove-RemoteCatalog -ConvertToLocal + +Converts the cached remote catalog into a local catalog and removes the remote registration. + +.EXAMPLE +PS> Get-RemoteCatalog | Remove-RemoteCatalog -Force + +Removes all remote catalog registrations without prompting. + +.OUTPUTS +System.Object + +.NOTES +- Supports -WhatIf and -Confirm through ShouldProcess. +- Removing a remote catalog does not delete local catalogs unless explicitly converted or managed separately. +- See also: Get-RemoteCatalog, Import-RemoteCatalog + +.LINK + Get-RemoteCatalog + Import-RemoteCatalog +#> +function Remove-RemoteCatalog { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [RemoteCatalogSource] $InputObject, + + [switch] $ConvertToLocal, + + [switch] $Force, + + [switch] $PassThru + ) + + process { + $action = if ($ConvertToLocal) { + "Convert cached remote catalog to local and remove registration" + } + else { + "Remove remote catalog registration" + } + + if ($PSCmdlet.ShouldProcess($InputObject.Url, $action)) { + $removed = $script:_noteStore.RemoveRemoteCatalog($InputObject.Url, [bool]$ConvertToLocal, [bool]$Force) + if ($PassThru) { $removed } + } + } +} diff --git a/src/Public/Remove-PSNote.ps1 b/src/Public/Remove-PSNote.ps1 new file mode 100644 index 0000000..2ff11c6 --- /dev/null +++ b/src/Public/Remove-PSNote.ps1 @@ -0,0 +1,154 @@ +<# +.SYNOPSIS +Removes one or more PSNotes from the note store. + +.DESCRIPTION +Remove-PSNote deletes notes from the PSNotes store. You can target notes by name, alias, +catalog, tag, or by piping PSNote objects from Get-PSNote. + +This cmdlet updates the store immediately and permanently removes the selected notes. +Supports ShouldProcess, enabling the use of -WhatIf and -Confirm for safer operations. + +Use -Force to suppress confirmation prompts where applicable. + +.FUNCTIONALITY +PSNotes Notes + +.ROLE +Public + +.COMPONENT +Notes + +.PARAMETER Name +The name of the note to remove. Wildcards may be supported depending on implementation. + +.PARAMETER Catalog +Removes notes from the specified catalog. + +.PARAMETER Tag +Removes notes that contain one or more specified tags. + +.PARAMETER SearchString +Performs a broader search across note properties such as name, alias, and tags to identify notes for removal. + +.PARAMETER InputObject +One or more PSNote objects to remove. Accepts pipeline input from Get-PSNote. + +.PARAMETER Force +Suppresses confirmation prompts. + +.EXAMPLE +PS> Remove-PSNote -Name 'OldNote' + +Removes the note named 'OldNote'. + +.EXAMPLE +PS> Get-PSNote -Catalog 'Archive' | Remove-PSNote + +Removes all notes in the Archive catalog. + +.EXAMPLE +PS> Remove-PSNote -Alias 'azvm' -WhatIf + +Shows what would happen if the note with alias 'azvm' were removed. + +.OUTPUTS +PSNote + +.NOTES +- Supports -WhatIf and -Confirm through ShouldProcess. +- Deletions are permanent once committed. +- Use Get-PSNote to preview notes before removing them. +- See also: Get-PSNote, New-PSNote, Set-PSNote, Move-PSNote +#> +Function Remove-PSNote { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'Note')] + param( + # Pipeline input from Get-PSNote + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByObject')] + [object]$InputObject, + + # Discovery params (match Get-PSNote) + [Parameter(Mandatory = $false, ParameterSetName = 'Note')] + [string]$Name = '*', + + [Parameter(Mandatory = $false, ParameterSetName = 'Note')] + [string]$Tag, + + [Parameter(Mandatory = $false, ParameterSetName = 'Note')] + [Parameter(Mandatory = $false, ParameterSetName = 'Search')] + [string[]]$Catalog, + + [Parameter(Mandatory = $true, ParameterSetName = 'Search')] + [string]$SearchString, + + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + begin { + Test-PSNotesInitalize + + if ($Force -and -not $PSBoundParameters.ContainsKey('Confirm')) { + $ConfirmPreference = 'None' + } + + $candidates = New-Object System.Collections.Generic.List[object] + } + + process { + if ($PSCmdlet.ParameterSetName -eq 'ByObject') { + if ($null -ne $InputObject -and + ($null -ne $InputObject.PSObject.Properties['Name'] -or $null -ne $InputObject.PSObject.Properties['Note']) -and + $null -ne $InputObject.PSObject.Properties['Catalog']) { + $candidates.Add($InputObject) | Out-Null + } + return + } + + # Use Get-PSNote for discovery so Remove stays consistent with Get behavior. + $gpParams = @{} + if ($PSCmdlet.ParameterSetName -eq 'Search') { + $gpParams['SearchString'] = $SearchString + } else { + $gpParams['Name'] = $Name + if ($Tag) { $gpParams['Tag'] = $Tag } + } + + if ($Catalog) { $gpParams['Catalog'] = $Catalog } + + foreach ($n in @(Get-PSNote @gpParams)) { + $candidates.Add($n) | Out-Null + } + } + + end { + # De-dupe by Catalog+Note (Get-PSNote can return duplicates if the store contains them) + $unique = @{} + foreach ($n in $candidates) { + if ($null -eq $n) { continue } + $noteName = if ($null -ne $n.PSObject.Properties['Name']) { $n.Name } elseif ($null -ne $n.PSObject.Properties['Note']) { $n.Note } else { $null } + $key = "{0}::{1}" -f $n.Catalog, $noteName + if (-not $unique.ContainsKey($key)) { $unique[$key] = $n } + } + + if ($unique.Count -eq 0) { + Write-Verbose "No matching notes found. No action taken." + return + } + + $removed = New-Object System.Collections.Generic.List[object] + + foreach ($n in $unique.Values) { + $noteName = if ($null -ne $n.PSObject.Properties['Name']) { $n.Name } elseif ($null -ne $n.PSObject.Properties['Note']) { $n.Note } else { $null } + $desc = "Removing note '{0}' from catalog '{1}'" -f $noteName, $n.Catalog + if ($PSCmdlet.ShouldProcess($desc)) { + $script:_noteStore.RemoveNote($noteName, $n.Catalog) + $removed.Add($n) | Out-Null + } + } + + $removed + } +} \ No newline at end of file diff --git a/src/Public/Set-PSNote.ps1 b/src/Public/Set-PSNote.ps1 new file mode 100644 index 0000000..ce0a9a9 --- /dev/null +++ b/src/Public/Set-PSNote.ps1 @@ -0,0 +1,171 @@ +<# +.SYNOPSIS +Updates an existing PSNote or creates it if it does not already exist. + +.DESCRIPTION +Set-PSNote modifies one or more properties of an existing note in the PSNotes store. If the +target note does not exist in the specified catalog, a warning is written and the note is +created. + +Only the properties you provide are updated. Properties you do not specify remain unchanged. +Internally, this cmdlet delegates to New-PSNote with -Force to perform an “upsert”. + +The note Kind is inferred from the content parameter you use: +- Snippet or ScriptBlock sets Kind to 'Snippet' (inline code) +- ScriptPath sets Kind to 'Script' (file reference) + +Set-PSNote supports pipeline input by property name, which makes it easy to bulk update notes +from Get-PSNote output or structured data sources such as CSV. + +.FUNCTIONALITY +PSNotes Notes + +.ROLE +Public + +.COMPONENT +Notes + +.PARAMETER Name +The name of the note to update or create. + +Accepts pipeline input by property name. + +.PARAMETER Catalog +The catalog where the note is stored. Defaults to 'Default'. + +If the note does not exist in the specified catalog, it will be created there. +Accepts pipeline input by property name. + +.PARAMETER Snippet +The snippet text to store in the note. Replaces the existing snippet content. + +Accepts pipeline input by property name. + +.PARAMETER ScriptBlock +A PowerShell script block containing the code to store in the note. + +Accepts pipeline input by property name. + +.PARAMETER ScriptPath +A file path to a PowerShell script (.ps1). When specified, the note references this script and +Kind is set to 'Script'. The file must exist. + +Accepts pipeline input by property name. + +.PARAMETER Details +A description or additional information about the note. Replaces the existing details. + +Accepts pipeline input by property name. + +.PARAMETER Alias +A new alias for the note. + +Aliases may contain letters, numbers, dashes (-), and underscores (_). The alias is registered +as a global alias that invokes Get-PSNoteAlias. + +Accepts pipeline input by property name. + +.PARAMETER Tags +A list of tags to associate with the note. This replaces all existing tags. + +Accepts pipeline input by property name. + +.PARAMETER Run +Controls whether the note should be executed automatically when retrieved (if supported by your +workflow). Set to $true to enable, or $false to disable. + +Accepts pipeline input by property name. + +.EXAMPLE +PS> Set-PSNote -Name 'ADUser' -Tags 'AD','Users','Updated' + +Updates the Tags for the note 'ADUser' in the Default catalog, replacing any existing tags. + +.EXAMPLE +PS> $NewSnippet = '(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__))' +Set-PSNote -Name 'DayOfWeek' -Snippet $NewSnippet + +Updates only the snippet content for the note 'DayOfWeek' while leaving other properties unchanged. + +.EXAMPLE +PS> Set-PSNote -Name 'CpuUsage' -ScriptBlock { + Get-CimInstance Win32_Processor | + Measure-Object -Property LoadPercentage -Average | + Select-Object -ExpandProperty Average +} + +Updates the note 'CpuUsage' with a new multi-line script block. + +.EXAMPLE +PS> Set-PSNote -Name 'CpuUsage' -Details "Returns average CPU usage percentage" -Alias 'cpu' + +Updates only the Details and Alias properties for the note 'CpuUsage'. + +.EXAMPLE +PS> Set-PSNote -Name 'BackupScript' -ScriptPath 'D:\Scripts\Backup-Database.ps1' -Details "Updated backup script location" + +Updates an existing note to reference a different script file, changing Kind to 'Script'. + +.EXAMPLE +PS> Get-PSNote -Tag 'deprecated' | Set-PSNote -Tags 'archived','old' + +Bulk-updates notes by piping objects from Get-PSNote and replacing their Tags. + +.EXAMPLE +PS> [PSCustomObject]@{ + Name = 'MyNote' + Snippet = 'Get-Process | Select-Object -First 10' + Details = 'Top 10 processes' + Tags = @('process','monitoring') +} | Set-PSNote + +Creates or updates a note using pipeline input by property name. + +.OUTPUTS +PSNote + +.NOTES +- Supports -WhatIf and -Confirm through ShouldProcess. +- This cmdlet is a convenience wrapper around New-PSNote -Force (an “upsert” operation). +- Updates replace existing values rather than merging (for example, Tags are replaced, not appended). + To append tags, retrieve the note first and then set the combined list. +- See also: New-PSNote, Get-PSNote, Remove-PSNote, Get-PSNoteAlias +#> +Function Set-PSNote { + [cmdletbinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + param( + [parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$True)] + [string]$Name, + [parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$True)] + [string]$Catalog = 'Default', + [parameter(Mandatory = $false, ValueFromPipelineByPropertyName=$True)] + [string]$Snippet, + [parameter(Mandatory = $false, ValueFromPipelineByPropertyName=$True)] + [ScriptBlock]$ScriptBlock, + [parameter(Mandatory = $false, ValueFromPipelineByPropertyName=$True)] + [string]$ScriptPath, + [parameter(Mandatory = $false,ValueFromPipelineByPropertyName=$True)] + [string]$Details, + [parameter(Mandatory = $false,ValueFromPipelineByPropertyName=$True)] + [string]$Alias, + [parameter(Mandatory = $false,ValueFromPipelineByPropertyName=$True)] + [string[]]$Tags, + [parameter(Mandatory = $false,ValueFromPipelineByPropertyName=$True)] + [bool]$Run = $false + ) + + begin { + Test-PSNotesInitalize + } + + process { + $check = $script:_noteStore.Notes | Where-Object { $_.Name -eq $Name -and $_.Catalog -eq $Catalog } + if (-not $check) { + Write-Warning "The note '$Name' does not exist in catalog '$Catalog'. An attempt will be made to create it." + } + + New-PSNote @PSBoundParameters -Force + } +} \ No newline at end of file diff --git a/Public/ConvertTo-Splatting.ps1 b/src/Public/Splatting/ConvertTo-Splatting.ps1 similarity index 72% rename from Public/ConvertTo-Splatting.ps1 rename to src/Public/Splatting/ConvertTo-Splatting.ps1 index 4206fe4..a87dfd0 100644 --- a/Public/ConvertTo-Splatting.ps1 +++ b/src/Public/Splatting/ConvertTo-Splatting.ps1 @@ -1,79 +1,71 @@ +<# +.SYNOPSIS +Converts an existing PowerShell command into a splatting hashtable and splatted command. + +.DESCRIPTION +ConvertTo-Splatting takes a PowerShell command provided as a string or script block and rewrites it +into a splatting-friendly format. It parses the command, identifies the command name and parameters, +and produces: + +- A hashtable assignment (for example, $GetItemParam = @{ ... }) +- A command invocation that uses splatting (for example, Get-Item @GetItemParam) + +This is useful for refactoring long command lines into a clearer, more maintainable structure, and for +turning backtick-continued commands into a single normalized form. + +.FUNCTIONALITY +PowerShell Utilities + +.ROLE +Utility + +.COMPONENT +Splatting + +.PARAMETER Command +The command text to convert to splatting. Provide a full command line as a string. + +.PARAMETER ScriptBlock +The command to convert to splatting, provided as a script block. The script block is converted to text +and normalized prior to parsing. + +.EXAMPLE +$splatme = @' +Set-AzVMExtension -ExtensionName "MicrosoftMonitoringAgent" -ResourceGroupName "rg-xxxx" -VMName "vm-xxxx" ` + -Publisher "Microsoft.EnterpriseCloud.Monitoring" -ExtensionType "MicrosoftMonitoringAgent" ` + -TypeHandlerVersion "1.0" -Settings @{"workspaceId" = "xxxx"} ` + -ProtectedSettings @{"workspaceKey" = "xxxx"} -Location "uksouth" +'@ +ConvertTo-Splatting $splatme + +Creates a parameter hashtable and a splatted Set-AzVMExtension call. + +.EXAMPLE +$splatme = { Copy-Item -Path "test.txt" -Destination "test2.txt" -WhatIf } +ConvertTo-Splatting $splatme + +Converts a script block command into a hashtable and a splatted Copy-Item call. Switch parameters are +represented as $true. + +.EXAMPLE +$splatme = { + Get-AzVM ` + -ResourceGroupName "ResourceGroup11" ` + -Name "VirtualMachine07" ` + -Status +} +ConvertTo-Splatting $splatme + +Normalizes backtick line continuations and converts the command to splatting. + +.NOTES +- If the command starts with a variable assignment, the variable(s) are preserved as part of the parsed command. +- The hashtable variable name is derived from the command name (for example, Get-Item -> $GetItemParam). If that + name conflicts with an existing constant variable, a fallback name is used. +- For background on splatting, see: + about_Splatting - https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_splatting +#> Function ConvertTo-Splatting { - <# - .SYNOPSIS - Use to convert an existing PowerShell command to splatting - - .DESCRIPTION - Splatting is a much cleaner and safer way to shorten command lines without needing to use backtick. - This function excepts any command as a string or a scriptblock and will convert the existing parameters - to a hashtable and output the fully splatted command for you. - - .PARAMETER Command - The command string you want to convert to using splatting - - .PARAMETER ScriptBlock - The command scriptblock you want to convert to using splatting - - .EXAMPLE - $splatme = @' - Set-AzVMExtension -ExtensionName "MicrosoftMonitoringAgent" -ResourceGroupName "rg-xxxx" -VMName "vm-xxxx" -Publisher "Microsoft.EnterpriseCloud.Monitoring" -ExtensionType "MicrosoftMonitoringAgent" -TypeHandlerVersion "1.0" -Settings @{"workspaceId" = "xxxx" } -ProtectedSettings @{"workspaceKey" = "xxxx"} -Location "uksouth" - '@ - ConvertTo-Splatting $splatme - - Converts the string splatme to splatting - - --- Output ---- - $SetAzVMExtensionParam = @{ - ExtensionName = "MicrosoftMonitoringAgent" - ResourceGroupName = "rg-xxxx" - VMName = "vm-xxxx" - Publisher = "Microsoft.EnterpriseCloud.Monitoring" - ExtensionType = "MicrosoftMonitoringAgent" - TypeHandlerVersion = "1.0" - Settings = @{ "workspaceId" = "xxxx" } - ProtectedSettings = @{ "workspaceKey" = "xxxx" } - Location = "uksouth" - } - Set-AzVMExtension @SetAzVMExtensionParam - - .EXAMPLE - $splatme = { - Copy-Item -Path "test.txt" -Destination "test2.txt" -WhatIf - } - ConvertTo-Splatting $splatme - - Converts the scriptblock splatme to splatting - - --- Output ---- - $CopyItemParam = @{ - Path = "test.txt" - Destination = "test2.txt" - WhatIf = $true - } - Copy-Item @CopyItemParam - - .EXAMPLE - $splatme = { - Get-AzVM ` - -ResourceGroupName "ResourceGroup11" ` - -Name "VirtualMachine07" ` - -Status - } - ConvertTo-Splatting $splatme - - Removed backticks and converts the scriptblock splatme to splatting - - --- Output ---- - $GetAzVMParam = @{ - ResourceGroupName = "ResourceGroup11" - Name = "VirtualMachine07" - Status = $true - } - Get-AzVM @GetAzVMParam - - .NOTES - about_Splatting - https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting - #> [CmdletBinding()] param( [Parameter(ParameterSetName = 'string', Position = 0)] diff --git a/src/Public/Splatting/Get-CommandSplatting.ps1 b/src/Public/Splatting/Get-CommandSplatting.ps1 new file mode 100644 index 0000000..ece52d7 --- /dev/null +++ b/src/Public/Splatting/Get-CommandSplatting.ps1 @@ -0,0 +1,197 @@ +<# +.SYNOPSIS +Generates a splatting template for a PowerShell command. + +.DESCRIPTION +Get-CommandSplatting inspects a command’s parameter metadata and produces a ready-to-paste splatting +template. It returns one or more objects that include: + +- A variable “set block” with typed variable declarations for each parameter +- A hashtable “hash block” formatted for splatting (including required-parameter comments) +- A final example invocation that splats the hashtable into the command + +By default, the cmdlet outputs the default parameter set for the command. You can list available +parameter sets, generate a specific parameter set, or generate templates for all parameter sets. +Optionally include the PowerShell common parameters and/or copy the first generated template to the +clipboard. + +.FUNCTIONALITY +PowerShell Utilities + +.ROLE +Utility + +.COMPONENT +Splatting + +.PARAMETER Command +The name of the command to generate a splatting template for (cmdlet/function/alias supported by Get-Command). + +.PARAMETER ParameterSet +The name of a specific parameter set to generate. Use -ListParameterSets to discover available names. + +.PARAMETER ListParameterSets +Lists available parameter sets for the specified command, including whether each set is the default and the +parameter names in that set. + +.PARAMETER All +Generates splatting templates for all parameter sets for the specified command. + +.PARAMETER IncludeCommon +Includes PowerShell common parameters (for example: Verbose, Debug, ErrorAction) in the generated output. + +.PARAMETER Copy +Copies the first generated template (SetBlock + HashBlock) to the clipboard. + +.EXAMPLE +Get-CommandSplatting -Command 'Get-Item' + +Generates a splatting template for the default parameter set of Get-Item. + +.EXAMPLE +Get-CommandSplatting -Command 'Get-Item' -ListParameterSets + +Lists the available parameter sets for Get-Item and shows the parameters included in each set. + +.EXAMPLE +Get-CommandSplatting -Command 'Get-Item' -ParameterSet LiteralPath + +Generates a splatting template for the LiteralPath parameter set. + +.EXAMPLE +Get-CommandSplatting -Command 'Get-Item' -All + +Generates splatting templates for all parameter sets of Get-Item. + +.EXAMPLE +Get-CommandSplatting -Command 'Get-Item' -IncludeCommon -Copy + +Generates the default parameter set template including common parameters and copies the first template to the clipboard. + +.OUTPUTS +SplatBlock + +.NOTES +- Required parameters are annotated in the hashtable output with a "#Required" comment. +- Switch parameters are represented as [Boolean] variables in the set block, defaulting to $false. +- Use -ListParameterSets to discover parameter set names before using -ParameterSet. +#> +Function Get-CommandSplatting { + [CmdletBinding(DefaultParameterSetName = 'ParameterSet')] + param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Command, + [Parameter(ParameterSetName = 'ParameterSet', Position = 1)] + [string]$ParameterSet, + [Parameter(ParameterSetName = 'ListParameterSets', Position = 1)] + [switch]$ListParameterSets, + [Parameter(ParameterSetName = 'All', Position = 1)] + [switch]$All, + [Parameter(Mandatory = $false, Position = 2)] + [switch]$IncludeCommon, + [Parameter(Mandatory = $false, Position = 3)] + [switch]$Copy + ) + + # Get the command + $commandData = Get-Command $command + + # Get the parameter sets + $ParameterSets = $null + if ($All -eq $true) { + $ParameterSets = @($commandData.ParameterSets) + if ($ParameterSets.Count -eq 0) { + throw "Unable to find parameter sets" + } + } + elseif ($ListParameterSets -eq $true) { + $Output = $commandData.ParameterSets | Select-Object -Property @{l = 'ParameterSet'; e = { $_.Name } }, IsDefault, @{l = 'Parameters'; e = { @($_.Parameters.Name | + Where-Object { $_ -notin [System.Management.Automation.Cmdlet]::CommonParameters }) -join (', ') } + } | + ForEach-Object { + [SplatBlock]@{ + ParameterSet = $_.ParameterSet + IsDefault = $_.IsDefault + HashBlock = $_.Parameters + SetBlock = $null + } + } + } + elseif (-not [string]::IsNullOrEmpty($ParameterSet)) { + $ParameterSets = $commandData.ParameterSets | Where-Object { $_.Name -eq $ParameterSet } + if ($ParameterSets.Count -lt 1 -and $Output.Count -eq 0) { + throw "Unable to find parameter set '$($ParameterSet)'" + } + } + else { + $ParameterSets = @($commandData.ParameterSets | Where-Object { $_.IsDefault -eq $trufe }) + if ($ParameterSets.Count -eq 0) { + $ParameterSets = @($commandData.ParameterSets[0]) + } + if ($ParameterSets.Count -lt 1) { + throw "Unable to find the default parameter set" + } + } + + $hash = $command.Split('-')[-1] + + if ($ListParameterSets -ne $true) { + $Output = foreach ($set in $ParameterSets) { + [System.Collections.Generic.List[PSObject]]$hashBlock = @() + [System.Collections.Generic.List[PSObject]]$setBlock = @() + $hashBlock.Add("`$$($hash)$($set.Name) = @{") + $length = 0 + + $Parameters = $set.Parameters + if ($IncludeCommon -ne $true) { + $Parameters = $set.Parameters | Where-Object { $_.Name -notin + [System.Management.Automation.Cmdlet]::CommonParameters } + } + $Parameters.Name | ForEach-Object { if ($_.Length -gt $length) { $length = $_.length } } + + $LastOrder = $Parameters | Sort-Object Position | Select-Object -ExpandProperty Position -Last 1 + if ($LastOrder -ge 0) { + $SortedParameters = $Parameters | + Select-Object -Property *, @{l = 'Order'; e = { if ($_.Position -lt 0) { $LastOrder + 1 } else { $_.Position } } } | + Sort-Object Order + } + else { + $SortedParameters = $Parameters | Sort-Object Position + } + + Foreach ($p in $SortedParameters) { + $l = $length - $p.Name.Length + + if ($p.ParameterType.Name -eq 'SwitchParameter') { + $setBlock.Add("[Boolean]`$$($p.Name) = `$false # Switch") + } + else { + $setBlock.Add("[$($p.ParameterType)]`$$($p.Name) = ''") + } + + [string]$row = "`t$($p.Name)$(' ' * $l) = `$$($p.Name)" + if ($p.IsMandatory -eq $true) { + $row += "$(' ' * $l) #Required" + } + $hashBlock.Add($row) + } + $hashBlock.Add('}') + $hashBlock.Add("$command @$($hash)$($set.Name)") + [SplatBlock]@{ + Command = $Command + ParameterSet = $set.Name + IsDefault = $set.IsDefault + HashBlock = ($hashBlock -join ("`n")) + SetBlock = ($setBlock -join ("`n")) + } + } + } + + if($Copy -eq $true){ + $Output | Select-Object -First 1 | ForEach-Object{ + "$($_.SetBlock)`n$($_.HashBlock)" | Set-Clipboard + } + } + + $Output +} \ No newline at end of file diff --git a/src/Public/Update-PSNoteStore.ps1 b/src/Public/Update-PSNoteStore.ps1 new file mode 100644 index 0000000..b81f8e9 --- /dev/null +++ b/src/Public/Update-PSNoteStore.ps1 @@ -0,0 +1,82 @@ +<# +.SYNOPSIS +Updates PSNotes catalogs to the latest format. + +.DESCRIPTION +Update-PSNoteStore scans the PSNotes home directory for JSON catalogs and migrates any catalogs +that are not in the current format. Migrated content is then imported back into the PSNotes +store using the specified conflict behavior. + +Use -DefaultBehavior to control how note conflicts are handled during import: +- Prompt: prompts when conflicts occur +- SkipMigratedNotes: keeps existing notes and skips conflicting migrated notes +- OverwriteExistingNotes: overwrites existing notes with migrated versions + +.FUNCTIONALITY +PSNotes Store Maintenance + +.ROLE +Maintenance + +.COMPONENT +Store + +.PARAMETER DefaultBehavior +Determines how to handle existing notes when conflicts are detected during import. + +Valid values: +- Prompt +- SkipMigratedNotes +- OverwriteExistingNotes + +The default is 'Prompt'. + +.EXAMPLE +Update-PSNoteStore + +Migrates any outdated catalogs and prompts when conflicts occur. + +.EXAMPLE +Update-PSNoteStore -DefaultBehavior SkipMigratedNotes + +Migrates catalogs and skips notes that already exist. + +.EXAMPLE +Update-PSNoteStore -DefaultBehavior OverwriteExistingNotes + +Migrates catalogs and overwrites existing notes when conflicts occur. + +.LINK +https://github.com/mdowst/PSNotes + +.NOTES +- This cmdlet migrates catalogs discovered under $env:PSNOTES_HOME. +- Conflict behavior applies when importing migrated notes back into the store. +- See also: Initialize-PSNoteStore, Import-PSNote, Export-PSNote +#> +function Update-PSNoteStore { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param( + [ValidateSet('Prompt', 'SkipMigratedNotes', 'OverwriteExistingNotes')] + [parameter(Mandatory = $false)] + [string]$DefaultBehavior = 'Prompt' + ) + + Write-Verbose "Updating PSNoteStore to latest format..." + + $toMigrate = Get-ChildItem -Path $env:PSNOTES_HOME -Filter '*.json' | ForEach-Object { + if (-not [NoteCatalog]::VersionCheck($_.FullName)) { + Write-Verbose "Migrating $($_.BaseName)..." + $_ + } + } + + foreach ($catalogPath in $toMigrate) { + $migratedStore = [NoteCatalog]::Migrate($catalogPath.FullName, $true) + Import-PSNoteCatalog -ImportedCatalog $migratedStore -DestinationCatalog $migratedStore.Catalog -DefaultBehavior $DefaultBehavior + } + + $script:_noteStore.InitializeAliases() + Write-Verbose "PSNoteStore update complete." +} \ No newline at end of file diff --git a/tests/Build/BuildValidation.Tests.ps1 b/tests/Build/BuildValidation.Tests.ps1 new file mode 100644 index 0000000..3dd6798 --- /dev/null +++ b/tests/Build/BuildValidation.Tests.ps1 @@ -0,0 +1,59 @@ +Get-Module PSNotes | Remove-Module -Force -ErrorAction SilentlyContinue +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} + +BeforeAll { + Set-StrictMode -Version Latest + $psd1 = (Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1').FullName + Import-Module $psd1 -Force + $script:Version = [regex]::Match($psd1.ToLower(), '\\psnotes\\([0-9]+(?:\.[0-9]+)*)\\psnotes\.psd1').Groups[1].Value +} + +Describe 'Build Script Validation' { + It 'produces expected module files' { + $psm1Path = Join-Path $Global:TopLevel 'bin' 'PSNotes' $script:Version 'PSNotes.psm1' + $psd1Path = Join-Path $Global:TopLevel 'bin' 'PSNotes' $script:Version 'PSNotes.psd1' + $formatPath = Join-Path $Global:TopLevel 'bin' 'PSNotes' $script:Version 'PSNotes.format.ps1xml' + Test-Path $psm1Path | Should -BeTrue + Test-Path $psd1Path | Should -BeTrue + Test-Path $formatPath | Should -BeTrue + } + + It 'loads the module without errors' { + $modulePath = Join-Path $Global:TopLevel 'bin' 'PSNotes' $script:Version 'PSNotes.psd1' + Import-Module $modulePath -Force -ErrorAction Stop + $mod = Get-Module PSNotes + $mod | Should -Not -BeNullOrEmpty + $mod.Version.ToString() | Should -Be $script:Version + } + + It 'contains expected exported functions' { + $expectedFunctions = @( + 'Export-PSNote', + 'Get-PSNote', + 'Get-PSNoteAlias', + 'Get-PSNoteMenu', + 'Import-PSNote', + 'Initialize-PSNoteStore', + 'Move-PSNote', + 'New-PSNote', + 'Remove-PSNote', + 'Set-PSNote', + 'Update-PSNoteStore', + 'Get-RemoteCatalog', + 'Import-RemoteCatalog', + 'Remove-RemoteCatalog', + 'ConvertTo-Splatting', + 'Get-CommandSplatting' + ) + $exportedFunctions = (Get-Command -Module PSNotes -CommandType Function).Name + foreach ($func in $expectedFunctions) { + $exportedFunctions | Should -Contain $func + } + foreach ($func in $exportedFunctions) { + $expectedFunctions | Should -Contain $func + } + } +} \ No newline at end of file diff --git a/tests/ScriptAnalyzer/PSScriptAnalyzerSettings.psd1 b/tests/ScriptAnalyzer/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..8837e96 --- /dev/null +++ b/tests/ScriptAnalyzer/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,4 @@ +@{ + Severity=@('Error','Warning') + ExcludeRules=@('PSUseToExportFieldsInManifest','PSUseShouldProcessForStateChangingFunctions') +} \ No newline at end of file diff --git a/tests/ScriptAnalyzer/ScriptAnalyzer.Linter.ps1 b/tests/ScriptAnalyzer/ScriptAnalyzer.Linter.ps1 new file mode 100644 index 0000000..0ccfdc0 --- /dev/null +++ b/tests/ScriptAnalyzer/ScriptAnalyzer.Linter.ps1 @@ -0,0 +1,5 @@ +Import-Module PSScriptAnalyzer +$Source = Join-Path (Split-Path(Split-Path $PSScriptRoot)) 'src' +$Settings = Join-Path $PSScriptRoot 'PSScriptAnalyzerSettings.psd1' +Invoke-ScriptAnalyzer -Path $Source -Recurse -Settings $Settings + diff --git a/tests/ScriptAnalyzer/ScriptAnalyzer.Tests.ps1 b/tests/ScriptAnalyzer/ScriptAnalyzer.Tests.ps1 new file mode 100644 index 0000000..5f1e60d --- /dev/null +++ b/tests/ScriptAnalyzer/ScriptAnalyzer.Tests.ps1 @@ -0,0 +1,43 @@ +Describe 'PSSA' { + $TopLevel = $PSScriptRoot + while ( -not (Test-Path (Join-Path $TopLevel 'src'))) { + $TopLevel = Split-Path $TopLevel -Parent + } + $TestPath = Join-Path $TopLevel 'tests' + + # Get the linter settings + $ScriptAnalyzerSettings = Join-Path -Path $TestPath -ChildPath 'ScriptAnalyzer' + $linterSettings = Get-ChildItem -Path $ScriptAnalyzerSettings -Filter '*.psd1' -Recurse | ForEach-Object { + Import-PowerShellDataFile -Path $_.FullName + } + + # Get the tests ran based on the linter settings + $Severity = @('Error', 'Warning', 'Information') + if ($linterSettings.Severity) { + $Severity = $linterSettings.Severity | Select-Object -Unique + } + + $TestParameters = Get-ScriptAnalyzerRule -Severity $Severity | Where-Object { $_.RuleName -notin $linterSettings.ExcludeRules } | ForEach-Object { + @{Name = $_.RuleName } + } + + # Run PSScriptAnalyzer Linter + + + # Run Pester Tests + Context 'ScriptAnalyzer Linter Tests' { + BeforeAll { + # Run PSScriptAnalyzer Linter + $analysis = Get-ChildItem -Path $ScriptAnalyzerSettings -Filter '*.Linter.ps1' -Recurse | ForEach-Object { + . $_.FullName + } + } + It ' has no rule violations' -ForEach $TestParameters { + ($analysis | Where-Object { $_.RuleName -eq $Name }).Message | Select-Object -Unique | Should -BeNullOrEmpty + } + + It 'has no rule violations' { + $analysis.Count | Should -Be 0 + } + } +} \ No newline at end of file diff --git a/tests/UnitTests/Export-PSNote.Tests.ps1 b/tests/UnitTests/Export-PSNote.Tests.ps1 new file mode 100644 index 0000000..f64ad83 --- /dev/null +++ b/tests/UnitTests/Export-PSNote.Tests.ps1 @@ -0,0 +1,289 @@ +# Pester tests for Export-PSNote +Get-Module PSNotes | Remove-Module -Force +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} +BeforeAll { + Set-StrictMode -Version Latest + + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\ExportPSNote" + if(Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + # Set up test environment variable + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + $script:MockPath = Join-Path -Path $PSScriptRoot -ChildPath 'Mocks' + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName + Import-Module $psd1 -Force +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome + + # Clean up test directory + if (Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } +} + +Describe "Export-PSNote" { + BeforeEach { + # Define a fake note store for testing + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestPersonalStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Personal.json') -Force + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestWorkStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Work.json') -Force + + Initialize-PSNoteStore + + # Create an export directory for testing + $script:ExportDir = Join-Path $script:TestDir 'Exports' + $null = New-Item -Path $script:ExportDir -ItemType Directory -Force + } + + Context "Exporting notes by object" { + + It "exports a single PSNote object to JSON file" { + $note = Get-PSNote -Name 'az-login' + $exportPath = Join-Path $script:ExportDir 'single.json' + + $note | Export-PSNote -Path $exportPath + + Test-Path $exportPath | Should -Be $true + (Get-Content $exportPath | ConvertFrom-Json).Notes.Count | Should -BeGreaterThan 0 + } + + It "exports multiple PSNote objects to JSON file" { + $notes = Get-PSNote -Name 'cred*' + $exportPath = Join-Path $script:ExportDir 'multiple.json' + + $notes | Export-PSNote -Path $exportPath + + Test-Path $exportPath | Should -Be $true + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes.Count | Should -Be 2 + } + + It "exports all notes from a catalog using -Catalog parameter" { + $exportPath = Join-Path $script:ExportDir 'catalog.json' + + Export-PSNote -Catalog 'Personal' -Path $exportPath + + Test-Path $exportPath | Should -Be $true + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Catalog | Should -Be 'Personal' + } + + It "includes correct note properties in exported JSON" { + $note = Get-PSNote -Name 'az-login' + $exportPath = Join-Path $script:ExportDir 'properties.json' + + $note | Export-PSNote -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $exportedNote = $json.Notes[0] + $exportedNote | Get-Member -MemberType NoteProperty | Should -Not -BeNullOrEmpty + $exportedNote.Name | Should -Be 'az-login' + $exportedNote.Snippet | Should -Be 'Connect-AzAccount' + } + + It "excludes the Path property from exported JSON" { + $note = Get-PSNote -Name 'az-login' + $exportPath = Join-Path $script:ExportDir 'no-path.json' + + $note | Export-PSNote -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $exportedNote = $json.Notes[0] + $exportedNote.PSObject.Properties.Name | Should -Not -Contain 'Path' + } + } + + Context "File handling" { + + It "throws an error when file already exists without -Force" { + $exportPath = Join-Path $script:ExportDir 'existing.json' + + # Create initial file + $note = Get-PSNote -Name 'az-login' + $note | Export-PSNote -Path $exportPath + + # Attempt to overwrite without -Force + { Get-PSNote -Name 'day-one' | Export-PSNote -Path $exportPath -ErrorAction Stop } | Should -Throw + } + + It "overwrites existing file with -Force" { + $exportPath = Join-Path $script:ExportDir 'force-overwrite.json' + + # Create initial file + $note1 = Get-PSNote -Name 'az-login' + $note1 | Export-PSNote -Path $exportPath + + # Overwrite with -Force + $note2 = Get-PSNote -Name 'day-one' + $note2 | Export-PSNote -Path $exportPath -Force + + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes[0].Name | Should -Be 'day-one' + } + + It "creates JSON file with UTF8 encoding without BOM" { + $exportPath = Join-Path $script:ExportDir 'encoding.json' + $note = Get-PSNote -Name 'az-login' + + $note | Export-PSNote -Path $exportPath + + $bytes = [System.IO.File]::ReadAllBytes($exportPath) + # UTF8 without BOM should not start with EF BB BF + ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) | Should -Be $false + } + } + + Context "Pipeline input" { + + It "accepts notes from Get-PSNote pipeline" { + $exportPath = Join-Path $script:ExportDir 'pipeline.json' + + Get-PSNote -Name 'az-login' | Export-PSNote -Path $exportPath + + Test-Path $exportPath | Should -Be $true + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes[0].Name | Should -Be 'az-login' + } + + It "handles multiple notes from pipeline" { + $exportPath = Join-Path $script:ExportDir 'pipeline-multiple.json' + + Get-PSNote | Export-PSNote -Path $exportPath + + Test-Path $exportPath | Should -Be $true + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes.Count | Should -BeGreaterThan 1 + } + + It "handles filtered notes from Get-PSNote -Tag" { + $exportPath = Join-Path $script:ExportDir 'filtered-tag.json' + + Get-PSNote -Tag 'Azure' | Export-PSNote -Path $exportPath + + Test-Path $exportPath | Should -Be $true + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes[0].Tags | Should -Contain 'Azure' + } + } + + Context "Catalog parameter set" { + + It "exports all notes from Personal catalog" { + $exportPath = Join-Path $script:ExportDir 'personal-catalog.json' + + Export-PSNote -Catalog 'Personal' -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Catalog | Should -Be 'Personal' + $json.Notes | Should -Not -BeNullOrEmpty + } + + It "exports all notes from Work catalog" { + $exportPath = Join-Path $script:ExportDir 'work-catalog.json' + + Export-PSNote -Catalog 'Work' -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Catalog | Should -Be 'Work' + } + + It "sets catalog name to the exported catalog name" { + $exportPath = Join-Path $script:ExportDir 'catalog-name.json' + + Export-PSNote -Catalog 'Personal' -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Catalog | Should -Be 'Personal' + } + } + + Context "Export object structure" { + + It "creates a valid NoteCatalog structure in JSON" { + $exportPath = Join-Path $script:ExportDir 'structure.json' + $note = Get-PSNote -Name 'az-login' + + $note | Export-PSNote -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $json | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name | Should -Contain 'StoreVersion' + $json | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name | Should -Contain 'Catalog' + $json | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name | Should -Contain 'Notes' + } + + It "sets Catalog to 'Export' when exporting from Note parameter set" { + $exportPath = Join-Path $script:ExportDir 'export-catalog.json' + $note = Get-PSNote -Name 'az-login' + + $note | Export-PSNote -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Catalog | Should -Be 'Export' + } + } + + Context "Complex scenarios" { + + It "exports notes with special characters in Details" { + New-PSNote -Name 'SpecialChars' -Snippet 'Test' -Details 'Text with "quotes" and special chars: @#$%' -Force + $exportPath = Join-Path $script:ExportDir 'special-chars.json' + + Get-PSNote -Name 'SpecialChars' | Export-PSNote -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes[0].Details | Should -Match 'quotes' + } + + It "exports notes with multiline Snippet" { + $multilineSnippet = @' +Get-Process | + Where-Object {$_.Memory -gt 100MB} | + Select-Object -Property Name, Id, Memory +'@ + New-PSNote -Name 'Multiline' -Snippet $multilineSnippet -Force + $exportPath = Join-Path $script:ExportDir 'multiline.json' + + Get-PSNote -Name 'Multiline' | Export-PSNote -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes[0].Snippet | Should -Match 'Get-Process' + } + + It "exports notes with multiple tags" { + New-PSNote -Name 'MultiTag' -Snippet 'Test' -Tags 'Tag1', 'Tag2', 'Tag3' -Force + $exportPath = Join-Path $script:ExportDir 'multi-tag.json' + + Get-PSNote -Name 'MultiTag' | Export-PSNote -Path $exportPath + + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes[0].Tags.Count | Should -BeGreaterThan 2 + } + + It "exports notes and can be re-imported" { + $exportPath = Join-Path $script:ExportDir 'reimport.json' + $importDir = Join-Path $script:TestDir 'Import' + $null = New-Item -Path $importDir -ItemType Directory -Force + + # Export notes + Get-PSNote -Name 'az-login' | Export-PSNote -Path $exportPath + + # Import the exported file + Import-PSNote -Path $exportPath -Catalog 'Imported' -DefaultBehavior 'OverwriteExistingNotes' + + # Verify the imported note exists + $imported = Get-PSNote -Name 'az-login' + $imported | Should -Not -BeNullOrEmpty + } + } +} diff --git a/tests/UnitTests/Get-PSNote.Tests.ps1 b/tests/UnitTests/Get-PSNote.Tests.ps1 new file mode 100644 index 0000000..f4d12a0 --- /dev/null +++ b/tests/UnitTests/Get-PSNote.Tests.ps1 @@ -0,0 +1,106 @@ +# Pester tests for Get-PSNote +Get-Module PSNotes | Remove-Module -Force +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} +BeforeAll { + Set-StrictMode -Version Latest + + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\GetPSNote" + if(Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + # Set up test environment variable + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + + $script:MockPath = Join-Path -Path $PSScriptRoot -ChildPath 'Mocks' + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName + Import-Module $psd1 -Force +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe "Get-PSNote" { + BeforeEach { + # Define a fake note store for testing + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestPersonalStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Personal.json') -Force + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestWorkStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Work.json') -Force + + Initialize-PSNoteStore + } + + Context "Note parameter set" { + + It "returns all notes when called with no parameters" { + $r = Get-PSNote + @($r).Count | Should -Be 4 + } + + It "filters notes by -Name wildcard" { + $r = Get-PSNote -Name 'cred*' + @($r).Count | Should -Be 2 + $r.Name | Should -Contain 'creds' + $r.Name | Should -Contain 'creds2' + } + + It "filters notes by -Tag (exact match)" { + $r = Get-PSNote -Tag 'Azure' + @($r).Count | Should -Be 1 + $r[0].Name | Should -Be 'az-login' + } + + It "filters notes by -Name and -Tag together" { + $r = Get-PSNote -Name '*cred*' -Tag 'AD' + @($r).Count | Should -Be 1 + $r[0].Name | Should -Be 'creds' + } + + It "copies the first returned snippet to clipboard when -Copy is used" { + Mock -CommandName Set-Clipboard -MockWith { param($Value) } -Verifiable -ModuleName PSNotes + Mock -CommandName Get-Command -MockWith { [pscustomobject]@{ Name = 'Set-Clipboard' } } -ModuleName PSNotes + Mock -CommandName Read-Host -MockWith { param($Prompt) 1 } -ModuleName PSNotes + + Get-PSNote -Name 'cred*' -Copy | Out-Null + + Assert-MockCalled -CommandName Set-Clipboard -Times 1 -Exactly -ModuleName PSNotes -ParameterFilter { + $Value -eq 'Get-Credential' + } + } + + It "invokes Invoke-PSNote with the first note when -Run is used" { + Mock -CommandName Invoke-PSNote -MockWith { param($Note) } -Verifiable -ModuleName PSNotes + Mock -CommandName Read-Host -MockWith { param($Prompt) 1 } -ModuleName PSNotes + + Get-PSNote -Name 'cred*' -Run | Out-Null + + Assert-MockCalled -CommandName Invoke-PSNote -Times 1 -Exactly -ModuleName PSNotes -ParameterFilter { + $Note.Name -eq 'creds' + } + } + } + + Context "Search parameter set" { + + It "returns notes where SearchString matches Note/Alias/Details/Snippet" { + $r = Get-PSNote -SearchString 'Azure' + @($r).Count | Should -BeGreaterThan 0 + $r.Name | Should -Contain 'az-login' + } + + It "returns notes where SearchString matches Tags (even if other fields don't match)" { + $r = Get-PSNote -SearchString 'Journal' + @($r).Count | Should -Be 1 + $r[0].Name | Should -Be 'day-one' + } + } + +} \ No newline at end of file diff --git a/tests/UnitTests/Get-PSNoteAlias.Tests.ps1 b/tests/UnitTests/Get-PSNoteAlias.Tests.ps1 new file mode 100644 index 0000000..30bab9e --- /dev/null +++ b/tests/UnitTests/Get-PSNoteAlias.Tests.ps1 @@ -0,0 +1,120 @@ +# Pester tests for Get-PSNote +Get-Module PSNotes | Remove-Module -Force +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} +BeforeAll { + Set-StrictMode -Version Latest + + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\GetPSNoteAlias" + if(Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + # Set up test environment variable + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + + $script:MockPath = Join-Path -Path $Global:TopLevel -ChildPath 'tests\UnitTests\Mocks' + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName + Import-Module $psd1 -Force +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe "Get-PSNoteAlias" { + BeforeEach { + # Define a fake note store for testing + Get-ChildItem -Path $script:TestDir -Filter '*.json' | Remove-Item -Force + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestPersonalStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Personal.json') -Force + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestWorkStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Work.json') -Force + + Initialize-PSNoteStore + } + + Context "Default aliases" { + + It "default to run" { + day | Should -Be 'day' + } + + It "default to copy" { + c2 | Should -Contain 'Write-Output creds2' + } + } + + Context "Copy switch" { + BeforeEach { + Mock Set-Clipboard -ModuleName PSNotes + } + + It "default to run with -Copy" { + day -Copy | Should -Be 'Write-Output day' + Should -Invoke Set-Clipboard -ParameterFilter { $Value -eq 'Write-Output day' } -Times 1 -ModuleName PSNotes + } + + It "default to copy with -Copy" { + c2 -Copy | Should -Contain 'Write-Output creds2' + Should -Invoke Set-Clipboard -ParameterFilter { $Value -eq 'Write-Output creds2' } -Times 1 -ModuleName PSNotes + } + } + + Context "Run switch" { + + It "default to run with -Run" { + day -Run | Should -Be 'day' + } + + It "default to copy with -Copy" { + c2 -Run | Should -Contain 'creds2' + } + } + + Context "Script execution and clipboard copying" { + + It "executes the note by default" { + $scriptFile = Join-Path $script:TestDir 'TestScriptPath.ps1' + Set-Content -Path $scriptFile -Value '"Hello Pester"' -Force + + New-PSNote -Name 'TestScriptPath' -ScriptPath $scriptFile -Details 'Test script path' -Catalog 'TestScriptStore' -Alias 'TestScriptPath' + + TestScriptPath | Should -Be 'Hello Pester' + } + + It "copies to clipboard without executing when -Copy is used" { + $scriptFile = Join-Path $script:TestDir 'TestScriptPath.ps1' + Set-Content -Path $scriptFile -Value '"Hello Pester"' -Force + + New-PSNote -Name 'TestScriptPath' -ScriptPath $scriptFile -Details 'Test script path' -Catalog 'TestScriptStore' -Alias 'TestScriptPath' + + TestScriptPath -Copy | Should -Be $scriptFile + } + + It "copies to clipboard without executing when -Copy is used" { + $scriptFile = Join-Path $script:TestDir 'TestScriptPath.ps1' + Set-Content -Path $scriptFile -Value '"Hello Pester"' -Force + + New-PSNote -Name 'TestScriptPath' -ScriptPath $scriptFile -Details 'Test script path' -Catalog 'TestScriptStore' -Alias 'TestScriptPath' + + TestScriptPath -Copy | Should -Be $scriptFile + } + } + + Context "Error handling" { + + It "returns an error when called directly" { + { Get-PSNoteAlias -ErrorAction Stop } | Should -Throw "The Get-PSNoteAlias cmdlet is designed to be called using an alias and not directly." + } + + It "returns an error when alias does not exist" { + { NonExistentAlias } | Should -Throw + } + } +} \ No newline at end of file diff --git a/tests/UnitTests/Get-RemoteCatalog.Tests.ps1 b/tests/UnitTests/Get-RemoteCatalog.Tests.ps1 new file mode 100644 index 0000000..4690be0 --- /dev/null +++ b/tests/UnitTests/Get-RemoteCatalog.Tests.ps1 @@ -0,0 +1,88 @@ +Get-Module PSNotes | Remove-Module -Force -ErrorAction SilentlyContinue + +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} + +BeforeAll { + Set-StrictMode -Version Latest + + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\RemoteCatalog" + if (Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | + Select-Object -Last 1 -ExpandProperty FullName + + Import-Module $psd1 -Force + + function New-MockCatalogJson { + param( + [Parameter(Mandatory)][string] $CatalogName + ) + + @( + [pscustomobject]@{ + Catalog = $CatalogName + Path = 'Ignored.json' + StoreVersion = 1 + Notes = @( + @{ + Note = 'remote-note-1' + Snippet = 'Get-Date' + Details = 'Remote note 1' + Alias = 'r1' + Tags = @('Remote','Test') + Catalog = $CatalogName + Run = $false + Kind = 'Snippet' + } + ) + } + ) | ConvertTo-Json -Depth 10 + } +} + +AfterAll { + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe "Remote Catalog Commands" { + + BeforeEach { + # Clean store between tests + Get-ChildItem -Path $env:PSNOTES_HOME -Recurse -ErrorAction SilentlyContinue | + Remove-Item -Force -Recurse -ErrorAction SilentlyContinue + + Initialize-PSNoteStore + } + + Context "Get-RemoteCatalog" { + + It "returns an empty array when no remotes are registered" { + $remotes = @(Get-RemoteCatalog) + $remotes.Count | Should -Be 0 + } + + It "returns registered remotes" { + Import-RemoteCatalog -Name 'Tools' -Url 'https://example.invalid/tools.json' + Import-RemoteCatalog -Name 'Math' -Url 'https://example.invalid/math.json' + + $remotes = @(Get-RemoteCatalog) + $remotes.Count | Should -Be 2 + + # Don’t over-assume shape — just verify url/name are present if your object has them + # If your RemoteCatalogSource uses different property names, adjust these assertions. + @($remotes.Url) | Should -Contain 'https://example.invalid/tools.json' + @($remotes.Url) | Should -Contain 'https://example.invalid/math.json' + @($remotes.Name) | Should -Contain 'Tools' + @($remotes.Name) | Should -Contain 'Math' + } + } +} diff --git a/tests/UnitTests/Import-PSNote.Tests.ps1 b/tests/UnitTests/Import-PSNote.Tests.ps1 new file mode 100644 index 0000000..11d036a --- /dev/null +++ b/tests/UnitTests/Import-PSNote.Tests.ps1 @@ -0,0 +1,409 @@ +# Pester tests for Import-PSNote +Get-Module PSNotes | Remove-Module -Force +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} +BeforeAll { + Set-StrictMode -Version Latest + + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\ImportPSNote" + if(Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + # Set up test environment variable + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + + $script:MockPath = Join-Path -Path $PSScriptRoot -ChildPath 'Mocks' + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName + Import-Module $psd1 -Force +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe "Import-PSNote" { + BeforeEach { + # Initialize clean store for each test + # Clean up any existing catalog files + Get-ChildItem -Path $env:PSNOTES_HOME -Filter '*.json' | Remove-Item -Force -ErrorAction SilentlyContinue + Initialize-PSNoteStore + Get-ChildItem -Path $script:MockPath -Filter '*.json' | Copy-Item -Destination $env:PSNOTES_HOME -Force + } + + Context "Basic import functionality" { + + It "imports notes from a valid current format file" { + $importPath = Join-Path $env:PSNOTES_HOME 'TestPersonalStore.json' + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $notes = Get-PSNote + @($notes).Count | Should -BeGreaterThan 0 + } + + It "imports notes to default catalog when not specified" { + $importPath = Join-Path $env:PSNOTES_HOME 'TestPersonalStore.json' + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $notes = Get-PSNote -Catalog 'Default' + @($notes).Count | Should -BeGreaterThan 0 + } + + It "imports notes to specified catalog" { + $importPath = Join-Path $env:PSNOTES_HOME 'TestPersonalStore.json' + Import-PSNote -Path $importPath -Catalog 'TestImport' -DefaultBehavior SkipMigratedNotes + + $notes = Get-PSNote -Catalog 'TestImport' + @($notes).Count | Should -BeGreaterThan 0 + $notes | ForEach-Object { + $_.Catalog | Should -Be 'TestImport' + } + } + + It "imports note with all properties intact" { + # Create a test file with a specific note + $testNote = @( + @{ + Note = "test-import" + Snippet = "Get-Date" + Details = "Test import note" + Alias = "testimport" + Tags = @("Test", "Import") + } + ) + $importPath = Join-Path $env:PSNOTES_HOME 'ImportTest.json' + $testNote | ConvertTo-Json -Depth 10 | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $imported = Get-PSNote -Name 'test-import' + $imported.Name | Should -Be 'test-import' + $imported.Snippet | Should -Be 'Get-Date' + $imported.Details | Should -Be 'Test import note' + $imported.Alias | Should -Be 'testimport' + $imported.Tags | Should -Contain 'Test' + $imported.Tags | Should -Contain 'Import' + } + } + + Context "Legacy format migration" { + + It "imports and migrates legacy format files" { + $importPath = Join-Path $env:PSNOTES_HOME 'TestPSNotesv0.json' + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $notes = Get-PSNote + @($notes).Count | Should -BeGreaterThan 0 + } + + It "validates legacy format before migration" { + # Create a legacy format file + $legacyNotes = @( + @{ + Note = "legacy-note" + Snippet = "Write-Host 'Legacy'" + Details = "Legacy format note" + Alias = "legacy" + Tags = @("Legacy") + } + ) + $importPath = Join-Path $env:PSNOTES_HOME 'LegacyImport.json' + $legacyNotes | ConvertTo-Json | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $imported = Get-PSNote -Name 'legacy-note' + $imported | Should -Not -BeNullOrEmpty + $imported.Name | Should -Be 'legacy-note' + } + } + + Context "Duplicate handling with DefaultBehavior parameter" { + + It "skips migrated notes when DefaultBehavior is SkipMigratedNotes" { + # Create initial note + New-PSNote -Name 'duplicate-test' -Snippet 'Write-Output "Original"' -Details 'Original note' -Tags 'Original' -Alias 'duptest' + + # Create import file with same alias + $testNote = @( + @{ + Note = "duplicate-test-new" + Snippet = "Write-Output 'New'" + Details = "New note" + Alias = "duptest" + Tags = @("New") + } + ) + $importPath = Join-Path $script:TestDir 'DuplicateTest.json' + $testNote | ConvertTo-Json -Depth 10 | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + # Verify original note still exists + $note = Get-PSNote -SearchString 'duptest' + $note.Name | Should -Be 'duplicate-test' + $note.Details | Should -Be 'Original note' + } + + It "overwrites existing notes when DefaultBehavior is OverwriteExistingNotes" { + # Create initial note + New-PSNote -Name 'overwrite-test' -Snippet 'Write-Output "Original"' -Details 'Original note' -Tags 'Original' -Alias 'overtest' + + # Create import file with same alias + $testNote = @( + @{ + Note = "overwrite-test-new" + Snippet = "Write-Output 'New'" + Details = "New note" + Alias = "overtest" + Tags = @("New") + } + ) + $importPath = Join-Path $script:TestDir 'OverwriteTest.json' + $testNote | ConvertTo-Json -Depth 10 | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -DefaultBehavior OverwriteExistingNotes + + # Verify new note exists + $note = Get-PSNote -SearchString 'overtest' + $note.Name | Should -Be 'overwrite-test-new' + $note.Details | Should -Be 'New note' + } + } + + Context "Validation and error handling" { + + It "throws error for invalid JSON file" { + $invalidPath = Join-Path $script:TestDir 'Invalid.json' + Set-Content -Path $invalidPath -Value "This is not valid JSON" + + { Import-PSNote -Path $invalidPath -ErrorAction Stop } | Should -Throw + } + + It "throws error for file with invalid structure" { + $invalidStructure = @{ + InvalidProperty = "This is not a valid PSNotes file" + } + $invalidPath = Join-Path $script:TestDir 'InvalidStructure.json' + $invalidStructure | ConvertTo-Json | Out-File -FilePath $invalidPath -Encoding UTF8NoBOM + + { Import-PSNote -Path $invalidPath -ErrorAction Stop } | Should -Throw + } + + It "throws error when file does not exist" { + $nonExistentPath = Join-Path $script:TestDir 'DoesNotExist.json' + + { Import-PSNote -Path $nonExistentPath -ErrorAction Stop } | Should -Throw + } + + It "validates notes before import" { + # Create a valid format file + $validNote = @( + @{ + Note = "valid-note" + Snippet = "Get-Process" + Details = "Valid note" + Alias = "validnote" + Tags = @("Test") + } + ) + $validPath = Join-Path $script:TestDir 'ValidImport.json' + $validNote | ConvertTo-Json -Depth 10 | Out-File -FilePath $validPath -Encoding UTF8NoBOM + + { Import-PSNote -Path $validPath -DefaultBehavior SkipMigratedNotes } | Should -Not -Throw + } + } + + Context "Multiple notes import" { + + It "imports multiple notes from single file" { + # Create file with multiple notes + $multipleNotes = @( + @{ + Note = "note-one" + Snippet = "Get-Process" + Details = "First note" + Alias = "note1" + Tags = @("Test") + }, + @{ + Note = "note-two" + Snippet = "Get-Service" + Details = "Second note" + Alias = "note2" + Tags = @("Test") + }, + @{ + Note = "note-three" + Snippet = "Get-ChildItem" + Details = "Third note" + Alias = "note3" + Tags = @("Test") + } + ) + $importPath = Join-Path $script:TestDir 'MultipleNotes.json' + $multipleNotes | ConvertTo-Json -Depth 10 | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $imported = Get-PSNote + @($imported).Count | Should -BeGreaterOrEqual 3 + Get-PSNote -Name 'note-one' | Should -Not -BeNullOrEmpty + Get-PSNote -Name 'note-two' | Should -Not -BeNullOrEmpty + Get-PSNote -Name 'note-three' | Should -Not -BeNullOrEmpty + } + + It "preserves unique aliases for all imported notes" { + $multipleNotes = @( + @{ + Note = "alias-test-1" + Snippet = "Get-Process" + Details = "First note" + Alias = "alias1" + Tags = @("Test") + }, + @{ + Note = "alias-test-2" + Snippet = "Get-Service" + Details = "Second note" + Alias = "alias2" + Tags = @("Test") + } + ) + $importPath = Join-Path $script:TestDir 'AliasTest.json' + $multipleNotes | ConvertTo-Json -Depth 10 | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $note1 = Get-PSNote -SearchString 'alias1' + $note2 = Get-PSNote -SearchString 'alias2' + $note1 | Should -Not -BeNullOrEmpty + $note2 | Should -Not -BeNullOrEmpty + $note1[0].Alias | Should -Be 'alias1' + $note2[0].Alias | Should -Be 'alias2' + } + } + + Context "Special characters and encoding" { + + It "handles notes with special characters" { + $specialNote = @( + @{ + Note = "special-chars" + Snippet = 'Get-Process | Where-Object {$_.Name -like "*code*"}' + Details = 'Has $special, "quotes", and symbols: @#$%^&*()' + Alias = "special" + Tags = @("Test", "Special") + } + ) + $importPath = Join-Path $script:TestDir 'SpecialChars.json' + $specialNote | ConvertTo-Json -Depth 10 | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $imported = Get-PSNote -Name 'special-chars' + $imported.Snippet | Should -Match '\$' + $imported.Details | Should -Match '\$special' + } + + It "handles notes with multiline snippets" { + $multilineSnippet = @" +Get-Process | + Where-Object CPU -GT 10 | + Select-Object Name, CPU +"@ + $multilineNote = @( + @{ + Note = "multiline" + Snippet = $multilineSnippet + Details = "Multiline snippet" + Alias = "multiline" + Tags = @("Test") + } + ) + $importPath = Join-Path $script:TestDir 'Multiline.json' + $multilineNote | ConvertTo-Json -Depth 10 | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $imported = Get-PSNote -Name 'multiline' + $imported.Snippet | Should -Match 'Get-Process' + $imported.Snippet | Should -Match 'Select-Object' + } + } + + Context "Parameter validation" { + + It "requires Path parameter" { + #{ Import-PSNote -ErrorAction Stop } | Should -Throw + } + + It "accepts valid Catalog parameter" { + $importPath = Join-Path $env:PSNOTES_HOME 'TestPersonalStore.json' + { Import-PSNote -Path $importPath -Catalog 'TestCatalog' -DefaultBehavior SkipMigratedNotes } | Should -Not -Throw + } + + It "accepts valid DefaultBehavior values" { + $importPath = Join-Path $env:PSNOTES_HOME 'TestPersonalStore.json' + { Import-PSNote -Path $importPath -DefaultBehavior 'Prompt' } | Should -Not -Throw + { Import-PSNote -Path $importPath -DefaultBehavior 'SkipMigratedNotes' } | Should -Not -Throw + { Import-PSNote -Path $importPath -DefaultBehavior 'OverwriteExistingNotes' } | Should -Not -Throw + } + } + + Context "Integration with catalog system" { + + It "updates note store after import" { + $countBefore = @(Get-PSNote).Count + + $newNote = @( + @{ + Note = "integration-test" + Snippet = "Write-Output 'Test'" + Details = "Integration test" + Alias = "inttest" + Tags = @("Test") + } + ) + $importPath = Join-Path $script:TestDir 'Integration.json' + $newNote | ConvertTo-Json -Depth 10 | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -DefaultBehavior SkipMigratedNotes + + $countAfter = @(Get-PSNote).Count + $countAfter | Should -BeGreaterThan $countBefore + } + + It "creates catalog file if it does not exist" { + $newCatalog = "NewTestCatalog" + $catalogPath = Join-Path $env:PSNOTES_HOME "$newCatalog.json" + + if (Test-Path $catalogPath) { + Remove-Item $catalogPath -Force + } + + $newNote = @( + @{ + Note = "new-catalog-test" + Snippet = "Get-Date" + Details = "New catalog test" + Alias = "newcat" + Tags = @("Test") + } + ) + $importPath = Join-Path $script:TestDir 'NewCatalog.json' + $newNote | ConvertTo-Json -Depth 10 | Out-File -FilePath $importPath -Encoding UTF8NoBOM + + Import-PSNote -Path $importPath -Catalog $newCatalog -DefaultBehavior SkipMigratedNotes + + Test-Path $catalogPath | Should -Be $true + } + } +} diff --git a/tests/UnitTests/Import-RemoteCatalog.Tests.ps1 b/tests/UnitTests/Import-RemoteCatalog.Tests.ps1 new file mode 100644 index 0000000..b8314d0 --- /dev/null +++ b/tests/UnitTests/Import-RemoteCatalog.Tests.ps1 @@ -0,0 +1,241 @@ +# Pester tests for Import-RemoteCatalog + +Get-Module PSNotes | Remove-Module -Force -ErrorAction SilentlyContinue + +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} + +BeforeAll { + Set-StrictMode -Version Latest + + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\ImportRemoteCatalog" + if (Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + # Set up test environment variable + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + + # Import the built module from bin + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | + Select-Object -Last 1 -ExpandProperty FullName + + Import-Module $psd1 -Force + + # A "current-format" catalog JSON payload that NoteCatalog.Open should accept. + # (Versioned object with Notes array) + $script:MockRemoteCatalogJson = @( + [pscustomobject]@{ + Catalog = 'RemotePayloadCatalog' + Path = 'Ignored.json' + StoreVersion = 1 + Notes = @( + @{ + Note = 'remote-note-1' + Snippet = 'Get-Date' + Details = 'Remote note 1' + Alias = 'r1' + Tags = @('Remote','Test') + Catalog = 'RemotePayloadCatalog' + }, + @{ + Note = 'remote-note-2' + Snippet = 'Get-Process | Select-Object -First 1' + Details = 'Remote note 2' + Alias = 'r2' + Tags = @('Remote','Test') + Catalog = 'RemotePayloadCatalog' + } + ) + } + ) | ConvertTo-Json -Depth 10 + + # Your real URLs (optional integration) + $script:RealUrls = @( + 'https://gist.githubusercontent.com/mdowst/7198756f760ad0de0f635aaef5c4d338/raw/0676b9c41b27c40c0d8a28cd1e166838a2639828/RemotePSNote.json', + 'https://gist.githubusercontent.com/mdowst/a01faa8ed7bdb560e585420a5b97ae06/raw/a895c50ba700f20fb019e948c28fb5c3797432ff/PSNotes.Math.json' + ) + # Load the class file + . "$Global:TopLevel\src\Classes\NoteStore.class.ps1" +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe "Import-RemoteCatalog" { + + BeforeEach { + # Clean out store between tests + Get-ChildItem -Path $env:PSNOTES_HOME -Filter '*.json' -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + + # Also clean nested folders (config/remote) if they exist + if (Test-Path (Join-Path $env:PSNOTES_HOME 'config')) { + Remove-Item (Join-Path $env:PSNOTES_HOME 'config') -Recurse -Force -ErrorAction SilentlyContinue + } + if (Test-Path (Join-Path $env:PSNOTES_HOME 'remote')) { + Remove-Item (Join-Path $env:PSNOTES_HOME 'remote') -Recurse -Force -ErrorAction SilentlyContinue + } + + Initialize-PSNoteStore + } + + Context "Default behavior (register remote URL)" { + + It "registers the remote catalog and does not download immediately" { + # If RegisterRemoteCatalog only registers, there should be no web call here. + Mock Invoke-WebRequest { throw "Invoke-WebRequest should not be called for registration." } + + Import-RemoteCatalog -Name 'Tools' -Url 'https://example.invalid/Tools.json' + + InModuleScope PSNotes { + $script:_noteStore | Should -Not -BeNullOrEmpty + $script:_noteStore.Config | Should -Not -BeNullOrEmpty + @($script:_noteStore.Config.RemoteCatalogs).Count | Should -Be 1 + + $script:_noteStore.Config.RemoteCatalogs[0].Name | Should -Be 'Tools' + $script:_noteStore.Config.RemoteCatalogs[0].Url | Should -Be 'https://example.invalid/Tools.json' + } + + Assert-MockCalled Invoke-WebRequest -Times 0 -Exactly + } + } + + Context "One-time import (creates local copy, no registration)" { + + It "downloads once and creates a local catalog file at ResolvePath(Name)" { + Mock Invoke-WebRequest { + [pscustomobject]@{ + Content = $script:MockRemoteCatalogJson + Headers = @{} + } + } -ModuleName PSNotes + + Import-RemoteCatalog -Name 'OneTimeCatalog' -Url 'https://example.invalid/OneTime.json' -AsLocal + + # File should exist at ResolvePath(Name) + $localPath = [NoteCatalog]::ResolvePath('OneTimeCatalog') + Test-Path $localPath | Should -Be $true + + # Should not register in config + InModuleScope PSNotes { + @($script:_noteStore.Config.RemoteCatalogs).Count | Should -Be 0 + } + + # Notes should be available in the new catalog + $notes = Get-PSNote -Catalog 'OneTimeCatalog' + @($notes).Count | Should -BeGreaterThan 0 + $notes | ForEach-Object { $_.Catalog | Should -Be 'OneTimeCatalog' } + + Assert-MockCalled Invoke-WebRequest -Times 1 -Exactly -ModuleName PSNotes + } + + It "throws when local catalog already exists and -Force is not provided" { + Mock Invoke-WebRequest { + [pscustomobject]@{ + Content = $script:MockRemoteCatalogJson + Headers = @{} + } + } -ModuleName PSNotes + + # First import creates the file + Import-RemoteCatalog -Name 'DupCatalog' -Url 'https://example.invalid/Dup.json' -AsLocal + + # Second import without -Force should throw + { Import-RemoteCatalog -Name 'DupCatalog' -Url 'https://example.invalid/Dup.json' -AsLocal -ErrorAction Stop } | + Should -Throw + + # Still should not register + InModuleScope PSNotes { + @($script:_noteStore.Config.RemoteCatalogs).Count | Should -Be 0 + } + } + + It "overwrites when local catalog already exists and -Force is provided" { + # First payload + $payload1 = @( + [pscustomobject]@{ + Catalog = 'RemotePayloadCatalog' + Path = 'Ignored.json' + StoreVersion = 1 + Notes = @( + @{ + Note = 'payload-1' + Snippet = 'Write-Output 1' + Details = 'First' + Alias = 'p1' + Tags = @('T') + Catalog = 'RemotePayloadCatalog' + } + ) + } + ) | ConvertTo-Json -Depth 10 + + # Second payload + $payload2 = @( + [pscustomobject]@{ + Catalog = 'RemotePayloadCatalog' + Path = 'Ignored.json' + StoreVersion = 1 + Notes = @( + @{ + Note = 'payload-2' + Snippet = 'Write-Output 2' + Details = 'Second' + Alias = 'p2' + Tags = @('T') + Catalog = 'RemotePayloadCatalog' + } + ) + } + ) | ConvertTo-Json -Depth 10 + + $script:call = 0 + Mock Invoke-WebRequest { + $script:call++ + [pscustomobject]@{ + Content = if ($script:call -eq 1) { $payload1 } else { $payload2 } + Headers = @{} + } + } -ModuleName PSNotes + + Import-RemoteCatalog -Name 'ForceCatalog' -Url 'https://example.invalid/Force.json' -AsLocal + (Get-PSNote -Catalog 'ForceCatalog').Name | Should -Contain 'payload-1' + + Import-RemoteCatalog -Name 'ForceCatalog' -Url 'https://example.invalid/Force.json' -AsLocal -Force + $notes = Get-PSNote -Catalog 'ForceCatalog' + @($notes.Name) | Should -Contain 'payload-2' + + Assert-MockCalled Invoke-WebRequest -Times 2 -ModuleName PSNotes + } + } + + Context "Optional: Live URL integration" -Tag 'Integration' { + + It "can one-time import from a real URL (skips if unreachable)" { + $url = $script:RealUrls[0] + $name = 'LiveRemoteImport' + + try { + Import-RemoteCatalog -Name $name -Url $url -AsLocal -Force -ErrorAction Stop + } + catch { + Set-ItResult -Skipped -Because "Live URL unreachable or blocked: $($_.Exception.Message)" + return + } + + $localPath = [NoteCatalog]::ResolvePath($name) + Test-Path $localPath | Should -Be $true + + $notes = Get-PSNote -Catalog $name + @($notes).Count | Should -BeGreaterThan 0 + } + } +} diff --git a/tests/UnitTests/Mocks/TestPSNotesv0.json b/tests/UnitTests/Mocks/TestPSNotesv0.json new file mode 100644 index 0000000..9bfaac1 --- /dev/null +++ b/tests/UnitTests/Mocks/TestPSNotesv0.json @@ -0,0 +1,11 @@ +[ + { + "Note": "NewPSNote", + "Alias": "Example", + "Details": "Example of creating a new Note", + "Tags": [ + "notes" + ], + "Snippet": "$Snippet = @\u0027\r\n(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__)\r\n\u0027@\r\nNew-PSNote -Note \u0027DayOfWeek\u0027 -Snippet $Snippet -Details \"Use to name of the day of the week\" -Tags \u0027date\u0027 -Alias \u0027today\u0027" + } +] \ No newline at end of file diff --git a/tests/UnitTests/Mocks/TestPersonalStore.json b/tests/UnitTests/Mocks/TestPersonalStore.json new file mode 100644 index 0000000..e1e1b75 --- /dev/null +++ b/tests/UnitTests/Mocks/TestPersonalStore.json @@ -0,0 +1,30 @@ +{ + "StoreVersion": 1, + "Catalog": "Personal", + "Notes": [ + { + "Note": "az-login", + "Snippet": "Connect-AzAccount", + "Details": "Azure login", + "Alias": "azlogin", + "Tags": [ + "Azure", + "Auth" + ], + "Run": true, + "Kind": 0 + }, + { + "Note": "day-one", + "Snippet": "Write-Output day", + "Details": "Notes about day", + "Alias": "day", + "Tags": [ + "Journal", + "Auth" + ], + "Run": true, + "Kind": 0 + } + ] +} \ No newline at end of file diff --git a/tests/UnitTests/Mocks/TestScriptPath.ps1 b/tests/UnitTests/Mocks/TestScriptPath.ps1 new file mode 100644 index 0000000..0bc5b18 --- /dev/null +++ b/tests/UnitTests/Mocks/TestScriptPath.ps1 @@ -0,0 +1 @@ +'Hello Pester' diff --git a/tests/UnitTests/Mocks/TestScriptStore.json b/tests/UnitTests/Mocks/TestScriptStore.json new file mode 100644 index 0000000..8d20a69 --- /dev/null +++ b/tests/UnitTests/Mocks/TestScriptStore.json @@ -0,0 +1,15 @@ +{ + "StoreVersion": 1, + "Catalog": "Default", + "Notes": [ + { + "Note": "TestScriptPath", + "Snippet": "C:\\Users\\mdows\\AppData\\Local\\Temp\\PSNotesTests\\GetPSNoteAlias\\TestScriptPath.ps1", + "Details": "Test script path", + "Alias": "TestScriptPath", + "Tags": null, + "Run": true, + "Kind": 1 + } + ] +} \ No newline at end of file diff --git a/tests/UnitTests/Mocks/TestWorkStore.json b/tests/UnitTests/Mocks/TestWorkStore.json new file mode 100644 index 0000000..c8315ba --- /dev/null +++ b/tests/UnitTests/Mocks/TestWorkStore.json @@ -0,0 +1,30 @@ +{ + "StoreVersion": 1, + "Notes": [ + { + "Note": "creds", + "Snippet": "Get-Credential", + "Details": "Credential helpers", + "Alias": "creds", + "Tags": [ + "AD", + "Auth" + ], + "Catalog": "Work", + "Run": false, + "Kind": 0 + }, + { + "Note": "creds2", + "Snippet": "Write-Output creds2", + "Details": "More creds", + "Alias": "c2", + "Tags": [ + "Auth" + ], + "Catalog": "Work", + "Run": false, + "Kind": 0 + } + ] +} \ No newline at end of file diff --git a/tests/UnitTests/New-PSNote.Tests.ps1 b/tests/UnitTests/New-PSNote.Tests.ps1 new file mode 100644 index 0000000..7ae7528 --- /dev/null +++ b/tests/UnitTests/New-PSNote.Tests.ps1 @@ -0,0 +1,282 @@ +# Pester tests for New-PSNote +Get-Module PSNotes | Remove-Module -Force +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} +BeforeAll { + Set-StrictMode -Version Latest + + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\NewPSNote" + if (Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + # Set up test environment variable + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + $script:MockPath = Join-Path -Path $PSScriptRoot -ChildPath 'Mocks' + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName + Import-Module $psd1 -Force +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe "New-PSNote" { + BeforeEach { + # Define a fake note store for testing + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestPersonalStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Personal.json') -Force + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestWorkStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Work.json') -Force + + Initialize-PSNoteStore + } + Context "Creating new notes" { + + It "creates a new note with Snippet parameter" { + New-PSNote -Name 'TestSnippet' -Snippet 'Get-Process' -Details 'Test snippet' -Tags 'Test' + + $result = Get-PSNote -Name 'TestSnippet' + $result.Name | Should -Be 'TestSnippet' + $result.Snippet | Should -Be 'Get-Process' + $result.Details | Should -Be 'Test snippet' + $result.Tags | Should -Contain 'Test' + } + + It "creates a new note with ScriptBlock parameter" { + $scriptBlock = { Get-Service | Where-Object Status -eq 'Running' } + New-PSNote -Name 'TestScriptBlock' -ScriptBlock $scriptBlock -Details 'Test scriptblock' + + $result = Get-PSNote -Name 'TestScriptBlock' + $result.Name | Should -Be 'TestScriptBlock' + $result.Snippet | Should -Be $scriptBlock.ToString() + $result.Details | Should -Be 'Test scriptblock' + } + + It "creates a new note with ScriptPath parameter" { + $scriptFile = Join-Path $script:TestDir 'TestScriptPath.ps1' + Set-Content -Path $scriptFile -Value 'Get-Date' -Force + + New-PSNote -Name 'TestScriptPath' -ScriptPath $scriptFile -Details 'Test script path' + + $result = Get-PSNote -Name 'TestScriptPath' + $result.Name | Should -Be 'TestScriptPath' + $result.Snippet | Should -Be $scriptFile + $result.Kind | Should -Be 'Script' + $result.Details | Should -Be 'Test script path' + } + + It "creates a note with multiple tags" { + New-PSNote -Name 'TestMultiTags' -Snippet 'Get-ChildItem' -Tags 'Files', 'Test', 'PowerShell' + + $result = Get-PSNote -Name 'TestMultiTags' + $result.Tags | Should -Contain 'Files' + $result.Tags | Should -Contain 'Test' + $result.Tags | Should -Contain 'PowerShell' + } + + It "creates a note with custom Alias" { + New-PSNote -Name 'TestAlias' -Snippet 'Test-Connection' -Alias 'ping-test' + + $result = Get-PSNote -Name 'TestAlias' + $result.Alias | Should -Be 'ping-test' + } + + It "leave Alias blank when Alias is not specified" { + New-PSNote -Name 'TestDefaultAlias' -Snippet 'Get-Date' + + $result = Get-PSNote -Name 'TestDefaultAlias' + $result.Alias | Should -Be '' + } + + It "creates a note with multiline snippet using here-string" { + $multilineSnippet = @' +$stringBuilder = New-Object System.Text.StringBuilder +for ($i = 0; $i -lt 10; $i++){ + $stringBuilder.Append("Line $i`r`n") | Out-Null +} +$stringBuilder.ToString() +'@ + New-PSNote -Name 'TestMultiline' -Snippet $multilineSnippet -Details 'Multiline test' + + $result = Get-PSNote -Name 'TestMultiline' + $result.Snippet | Should -Be $multilineSnippet + } + } + + Context "Run and Alias properties" { + + It "creates a note without an Alias and without Run" { + New-PSNote -Name 'TestNoAliasNoRun' -Snippet 'Write-Output "Test snippet"' -Details 'Test snippet' -Tags 'Test' -Catalog 'TestCatalog' + + { TestNoAliasNoRun } | Should -Throw + + $result = Get-PSNote -Name 'TestNoAliasNoRun' + $result.Run | Should -Be $false + $result.Alias | Should -Be '' + + Get-PSNote -Name 'TestNoAliasNoRun' -Run | Should -Be "Test snippet" + } + + It "creates a note with an Alias and without Run" { + New-PSNote -Name 'TestAliasNoRun' -Snippet 'Write-Output "Test Alias and without Run"' -Details 'Test Alias and without Run' -Tags 'Test' -Catalog 'TestCatalog' -Alias 'testaliasnorun' + + testaliasnorun | Should -Be 'Write-Output "Test Alias and without Run"' + testaliasnorun -run | Should -Be 'Test Alias and without Run' + testaliasnorun -copy | Should -Be 'Write-Output "Test Alias and without Run"' + + $result = Get-PSNote -Name 'TestAliasNoRun' + $result.Run | Should -Be $false + $result.Alias | Should -Be 'testaliasnorun' + + Get-PSNote -Name 'TestAliasNoRun' -Run | Should -Be 'Test Alias and without Run' + } + + It "creates a note with an Alias and with Run" { + New-PSNote -Name 'TestRunAliasRun' -Snippet 'Write-Output "Test Alias and with Run"' -Details 'Test Alias and with Run' -Tags 'Test' -Catalog 'TestCatalog' -Alias 'testrunaliasrun' -Run $true + + testrunaliasrun | Should -Be 'Test Alias and with Run' + testrunaliasrun -run | Should -Be 'Test Alias and with Run' + testrunaliasrun -copy | Should -Be 'Write-Output "Test Alias and with Run"' + + $result = Get-PSNote -Name 'TestRunAliasRun' + $result.Run | Should -Be $true + $result.Alias | Should -Be 'testrunaliasrun' + + Get-PSNote -Name 'TestRunAliasRun' -Run | Should -Be 'Test Alias and with Run' + } + + It "creates a note without an Alias and with Run" { + New-PSNote -Name 'TestRunNoAliasRun' -Snippet 'Write-Output "Test no alias and with Run"' -Details 'Test no alias and with Run' -Tags 'Test' -Catalog 'TestCatalog' -Run $true + + { TestRunNoAliasRun } | Should -Throw + + Get-PSNote -Name 'TestRunNoAliasRun' + $result = Get-PSNote -Name 'TestRunNoAliasRun' + $result.Run | Should -Be $true + $result.Alias | Should -Be '' + + Get-PSNote -Name 'TestRunNoAliasRun' -Run | Should -Be 'Test no alias and with Run' + } + } + + Context "Updating existing notes" { + + BeforeEach { + # Create a note to update in each test + New-PSNote -Name 'UpdateTest' -Snippet 'Get-Process' -Details 'Original' -Tags 'Test' -Force + } + + It "throws an error when trying to overwrite without -Force" { + New-PSNote -Name 'UpdateTest' -Snippet 'Get-Service' -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -BeGreaterThan 0 + $err[0].Exception.Message | Should -Match "already exists" + } + + It "updates an existing note with -Force" { + New-PSNote -Name 'UpdateTest' -Snippet 'Get-Service' -Force + + $result = Get-PSNote -Name 'UpdateTest' + $result.Snippet | Should -Be 'Get-Service' + } + + It "updates only specified properties with -Force" { + New-PSNote -Name 'UpdateTest' -Details 'Updated details' -Force + + $result = Get-PSNote -Name 'UpdateTest' + $result.Details | Should -Be 'Updated details' + $result.Snippet | Should -Be 'Get-Process' # Original snippet should remain + } + + It "updates Tags with -Force" { + New-PSNote -Name 'UpdateTest' -Tags 'Updated', 'NewTag' -Force + + $result = Get-PSNote -Name 'UpdateTest' + $result.Tags | Should -Contain 'Updated' + $result.Tags | Should -Contain 'NewTag' + } + + It "updates Alias with -Force" { + New-PSNote -Name 'UpdateTest' -Alias 'new-alias' -Force + + $result = Get-PSNote -Name 'UpdateTest' + $result.Alias | Should -Be 'new-alias' + } + + It "updates existing note to ScriptPath with -Force" { + $scriptFile = Join-Path $script:TestDir 'UpdateTestScript.ps1' + Set-Content -Path $scriptFile -Value 'Get-Process' -Force + + New-PSNote -Name 'UpdateTest' -ScriptPath $scriptFile -Force + + $result = Get-PSNote -Name 'UpdateTest' + $result.Snippet | Should -Be $scriptFile + $result.Kind | Should -Be 'Script' + } + } + + Context "Alias validation" { + + It "accepts valid alias with letters, numbers, dashes, and underscores" { + { New-PSNote -Name 'ValidAlias1' -Snippet 'Test' -Alias 'valid-alias_123' } | Should -Not -Throw + } + + It "throws an error for alias with spaces" { + { New-PSNote -Name 'InvalidAlias1' -Snippet 'Test' -Alias 'invalid alias' } | Should -Throw + } + + It "throws an error for alias with special characters" { + { New-PSNote -Name 'InvalidAlias2' -Snippet 'Test' -Alias 'invalid@alias' } | Should -Throw + } + + It "throws an error for alias with dots" { + { New-PSNote -Name 'InvalidAlias3' -Snippet 'Test' -Alias 'invalid.alias' } | Should -Throw + } + } + + Context "Parameter sets" { + + It "accepts Snippet parameter" { + { New-PSNote -Name 'SnippetParam' -Snippet 'Get-Date' } | Should -Not -Throw + } + + It "accepts ScriptBlock parameter" { + { New-PSNote -Name 'ScriptBlockParam' -ScriptBlock { Get-Date } } | Should -Not -Throw + } + + It "accepts ScriptPath parameter" { + $scriptFile = Join-Path $script:TestDir 'ParamScriptPath.ps1' + Set-Content -Path $scriptFile -Value 'Get-ChildItem' -Force + { New-PSNote -Name 'ScriptPathParam' -ScriptPath $scriptFile } | Should -Not -Throw + } + + It "converts ScriptBlock to string for storage" { + $sb = { Get-Process | Select-Object -First 5 } + New-PSNote -Name 'ScriptBlockConversion' -ScriptBlock $sb + + $result = Get-PSNote -Name 'ScriptBlockConversion' + $result.Snippet | Should -Be $sb.ToString() + } + + It "throws when ScriptPath does not exist" { + $missingFile = Join-Path $script:TestDir 'MissingScript.ps1' + { New-PSNote -Name 'MissingScriptPath' -ScriptPath $missingFile } | Should -Throw + } + } + + Context "Global alias creation" { + + It "creates a global alias for the note" { + New-PSNote -Name 'AliasCreation' -Snippet 'Get-Date' -Alias 'test-global-alias' + + # Check if alias exists + $aliasExists = Test-Path Alias:\test-global-alias + $aliasExists | Should -Be $true + } + } +} diff --git a/tests/UnitTests/NoteStore.class.Tests.ps1 b/tests/UnitTests/NoteStore.class.Tests.ps1 new file mode 100644 index 0000000..8cb04ad --- /dev/null +++ b/tests/UnitTests/NoteStore.class.Tests.ps1 @@ -0,0 +1,500 @@ +#requires -Version 5.1 +# Pester 5.x tests for classes in NoteStore.class.ps1 (simple_console) +# Locate repo root (walk up until src/ exists) +Get-Module PSNotes | Remove-Module -Force +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} + +BeforeAll { + Set-StrictMode -Version Latest + + # ---- Test sandbox ---- + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + + $script:TestRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("PSNotesTests_{0}" -f ([guid]::NewGuid().ToString('N'))) + $null = New-Item -Path $script:TestRoot -ItemType Directory -Force + + $env:PSNOTES_HOME = $script:TestRoot + + # Ensure the alias target exists if aliasing is not mocked for some reason + function Get-PSNoteAlias { param() } + $script:ClassPath = Join-Path $Global:TopLevel 'src\Classes\NoteStore.class.ps1' + # Load classes + . $script:ClassPath +} + +AfterAll { + $env:PSNOTES_HOME = $script:OriginalPSNotesHome + if (Test-Path $script:TestRoot) { + Remove-Item -Path $script:TestRoot -Recurse -Force -ErrorAction SilentlyContinue + } +} + +Describe 'NoteStore.class.ps1 classes' { + BeforeEach { + # Clean PSNOTES_HOME between tests (but preserve root) + Get-ChildItem -Path $env:PSNOTES_HOME -Force -ErrorAction SilentlyContinue | ForEach-Object { + Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue + } | Out-Null + + # Recreate expected subdirs if tests want them + $null = New-Item -Path $env:PSNOTES_HOME -ItemType Directory -Force + } + + Describe 'PSNote' { + It '5-parameter constructor sets defaults (Catalog=Default, Kind=Snippet, Run=$false)' { + $n = [PSNote]::new('N1', 'Get-Date', 'd', 'a1', @('t1')) + + $n.Name | Should -Be 'N1' + $n.Snippet | Should -Be 'Get-Date' + $n.Details | Should -Be 'd' + $n.Alias | Should -Be 'a1' + $n.Tags | Should -Be @('t1') + $n.Catalog | Should -Be 'Default' + $n.Run | Should -BeFalse + $n.Kind | Should -Be ([PSNoteKind]::Snippet) + } + + It '8-parameter constructor supports Script kind' { + $n = [PSNote]::new('RunIt', [PSNoteKind]::Script, 'C:\x.ps1', 'd', 'run', @('s'), 'Cat1', $true) + + $n.Kind | Should -Be ([PSNoteKind]::Script) + $n.Snippet | Should -Be 'C:\x.ps1' + $n.Catalog | Should -Be 'Cat1' + $n.Run | Should -BeTrue + } + + It 'object constructor tolerates missing/invalid Kind and Run' { + $obj = [pscustomobject]@{ + Name = 'N' + Snippet = 'S' + Details = 'D' + Alias = 'A' + Tags = @() + Catalog = 'C' + Kind = 'NotARealKind' + Run = 'notabool' + } + + $n = [PSNote]::new($obj) + $n.Kind | Should -Be ([PSNoteKind]::Snippet) + $n.Run | Should -BeFalse + } + + It 'GetKey returns Catalog::Alias' { + $n = [PSNote]::new('N', 'S', 'D', 'A', @()) + $n.Catalog = 'CatX' + $n.GetKey() | Should -Be 'CatX::A' + } + + It 'GetDisplayText adds (Script) for script notes' { + $s = [PSNote]::new('N', [PSNoteKind]::Script, 'C:\x.ps1', 'D', 'A', @(), 'C', $false) + $s.GetDisplayText() | Should -Be 'A (Script)' + + $p = [PSNote]::new('N2', 'Get-Process', 'D', 'gp', @()) + $p.GetDisplayText() | Should -Be 'gp' + } + } + + Describe 'NoteCatalog - ResolvePath/Open/Versioning/Validation' { + It 'ResolvePath creates PSNOTES_HOME if needed and returns *.json' { + Remove-Item -Path $env:PSNOTES_HOME -Recurse -Force + $p = [NoteCatalog]::ResolvePath('MyCat', $env:PSNOTES_HOME) + + (Split-Path -Parent $p) | Should -Exist + $p | Should -Match 'MyCat\.json$' + } + + It 'Save writes current store version and ToJson excludes per-note Catalog property' { + $cat = [NoteCatalog]::new($true) + $cat.Path = [NoteCatalog]::ResolvePath('X') + $cat.Catalog = 'X' + $cat.Notes.Add([PSNote]::new('N1', 'S', 'D', 'A', @())) | Out-Null + $cat.Notes[0].Catalog = 'X' + $cat.Save() + + $json = Get-Content -Path $cat.Path -Raw -Encoding UTF8 + $data = $json | ConvertFrom-Json + + $data.StoreVersion | Should -Be ([NoteCatalog]::CurrentStoreVersion) + $data.Catalog | Should -Be 'X' + $data.Notes.Count | Should -Be 1 + + # Stored notes intentionally exclude Catalog + $data.Notes[0].PSObject.Properties.Name | Should -Not -Contain 'Catalog' + } + + It 'Open refuses to load when StoreVersion mismatches' { + $path = [NoteCatalog]::ResolvePath('BadVer') + $bad = [pscustomobject]@{ + StoreVersion = 999 + Catalog = 'BadVer' + Notes = @( + [pscustomobject]@{ Name = 'N'; Snippet = 'S'; Details = 'D'; Alias = 'A'; Tags = @() } + ) + } | ConvertTo-Json -Depth 10 + + $bad | Set-Content -Path $path -Encoding UTF8 + + Mock -CommandName Write-Warning + $c = [NoteCatalog]::new('BadVer') + + $c.Notes.Count | Should -Be 0 + Assert-MockCalled Write-Warning -Times 1 + } + + It 'VersionCheck returns true only for current store version' { + $path = [NoteCatalog]::ResolvePath('VC') + + $ok = [pscustomobject]@{ + StoreVersion = [NoteCatalog]::CurrentStoreVersion + Catalog = 'VC' + Notes = @( + [pscustomobject]@{ Name = 'N'; Snippet = 'S'; Details = 'D'; Alias = 'A'; Tags = @() } + ) + } | ConvertTo-Json -Depth 10 + $ok | Set-Content -Path $path -Encoding UTF8 + + [NoteCatalog]::VersionCheck($path) | Should -BeTrue + + $bad = $ok | ConvertFrom-Json + $bad.StoreVersion = 0 + ($bad | ConvertTo-Json -Depth 10) | Set-Content -Path $path -Encoding UTF8 + + [NoteCatalog]::VersionCheck($path) | Should -BeFalse + } + + It 'ValidateNotes flags legacy array format and missing Alias as warnings' { + $path = [NoteCatalog]::ResolvePath('Legacy') + + @( + [pscustomobject]@{ Note = 'N1'; Snippet = 'S1'; Details = 'D'; Tags = @('t') } # no Alias + ) | ConvertTo-Json -Depth 10 | Set-Content -Path $path -Encoding UTF8 + + $r = [NoteCatalog]::ValidateNotes($path) + $r.IsValid | Should -BeTrue + $r.StoreVersion | Should -Be 'Legacy' + ($r.Warnings -join "`n") | Should -Match 'Legacy note catalog format' + ($r.Warnings -join "`n") | Should -Match "Missing 'Alias'" + } + + It 'ValidateNotes fails for empty file' { + $path = [NoteCatalog]::ResolvePath('Empty') + '' | Set-Content -Path $path -Encoding UTF8 + + $r = [NoteCatalog]::ValidateNotes($path) + $r.IsValid | Should -BeFalse + ($r.Errors -join "`n") | Should -Match 'File is empty' + } + } + + Describe 'NoteCatalog - AtomicSaveWithBackup' { + It 'creates backup when overwriting and keeps only last 10 backups' { + $path = [NoteCatalog]::ResolvePath('B') + 'one' | Set-Content -Path $path -Encoding UTF8 + + # Write 12 times -> backups should be trimmed to 10 + 1..12 | ForEach-Object { + [NoteCatalog]::AtomicSaveWithBackup($path, "content $_") + } + + $backupDir = Join-Path $env:PSNOTES_HOME 'backups' + $backupDir | Should -Exist + + $fileName = [System.IO.Path]::GetFileName($path) + $bak = Get-ChildItem -Path $backupDir -Filter "$fileName.*.bak" -File -ErrorAction SilentlyContinue + $bak.Count | Should -BeLessOrEqual 10 + + (Get-Content -Path $path -Raw -Encoding UTF8) | Should -Match 'content 12' + } + } + + Describe 'NoteCatalog - Migrate' { + It 'migrates legacy array to current object format and creates backup artifacts' { + $legacyPath = [NoteCatalog]::ResolvePath('LegacyToMigrate') + @( + [pscustomobject]@{ Name = 'N1'; Snippet = 'S1'; Details = 'D1'; Alias = 'A1'; Tags = @('t1') }, + [pscustomobject]@{ Name = 'N2'; Snippet = 'S2'; Details = 'D2'; Alias = 'A2'; Tags = @() } + ) | ConvertTo-Json -Depth 10 | Set-Content -Path $legacyPath -Encoding UTF8 + + $m = [NoteCatalog]::Migrate($legacyPath, $true) + + $m.Notes.Count | Should -Be 2 + $m.Notes[0].Catalog | Should -Be 'LegacyToMigrate' + + $backupDir = Join-Path $env:PSNOTES_HOME 'backups' + $backupDir | Should -Exist + + # Should have created at least one pre-migration backup + $fileName = [System.IO.Path]::GetFileName($legacyPath) + (Get-ChildItem -Path $backupDir -Filter "$fileName.pre-migration.*.bak" -File -ErrorAction SilentlyContinue | Measure-Object).Count | + Should -BeGreaterThan 0 + } + } + + Describe 'NoteStore - local catalogs, add/remove/update, duplicate behavior' { + BeforeEach { + Mock -CommandName Set-Alias + Mock -CommandName Write-Warning + Mock -CommandName Write-Verbose + Mock -CommandName Write-Debug + } + + It 'constructor loads local catalogs from PSNOTES_HOME (current format)' { + $path = [NoteCatalog]::ResolvePath('Cat1') + $obj = [pscustomobject]@{ + StoreVersion = [NoteCatalog]::CurrentStoreVersion + Catalog = 'Cat1' + Notes = @( + [pscustomobject]@{ Name = 'N1'; Snippet = 'S1'; Details = 'D1'; Alias = 'a1'; Tags = @('t') } + ) + } | ConvertTo-Json -Depth 10 + $obj | Set-Content -Path $path -Encoding UTF8 + + $s = [NoteStore]::new() + + $s.Catalogs.Catalog | Should -Contain 'Cat1' + ($s.Notes | Where-Object Alias -eq 'a1' | Measure-Object).Count | Should -Be 1 + + Assert-MockCalled Set-Alias -Times 1 -Exactly + } + + It 'LoadCatalog skips duplicate alias when already present from different catalog' { + $s = [NoteStore]::new() + + $c1 = [NoteCatalog]::new($true); $c1.Catalog = 'C1'; $c1.Path = [NoteCatalog]::ResolvePath('C1') + $c2 = [NoteCatalog]::new($true); $c2.Catalog = 'C2'; $c2.Path = [NoteCatalog]::ResolvePath('C2') + + $n1 = [PSNote]::new('N1', 'S', 'D', 'dup', @()); $n1.Catalog = 'C1' + $n2 = [PSNote]::new('N2', 'S', 'D', 'dup', @()); $n2.Catalog = 'C2' + + $c1.Notes.Add($n1) | Out-Null + $c2.Notes.Add($n2) | Out-Null + + $s.LoadCatalog($c1) + $s.LoadCatalog($c2) + + ($s.Notes | Where-Object Alias -eq 'dup' | Measure-Object).Count | Should -Be 1 + Assert-MockCalled Write-Warning -Times 1 + } + + It 'AddNote blocks adding into remote catalog' { + $s = [NoteStore]::new() + + $remote = [NoteCatalog]::new($true) + $remote.Catalog = 'Remote1' + $remote.Path = 'x' + $remote.IsRemote = $true + $s.Catalogs.Add($remote) | Out-Null + + $n = [PSNote]::new('N', 'S', 'D', 'a', @()); $n.Catalog = 'Remote1' + $s.AddNote($n) + + ($s.Notes | Where-Object Name -eq 'N' | Measure-Object).Count | Should -Be 0 + Assert-MockCalled Write-Warning -Times 1 + } + + It 'RemoveNote blocks removing from remote catalog' { + $s = [NoteStore]::new() + + $remote = [NoteCatalog]::new($true) + $remote.Catalog = 'Remote1' + $remote.Path = 'x' + $remote.IsRemote = $true + + $n = [PSNote]::new('N', 'S', 'D', 'a', @()); $n.Catalog = 'Remote1' + $remote.Notes.Add($n) | Out-Null + + $s.Catalogs.Add($remote) | Out-Null + $s.Notes.Add($n) | Out-Null + + $s.RemoveNote('N', 'Remote1') + ($s.Notes | Where-Object Name -eq 'N' | Measure-Object).Count | Should -Be 1 + Assert-MockCalled Write-Warning -Times 1 + } + + It 'UpdateNote blocks updating remote notes (existing lives in remote)' { + $s = [NoteStore]::new() + + $remote = [NoteCatalog]::new($true) + $remote.Catalog = 'Remote1' + $remote.Path = 'x' + $remote.IsRemote = $true + + $existing = [PSNote]::new('N', 'S', 'D', 'a', @()); $existing.Catalog = 'Remote1' + $remote.Notes.Add($existing) | Out-Null + + $s.Catalogs.Add($remote) | Out-Null + $s.Notes.Add($existing) | Out-Null + + $updated = [PSNote]::new('N', 'S2', 'D2', 'a', @()); $updated.Catalog = 'Remote1' + $s.UpdateNote($updated) + + ($s.Notes | Where-Object Name -eq 'N' | Select-Object -First 1).Snippet | Should -Be 'S' + Assert-MockCalled Write-Warning -Times 1 + } + } + + Describe 'NoteStore - favorites + MoveNote' { + BeforeEach { + Mock -CommandName Set-Alias + Mock -CommandName Write-Warning + Mock -CommandName Write-Verbose + Mock -CommandName Write-Debug + } + + It 'AddFavorite/IsFavorite/GetFavorites/RemoveFavorite works' { + $s = [NoteStore]::new() + + $c = [NoteCatalog]::new($true); $c.Catalog = 'C1'; $c.Path = [NoteCatalog]::ResolvePath('C1') + $n = [PSNote]::new('N', 'S', 'D', 'a', @()); $n.Catalog = 'C1' + $c.Notes.Add($n) | Out-Null + $s.LoadCatalog($c) + + $s.IsFavorite($n) | Should -BeFalse + $s.AddFavorite($n) + $s.IsFavorite($n) | Should -BeTrue + + $f = $s.GetFavorites() + $f.Count | Should -Be 1 + $f[0].Name | Should -Be 'N' + + $s.RemoveFavorite($n) + $s.IsFavorite($n) | Should -BeFalse + } + + It 'MoveNote preserves favorite status and supports -Force overwrite' { + $s = [NoteStore]::new() + + $src = [NoteCatalog]::new($true); $src.Catalog = 'Src'; $src.Path = [NoteCatalog]::ResolvePath('Src') + $dst = [NoteCatalog]::new($true); $dst.Catalog = 'Dst'; $dst.Path = [NoteCatalog]::ResolvePath('Dst') + + $n = [PSNote]::new('N', 'S', 'D', 'a', @()); $n.Catalog = 'Src' + $src.Notes.Add($n) | Out-Null + + # Conflict in destination + $conflict = [PSNote]::new('Other', 'S', 'D', 'a', @()); $conflict.Catalog = 'Dst' + $dst.Notes.Add($conflict) | Out-Null + + $s.LoadCatalog($src) + $s.LoadCatalog($dst) + + $s.AddFavorite($n) + $s.IsFavorite($n) | Should -BeTrue + + { $s.MoveNote($n, 'Dst', $false) } | Should -Throw + + $s.MoveNote($n, 'Dst', $true) + + # After move: + $n.Catalog | Should -Be 'Dst' + $s.IsFavorite($n) | Should -BeTrue + + # Destination should now contain moved note (alias 'a') + ($s.Catalogs | Where-Object Catalog -eq 'Dst' | Select-Object -First 1).Notes | + Where-Object Alias -eq 'a' | + Select-Object -First 1 | + ForEach-Object { $_.Name } | Should -Be 'N' + } + } + + Describe 'NoteStore - remote catalogs (mocked web)' { + BeforeEach { + Mock -CommandName Set-Alias + Mock -CommandName Write-Warning + Mock -CommandName Write-Verbose + Mock -CommandName Write-Debug + } + + It 'RegisterRemoteCatalog stores config and creates stable cache file path' { + # mock web so LoadRemoteCatalogs/SyncRemoteCatalogs doesn’t do real network + Mock -CommandName Invoke-WebRequest -MockWith { + [pscustomobject]@{ + Content = ([pscustomobject]@{ + StoreVersion = [NoteCatalog]::CurrentStoreVersion + Catalog = 'RemoteIgnored' + Notes = @( + [pscustomobject]@{ Name = 'N'; Snippet = 'S'; Details = 'D'; Alias = 'a'; Tags = @() } + ) + } | ConvertTo-Json -Depth 10) + Headers = @{ ETag = '"x"'; 'Last-Modified' = 'Wed, 01 Jan 2025 00:00:00 GMT' } + } + } + + $s = [NoteStore]::new() + $entry = $s.RegisterRemoteCatalog('MyRemote', 'https://example.invalid/catalog.json') + + $entry.Name | Should -Be 'MyRemote' + $entry.Url | Should -Be 'https://example.invalid/catalog.json' + $entry.CacheFile | Should -Match '^remote[\\/].+\.json$' + + # Config file should exist + (Join-Path $env:PSNOTES_HOME 'config\psnoteconfigstore.json') | Should -Exist + } + + It 'LoadRemoteCatalogs loads cache as IsRemote and overrides catalog name with config Name' { + Mock -CommandName Invoke-WebRequest -MockWith { + [pscustomobject]@{ + Content = ([pscustomobject]@{ + StoreVersion = [NoteCatalog]::CurrentStoreVersion + Catalog = 'RemoteCatalogName' + Notes = @( + [pscustomobject]@{ Name = 'N1'; Snippet = 'S1'; Details = 'D1'; Alias = 'r1'; Tags = @() } + ) + } | ConvertTo-Json -Depth 10) + Headers = @{ ETag = '"x"' } + } + } + + $s = [NoteStore]::new() + $null = $s.RegisterRemoteCatalog('FriendlyRemote', 'https://example.invalid/r.json') + + # After register it calls LoadRemoteCatalogs; verify remote loaded + $rc = $s.Catalogs | Where-Object Catalog -eq 'FriendlyRemote' | Select-Object -First 1 + $rc | Should -Not -BeNullOrEmpty + $rc.IsRemote | Should -BeTrue + + ($s.Notes | Where-Object Catalog -eq 'FriendlyRemote' | Select-Object -First 1).Alias | Should -Be 'r1' + } + + It 'RemoveRemoteCatalog throws when none registered' { + $s = [NoteStore]::new() + { $s.RemoveRemoteCatalog('https://nope', $false, $false) } | Should -Throw + } + + It 'RemoveRemoteCatalog -ConvertToLocal writes local catalog and unregisters remote' { + # Prepare remote registration + cached file + Mock -CommandName Invoke-WebRequest -MockWith { + [pscustomobject]@{ + Content = ([pscustomobject]@{ + StoreVersion = [NoteCatalog]::CurrentStoreVersion + Catalog = 'RemoteCatalogName' + Notes = @( + [pscustomobject]@{ Name = 'N1'; Snippet = 'S1'; Details = 'D1'; Alias = 'r1'; Tags = @() } + ) + } | ConvertTo-Json -Depth 10) + Headers = @{ } + } + } + + $s = [NoteStore]::new() + $entry = $s.RegisterRemoteCatalog('FriendlyRemote', 'https://example.invalid/r.json') + + # Force a cache presence (register already syncs; but be explicit) + $cachePath = Join-Path $env:PSNOTES_HOME $entry.CacheFile + $cachePath | Should -Exist + + # Convert to local -> should create FriendlyRemote.json locally + $removed = $s.RemoveRemoteCatalog($entry.Url, $true, $true) + $removed.Url | Should -Be $entry.Url + + $localPath = [NoteCatalog]::ResolvePath('FriendlyRemote') + $localPath | Should -Exist + + # Should no longer be registered + ($s.Config.RemoteCatalogs | Where-Object Url -eq $entry.Url | Measure-Object).Count | Should -Be 0 + } + } +} \ No newline at end of file diff --git a/tests/UnitTests/Remove-PSNote.Tests.ps1 b/tests/UnitTests/Remove-PSNote.Tests.ps1 new file mode 100644 index 0000000..8acf395 --- /dev/null +++ b/tests/UnitTests/Remove-PSNote.Tests.ps1 @@ -0,0 +1,101 @@ +# Pester tests for Remove-PSNote +Get-Module PSNotes | Remove-Module -Force +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} +BeforeAll { + Set-StrictMode -Version Latest + + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\RemovePSNote" + if(Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + # Set up test environment variable + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + $script:MockPath = Join-Path -Path $PSScriptRoot -ChildPath 'Mocks' + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName + Import-Module $psd1 -Force +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe "Remove-PSNote" { + BeforeEach { + # Define a fake note store for testing + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestPersonalStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Personal.json') -Force + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestWorkStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Work.json') -Force + + Initialize-PSNoteStore + } + + Context "Pipeline (ByObject parameter set)" { + + It "removes each piped note (calls NoteStore.RemoveNote)" { + $toRemove = @( + Get-PSnote -Name 'creds' -Catalog 'Work' + Get-PSnote -Name 'az-login' -Catalog 'Personal' + ) + + $r = $toRemove | Remove-PSNote -Confirm:$false + + @($r).Count | Should -Be 2 + Get-PSnote -Name 'creds' -Catalog 'Work' | Should -Be $null + Get-PSnote -Name 'az-login' -Catalog 'Personal' | Should -Be $null + } + + It "honors -WhatIf (does not call RemoveNote)" { + $toRemove = @( + Get-PSnote -Name 'creds2' -Catalog 'Work' + ) + + $null = $toRemove | Remove-PSNote -WhatIf + + Get-PSnote -Name 'creds2' -Catalog 'Work' | Should -Not -Be $null + } + + It "de-dupes piped notes by Catalog+Note" { + $toRemove = @( + Get-PSnote -Name 'creds2' -Catalog 'Work' + Get-PSnote -Name 'creds2' -Catalog 'Work' + ) + + $r = $toRemove | Remove-PSNote -Confirm:$false + + @($r).Count | Should -Be 1 + } + } + + Context "Discovery parameter sets (delegates to Get-PSNote)" { + + It "calls Get-PSNote with Name/Tag/Catalog when using Note parameter set" { + $r = Remove-PSNote -Name 'cred*' -Tag 'AD' -Catalog 'Work' -Confirm:$false + + @($r).Count | Should -Be 1 + Get-PSnote -Name 'creds' -Catalog 'Work' | Should -Be $null + } + + It "calls Get-PSNote with SearchString and Catalog when using Search parameter set" { + $r = Remove-PSNote -SearchString 'Azure' -Catalog 'Personal' -Confirm:$false + + @($r).Count | Should -Be 1 + Get-PSnote -Name 'az-login' -Catalog 'Personal' | Should -Be $null + } + + It "returns no output and does not call RemoveNote when Get-PSNote finds nothing" { + Mock -CommandName Get-PSNote -MockWith { @() } -ModuleName PSNotes + + $r = Remove-PSNote -Name 'nope*' -Catalog 'Work' -Confirm:$false + + @($r).Count | Should -Be 0 + } + } +} diff --git a/tests/UnitTests/Remove-RemoteCatalog.Tests.ps1 b/tests/UnitTests/Remove-RemoteCatalog.Tests.ps1 new file mode 100644 index 0000000..c7afde0 --- /dev/null +++ b/tests/UnitTests/Remove-RemoteCatalog.Tests.ps1 @@ -0,0 +1,252 @@ +Get-Module PSNotes | Remove-Module -Force -ErrorAction SilentlyContinue + +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} + +BeforeAll { + Set-StrictMode -Version Latest + + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\RemoveRemoteCatalog" + if (Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | + Select-Object -Last 1 -ExpandProperty FullName + + Import-Module $psd1 -Force + + function New-MockCatalogJson { + param( + [Parameter(Mandatory)][string] $CatalogName + ) + + @( + [pscustomobject]@{ + Catalog = $CatalogName + Path = 'Ignored.json' + StoreVersion = 1 + Notes = @( + @{ + Note = 'remote-note-1' + Snippet = 'Get-Date' + Details = 'Remote note 1' + Alias = 'r1' + Tags = @('Remote', 'Test') + Catalog = $CatalogName + Run = $false + Kind = 'Snippet' + } + ) + } + ) | ConvertTo-Json -Depth 10 + } + + function Set-CacheForRemoteEntry { + param( + [Parameter(Mandatory)][RemoteCatalogSource] $Entry, + [Parameter(Mandatory)][string] $CatalogName + ) + + # Some implementations store CacheFile relative to PSNOTES_HOME; others may store absolute. + $cacheRelOrAbs = [string]$Entry.CacheFile + $cacheRelOrAbs | Should -Not -BeNullOrEmpty + + $cachePath = if ([System.IO.Path]::IsPathRooted($cacheRelOrAbs)) { + $cacheRelOrAbs + } + else { + Join-Path $env:PSNOTES_HOME $cacheRelOrAbs + } + + $cacheDir = Split-Path $cachePath -Parent + if (-not (Test-Path $cacheDir)) { $null = New-Item -Path $cacheDir -ItemType Directory -Force } + + (New-MockCatalogJson -CatalogName $CatalogName) | Set-Content -Path $cachePath -Encoding UTF8 + } +} + +AfterAll { + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe "Remote Catalog Commands" { + + BeforeEach { + # Clean store between tests + Get-ChildItem -Path $env:PSNOTES_HOME -Recurse -ErrorAction SilentlyContinue | + Remove-Item -Force -Recurse -ErrorAction SilentlyContinue + + Initialize-PSNoteStore + } + + Context "Remove-RemoteCatalog (unregister)" { + + It "removes a registered remote by pipeline object" { + $url = 'https://example.invalid/tools.json' + Import-RemoteCatalog -Name 'TestTools' -Url $url + + $remotes = @(Get-RemoteCatalog) + $remotes.Count | Should -Be 1 + + $target = $remotes | Where-Object Url -eq $url | Select-Object -First 1 + $target | Should -Not -BeNullOrEmpty + + $target | Remove-RemoteCatalog -Confirm:$false + + @(Get-RemoteCatalog).Count | Should -Be 0 + } + + It "throws when attempting to remove an unregistered remote object" { + InModuleScope PSNotes { + $fake = [RemoteCatalogSource]::new() + $fake.Name = 'Fake' + $fake.Url = 'https://example.invalid/not-registered.json' + $fake.CacheFile = 'remote\does-not-matter.json' + { $fake | Remove-RemoteCatalog -Confirm:$false -ErrorAction Stop } | Should -Throw + } + } + } + + Context "Remove-RemoteCatalog -ConvertToLocal" { + + It "throws if no cached copy exists for the registered remote" { + $url = 'https://example.invalid/tools.json' + Import-RemoteCatalog -Name 'TestTools' -Url $url + + $entry = (Get-RemoteCatalog | Where-Object Url -eq $url | Select-Object -First 1) + $entry | Should -Not -BeNullOrEmpty + + { $entry | Remove-RemoteCatalog -ConvertToLocal -Confirm:$false -ErrorAction Stop } | Should -Throw + } + + It "converts cached remote catalog to a local catalog file and unregisters the remote" { + InModuleScope PSNotes { + $url = 'https://example.invalid/tools.json' + $name = 'TestTools' + + Import-RemoteCatalog -Name $name -Url $url + + $entry = (Get-RemoteCatalog | Where-Object Url -eq $url | Select-Object -First 1) + $entry | Should -Not -BeNullOrEmpty + + # Create cached catalog JSON at the configured cache path + $MockCatalogJson = @( + [pscustomobject]@{ + Catalog = $name + Path = 'Ignored.json' + StoreVersion = 1 + Notes = @( + @{ + Note = 'remote-note-1' + Snippet = 'Get-Date' + Details = 'Remote note 1' + Alias = 'r1' + Tags = @('Remote', 'Test') + Catalog = $name + Run = $false + Kind = 'Snippet' + } + ) + } + ) | ConvertTo-Json -Depth 10 + # Some implementations store CacheFile relative to PSNOTES_HOME; others may store absolute. + $cacheRelOrAbs = [string]$Entry.CacheFile + $cacheRelOrAbs | Should -Not -BeNullOrEmpty + + $cachePath = if ([System.IO.Path]::IsPathRooted($cacheRelOrAbs)) { + $cacheRelOrAbs + } + else { + Join-Path $env:PSNOTES_HOME $cacheRelOrAbs + } + + $cacheDir = Split-Path $cachePath -Parent + if (-not (Test-Path $cacheDir)) { $null = New-Item -Path $cacheDir -ItemType Directory -Force } + + $MockCatalogJson | Set-Content -Path $cachePath -Encoding UTF8 + + $entry | Remove-RemoteCatalog -ConvertToLocal -Confirm:$false + + # Remote unregistered + @(Get-RemoteCatalog).Count | Should -Be 0 + + # Local file created + $localPath = [NoteCatalog]::ResolvePath($name) + Test-Path $localPath | Should -Be $true + + # Notes load under that local catalog + $notes = @(Get-PSNote -Catalog $name) + $notes.Count | Should -BeGreaterThan 0 + $notes[0].Catalog | Should -Be $name + } + } + + It "does not overwrite an existing local catalog unless -Force is provided" { + InModuleScope PSNotes { + $url = 'https://example.invalid/tools.json' + $name = 'TestTools' + + Import-RemoteCatalog -Name $name -Url $url + + $entry = (Get-RemoteCatalog | Where-Object Url -eq $url | Select-Object -First 1) + $entry | Should -Not -BeNullOrEmpty + + # Pre-create local catalog + + $localPath = [NoteCatalog]::ResolvePath($name) + $MockCatalogJson = @( + [pscustomobject]@{ + Catalog = $name + Path = 'Ignored.json' + StoreVersion = 1 + Notes = @( + @{ + Note = 'remote-note-1' + Snippet = 'Get-Date' + Details = 'Remote note 1' + Alias = 'r1' + Tags = @('Remote', 'Test') + Catalog = $name + Run = $false + Kind = 'Snippet' + } + ) + } + ) | ConvertTo-Json -Depth 10 + $MockCatalogJson | Set-Content -Path $localPath -Encoding UTF8 + + # Also create cached remote copy (required for convert) + # Some implementations store CacheFile relative to PSNOTES_HOME; others may store absolute. + $cacheRelOrAbs = [string]$Entry.CacheFile + $cacheRelOrAbs | Should -Not -BeNullOrEmpty + + $cachePath = if ([System.IO.Path]::IsPathRooted($cacheRelOrAbs)) { + $cacheRelOrAbs + } + else { + Join-Path $env:PSNOTES_HOME $cacheRelOrAbs + } + + $cacheDir = Split-Path $cachePath -Parent + if (-not (Test-Path $cacheDir)) { $null = New-Item -Path $cacheDir -ItemType Directory -Force } + + $MockCatalogJson | Set-Content -Path $cachePath -Encoding UTF8 + + { $entry | Remove-RemoteCatalog -ConvertToLocal -Confirm:$false -ErrorAction Stop } | Should -Throw + + # With -Force it should succeed + $entry | Remove-RemoteCatalog -ConvertToLocal -Force -Confirm:$false + + @(Get-RemoteCatalog).Count | Should -Be 0 + Test-Path $localPath | Should -Be $true + } + } + } +} diff --git a/tests/UnitTests/Set-PSNote.Tests.ps1 b/tests/UnitTests/Set-PSNote.Tests.ps1 new file mode 100644 index 0000000..5ad3de6 --- /dev/null +++ b/tests/UnitTests/Set-PSNote.Tests.ps1 @@ -0,0 +1,272 @@ +# Pester tests for Set-PSNote +Get-Module PSNotes | Remove-Module -Force +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} +BeforeAll { + Set-StrictMode -Version Latest + + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\SetPSNote" + if(Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + # Set up test environment variable + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + $script:MockPath = Join-Path -Path $PSScriptRoot -ChildPath 'Mocks' + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName + Import-Module $psd1 -Force +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe "Set-PSNote" { + BeforeEach { + # Define a fake note store for testing + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestPersonalStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Personal.json') -Force + Copy-Item -Path (Join-Path -Path $script:MockPath -ChildPath 'TestWorkStore.json') -Destination (Join-Path -Path $env:PSNOTES_HOME -ChildPath 'Work.json') -Force + + Initialize-PSNoteStore + } + + Context "Updating existing notes" { + + BeforeEach { + # Create a note to update + New-PSNote -Name 'SetTestNote' -Snippet 'Get-Process' -Details 'Original details' -Tags 'Test' -Catalog 'Personal' + } + + It "updates Snippet of an existing note" { + Set-PSNote -Name 'SetTestNote' -Snippet 'Get-Service' -Catalog 'Personal' + + $result = Get-PSNote -Name 'SetTestNote' + $result.Snippet | Should -Be 'Get-Service' + } + + It "updates Details of an existing note" { + Set-PSNote -Name 'SetTestNote' -Details 'Updated details' -Catalog 'Personal' + + $result = Get-PSNote -Name 'SetTestNote' + $result.Details | Should -Be 'Updated details' + $result.Snippet | Should -Be 'Get-Process' # Original snippet should remain + } + + It "updates Tags of an existing note" { + Set-PSNote -Name 'SetTestNote' -Tags 'Updated', 'NewTag' -Catalog 'Personal' + + $result = Get-PSNote -Name 'SetTestNote' + $result.Tags | Should -Contain 'Updated' + $result.Tags | Should -Contain 'NewTag' + } + + It "updates Alias of an existing note" { + Set-PSNote -Name 'SetTestNote' -Alias 'new-alias' -Catalog 'Personal' + + $result = Get-PSNote -Name 'SetTestNote' + $result.Alias | Should -Be 'new-alias' + } + + It "updates with ScriptBlock parameter" { + $scriptBlock = { Get-ChildItem | Measure-Object } + Set-PSNote -Name 'SetTestNote' -ScriptBlock $scriptBlock -Catalog 'Personal' + + $result = Get-PSNote -Name 'SetTestNote' + $result.Snippet | Should -Be $scriptBlock.ToString() + } + + It "updates with ScriptPath parameter" { + $scriptFile = Join-Path $script:TestDir 'SetTestScriptPath.ps1' + Set-Content -Path $scriptFile -Value 'Get-Date' -Force + + Set-PSNote -Name 'SetTestNote' -ScriptPath $scriptFile -Catalog 'Personal' + + $result = Get-PSNote -Name 'SetTestNote' + $result.Snippet | Should -Be $scriptFile + $result.Kind | Should -Be 'Script' + } + + It "updates multiple properties at once" { + Set-PSNote -Name 'SetTestNote' -Snippet 'Get-Date' -Details 'New details' -Tags 'Updated','Time' -Alias 'date-alias' -Catalog 'Personal' + + $result = Get-PSNote -Name 'SetTestNote' + $result.Snippet | Should -Be 'Get-Date' + $result.Details | Should -Be 'New details' + $result.Tags | Should -Contain 'Updated' + $result.Tags | Should -Contain 'Time' + $result.Alias | Should -Be 'date-alias' + } + } + + Context "Creating notes when they don't exist" { + + It "creates a new note with warning when note doesn't exist" { + Set-PSNote -Name 'NonExistentNote' -Snippet 'Get-Date' -Details 'New note' -Catalog 'Personal' -WarningVariable warn -WarningAction SilentlyContinue + + $result = Get-PSNote -Name 'NonExistentNote' + $result.Name | Should -Be 'NonExistentNote' + $result.Snippet | Should -Be 'Get-Date' + $warn.Count | Should -BeGreaterThan 0 + } + + It "creates a new note with blank alias when creating non-existent note" { + Set-PSNote -Name 'CreatedNoteBlank' -Snippet 'Test' -Catalog 'Personal' + + $result = Get-PSNote -Name 'CreatedNoteBlank' + $result.Alias | Should -Be '' + } + + It "creates a new note with alias when creating non-existent note" { + Set-PSNote -Name 'CreatedNoteAlias' -Snippet 'Test' -Catalog 'Personal' -Alias 'CreatedNoteAlias' + + $result = Get-PSNote -Name 'CreatedNoteAlias' + $result.Alias | Should -Be 'CreatedNoteAlias' + } + + It "creates a new note with custom alias when creating non-existent note" { + Set-PSNote -Name 'CreatedWithAlias' -Snippet 'Test' -Alias 'custom-alias' -Catalog 'Personal' + + $result = Get-PSNote -Name 'CreatedWithAlias' + $result.Alias | Should -Be 'custom-alias' + } + + It "creates a new note from ScriptPath when note doesn't exist" { + $scriptFile = Join-Path $script:TestDir 'CreateScriptPath.ps1' + Set-Content -Path $scriptFile -Value 'Get-Process' -Force + + Set-PSNote -Name 'CreatedFromScriptPath' -ScriptPath $scriptFile -Catalog 'Personal' + + $result = Get-PSNote -Name 'CreatedFromScriptPath' + $result.Snippet | Should -Be $scriptFile + $result.Kind | Should -Be 'Script' + } + } + + Context "Pipeline input" { + It "accepts Note from pipeline by property name" { + New-PSNote -Name 'PipelineTestNote' -Snippet 'Get-Process' -Catalog 'Personal' -Force + $noteObject = [PSCustomObject]@{ + Name = 'PipelineTestNote' + Snippet = 'Get-Service' + Catalog = 'Personal' + } + + $noteObject | Set-PSNote + + $result = Get-PSNote -Name 'PipelineTestNote' + $result.Snippet | Should -Be 'Get-Service' + } + + It "accepts Catalog from pipeline by property name" { + New-PSNote -Name 'PipelineTestNote' -Snippet 'Get-Process' -Catalog 'Personal' -Force + $noteObject = [PSCustomObject]@{ + Name = 'PipelineTestNote' + Snippet = 'Get-Content' + Catalog = 'Personal' + } + + $noteObject | Set-PSNote + + $result = Get-PSNote -Name 'PipelineTestNote' + $result.Snippet | Should -Be 'Get-Content' + } + + It "accepts ScriptPath from pipeline by property name" { + New-PSNote -Name 'PipelineScriptPathNote' -Snippet 'Get-Date' -Catalog 'Personal' -Force + $scriptFile = Join-Path $script:TestDir 'PipelineScriptPath.ps1' + Set-Content -Path $scriptFile -Value 'Get-ChildItem' -Force + + $noteObject = [PSCustomObject]@{ + Name = 'PipelineScriptPathNote' + ScriptPath = $scriptFile + Catalog = 'Personal' + } + + $noteObject | Set-PSNote + + $result = Get-PSNote -Name 'PipelineScriptPathNote' + $result.Snippet | Should -Be $scriptFile + $result.Kind | Should -Be 'Script' + } + } + + Context "Alias validation" { + + It "accepts valid alias with letters, numbers, dashes, and underscores" { + { Set-PSNote -Name 'ValidAliasSet' -Alias 'valid-alias_123' -Snippet 'Test' -Catalog 'Personal' } | Should -Not -Throw + } + + It "throws an error for alias with spaces" { + { Set-PSNote -Name 'InvalidAliasSet1' -Alias 'invalid alias' -Snippet 'Test' -Catalog 'Personal' } | Should -Throw + } + + It "throws an error for alias with special characters" { + { Set-PSNote -Name 'InvalidAliasSet2' -Alias 'invalid@alias' -Snippet 'Test' -Catalog 'Personal' } | Should -Throw + } + } + + Context "Parameter sets" { + + It "accepts Snippet parameter" { + { Set-PSNote -Name 'SnippetParamSet' -Snippet 'Get-Date' -Catalog 'Personal' } | Should -Not -Throw + } + + It "accepts ScriptBlock parameter" { + { Set-PSNote -Name 'ScriptBlockParamSet' -ScriptBlock { Get-Date } -Catalog 'Personal' } | Should -Not -Throw + } + + It "accepts ScriptPath parameter" { + $scriptFile = Join-Path $script:TestDir 'ScriptPathParamSet.ps1' + Set-Content -Path $scriptFile -Value 'Get-ChildItem' -Force + { Set-PSNote -Name 'ScriptPathParamSet' -ScriptPath $scriptFile -Catalog 'Personal' } | Should -Not -Throw + } + + It "converts ScriptBlock to string for storage" { + $sb = { Get-Process | Select-Object -First 5 } + Set-PSNote -Name 'ScriptBlockConversionSet' -ScriptBlock $sb -Catalog 'Personal' + + $result = Get-PSNote -Name 'ScriptBlockConversionSet' + $result.Snippet | Should -Be $sb.ToString() + } + + It "throws when ScriptPath does not exist" { + $missingFile = Join-Path $script:TestDir 'MissingScriptPath.ps1' + { Set-PSNote -Name 'MissingScriptPath' -ScriptPath $missingFile -Catalog 'Personal' } | Should -Throw + } + } + + Context "Multiline snippets" { + + It "updates a note with multiline snippet using here-string" { + $multilineSnippet = @' +$array = @() +for ($i = 0; $i -lt 10; $i++){ + $array += $i +} +$array +'@ + Set-PSNote -Name 'MultilineSetTest' -Snippet $multilineSnippet -Catalog 'Personal' + + $result = Get-PSNote -Name 'MultilineSetTest' + $result.Snippet | Should -Be $multilineSnippet + } + } + + Context "Global alias creation" { + + It "creates or updates a global alias for the note" { + Set-PSNote -Name 'AliasSetCreation' -Snippet 'Get-Date' -Alias 'test-set-alias' -Catalog 'Personal' + + # Check if alias exists + $aliasExists = Test-Path Alias:\test-set-alias + $aliasExists | Should -Be $true + } + } +} diff --git a/tests/UnitTests/Update-PSNoteStore.Tests.ps1 b/tests/UnitTests/Update-PSNoteStore.Tests.ps1 new file mode 100644 index 0000000..002cc8f --- /dev/null +++ b/tests/UnitTests/Update-PSNoteStore.Tests.ps1 @@ -0,0 +1,108 @@ +# Pester tests for Update-PSNoteStore duplicate handling +Get-Module PSNotes | Remove-Module -Force +$Global:TopLevel = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Global:TopLevel 'src'))) { + $Global:TopLevel = Split-Path $Global:TopLevel -Parent +} + +BeforeAll { + Set-StrictMode -Version Latest + + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\UpdatePSNoteStore" + if(Test-Path $script:TestDir) { + Remove-Item -Path $script:TestDir -Recurse -Force + } + $null = New-Item -Path $script:TestDir -ItemType Directory -Force + + # Set up test environment variable + $script:OriginalPSNotesHome = $env:PSNOTES_HOME + $env:PSNOTES_HOME = $script:TestDir + + $psd1 = Get-ChildItem -Path (Join-Path $Global:TopLevel 'bin') -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName + Import-Module $psd1 -Force +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe 'Update-PSNoteStore duplicate handling' { + BeforeEach { + if (Test-Path $env:PSNOTES_HOME) { + Remove-Item -Path $env:PSNOTES_HOME -Recurse -Force + } + $null = New-Item -Path $env:PSNOTES_HOME -ItemType Directory -Force + + # Existing catalog with duplicate alias + $existing = [pscustomobject]@{ + StoreVersion = 1 + Catalog = 'ExistingDup' + Notes = @( + [pscustomobject]@{ + Name = 'MigrateDup01' + Snippet = 'blah' + Details = '' + Alias = 'dup1' + Tags = $null + Run = $false + Kind = 0 + }, + [pscustomobject]@{ + Name = 'Unique01' + Snippet = 'blah' + Details = '' + Alias = 'notadupadup' + Tags = $null + Run = $false + Kind = 0 + } + ) + } + $existing | ConvertTo-Json -Depth 6 | Out-File (Join-Path $env:PSNOTES_HOME 'ExistingDup.json') -Encoding utf8 + + # Old format catalog (array) with duplicate and unique aliases + $legacy = @( + [pscustomobject]@{ + Note = 'MigrateDup01' + Alias = 'dup1' + Details = 'dup' + Tags = @() + Snippet = 'test' + }, + [pscustomobject]@{ + Note = 'MigrateDup02' + Alias = 'dup2' + Details = 'dup' + Tags = @() + Snippet = 'test' + } + ) + $legacy | ConvertTo-Json -Depth 6 | Out-File (Join-Path $env:PSNOTES_HOME 'PSNotes.json') -Encoding utf8 + + Initialize-PSNoteStore + } + + It 'SkipMigratedNotes keeps existing duplicate and removes migrated duplicate' { + Update-PSNoteStore -DefaultBehavior SkipMigratedNotes | Out-Null + + $migrated = Get-Content (Join-Path $env:PSNOTES_HOME 'PSNotes.json') -Raw | ConvertFrom-Json + $migrated.Notes.Alias | Should -Contain 'dup2' + $migrated.Notes.Alias | Should -Not -Contain 'dup1' + + $existingAfter = Get-Content (Join-Path $env:PSNOTES_HOME 'ExistingDup.json') -Raw | ConvertFrom-Json + $existingAfter.Notes.Alias | Should -Contain 'dup1' + } + + It 'OverwriteExistingNotes keeps migrated duplicate and removes existing duplicate' { + Update-PSNoteStore -DefaultBehavior OverwriteExistingNotes | Out-Null + + $migrated = Get-Content (Join-Path $env:PSNOTES_HOME 'PSNotes.json') -Raw | ConvertFrom-Json + $migrated.Notes.Alias | Should -Contain 'dup1' + $migrated.Notes.Alias | Should -Contain 'dup2' + + $existingAfter = Get-Content (Join-Path $env:PSNOTES_HOME 'ExistingDup.json') -Raw | ConvertFrom-Json + $existingAfter.Notes.Alias | Should -Not -Contain 'dup1' + } +} diff --git a/tools/PSNotes.ezformat.ps1 b/tools/PSNotes.ezformat.ps1 new file mode 100644 index 0000000..fc774f2 --- /dev/null +++ b/tools/PSNotes.ezformat.ps1 @@ -0,0 +1,48 @@ +#requires -Module EZOut +param( + $formatPath = (Join-Path $PSScriptRoot 'src/PSNotes.format.ps1xml') +) +# Regenerates PSNotes.format.ps1xml using EZOut. + +$views = @( + Write-FormatView -TypeName 'PSNote' -Name 'PSNote' -Action { + "$('-' * 40)`n`n" + + "Note : $($_.Note)`n" + + "Catalog : $($_.Catalog)`n" + + "Details : $($_.Details)`n" + + "Alias : $($_.Alias)`n" + + "Snippet :`n`n" + + "$($_.Snippet)`n`n" + } + + Write-FormatView -TypeName 'SplatBlock' -Name 'SplatBlock' -Action { + if ($_.SetBlock) { + "ParameterSet : $($_.ParameterSet)`n" + + "IsDefault : $($_.IsDefault)`n" + + "SetBlock :" + + "$($_.SetBlock.Split("`n") | ForEach-Object { "`n $($_.Trim())" })" + + "`nHashBlock :" + + "$($_.HashBlock.Split("`n") | ForEach-Object { "`n $($_)" })" + } + elseif ($_.ParameterSet) { + "ParameterSet : $($_.ParameterSet)`n" + + "IsDefault : $($_.IsDefault)`n" + + "Parameters : $($_.HashBlock)`n" + } + else { + "$($_.HashBlock)" + } + } + + Write-FormatView -TypeName 'PSNote' -Name 'PSNote' -Property Note, Catalog, Alias, Tags, Snippet -Width 25, 15, 15, 15, 0 + + Write-FormatView -TypeName 'PSNote' -Name 'PSNote' -Property Note, Catalog, Alias, Tags, Snippet -AsList +) + +$views | +Out-FormatData -ModuleName 'PSNotes' | +Set-Content -Path $formatPath -Encoding UTF8 + + +# Emit the file path so build scripts can consume it easily. +$formatPath diff --git a/tools/PSNotes.nuspec b/tools/PSNotes.nuspec new file mode 100644 index 0000000..81b6f2d --- /dev/null +++ b/tools/PSNotes.nuspec @@ -0,0 +1,10 @@ + + + + PSNotes + 1.0.0.0 + PSNotes + Matthew Dowst + PSModule + + \ No newline at end of file diff --git a/tools/PublishToPSGallery.ps1 b/tools/PublishToPSGallery.ps1 new file mode 100644 index 0000000..71ddd5f --- /dev/null +++ b/tools/PublishToPSGallery.ps1 @@ -0,0 +1,17 @@ +<# +Run build.ps1 first +#> + +# Script variables +$TopLevel = (Split-Path $PSScriptRoot) +$NugetAPIKey = Get-Content (Join-Path $PSScriptRoot 'APIKey.json') + +Set-Location $TopLevel + +# Get the module manifest +$psd1File = Get-ChildItem -Path (Join-Path $TopLevel 'bin\PSNotes') -Filter 'PSNotes.psd1' -Recurse | Select-Object -Last 1 +$psd1 = Test-ModuleManifest $psd1File +Read-Host "Publish version '$($psd1.Version)'" + +# Publish to powershell gallery +Publish-Module -Path $psd1File.DirectoryName -NugetAPIKey $NugetAPIKey -Verbose \ No newline at end of file diff --git a/tools/build-docs.ps1 b/tools/build-docs.ps1 new file mode 100644 index 0000000..c7407f4 --- /dev/null +++ b/tools/build-docs.ps1 @@ -0,0 +1,116 @@ +$currentPath = (Get-Location).Path +$sourceRoot = Split-Path $PSScriptRoot -Parent +Set-Location -LiteralPath $sourceRoot + +function Add-ExamplePowerShellFence { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Path + ) + + $lines = Get-Content -LiteralPath $Path + $updated = [System.Collections.Generic.List[string]]::new() + + $pendingExample = $false + $inInsertedFence = $false + $removingAlias = $false + + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i] + + if($removingAlias) { + if ($line -match '^##') { + $removingAlias = $false + } + else { + continue + } + } + + if ($line -match '^## ALIASES\b') { + $removingAlias = $true + continue + } + elseif ($line -match '^\{\{ ') { + continue + } + elseif ($line -match '^### EXAMPLE\b') { + $updated.Add($line) + $pendingExample = $true + continue + } + + if ($pendingExample) { + if ([string]::IsNullOrWhiteSpace($line)) { + $updated.Add($line) + continue + } + + if ($line -eq '```powershell') { + $updated.Add($line) + $pendingExample = $false + $inInsertedFence = $true + continue + } + + $updated.Add('```powershell') + $updated.Add($line) + $pendingExample = $false + $inInsertedFence = $true + continue + } + + if ($inInsertedFence -and [string]::IsNullOrWhiteSpace($line)) { + if ($updated.Count -eq 0 -or $updated[$updated.Count - 1] -ne '```') { + $updated.Add('```') + } + $updated.Add($line) + $inInsertedFence = $false + continue + } + + if ($inInsertedFence -and $line -eq '```') { + $updated.Add($line) + $inInsertedFence = $false + continue + } + + $updated.Add($line) + } + + if ($inInsertedFence -and ($updated.Count -eq 0 -or $updated[$updated.Count - 1] -ne '```')) { + $updated.Add('```') + } + + Set-Content -LiteralPath $Path -Value $updated +} + +if (-not (Test-Path .\bin\PSNotes\)) { + throw "Project must be built first" +} + +$psd1 = Get-ChildItem .\bin -Filter 'PSNotes.psd1' -Recurse | Select-Object -Last 1 +Import-Module $psd1.FullName -Force + +Get-ChildItem .\Documentation -Filter '*.md' -Recurse | Remove-Item -Force + +$newMarkdownCommandHelpSplat = @{ + ModuleInfo = Get-Module PSNotes + OutputFolder = '.\Documentation' + HelpVersion = '1.0.0.0' + WithModulePage = $true +} +New-MarkdownCommandHelp @newMarkdownCommandHelpSplat + +Get-ChildItem .\Documentation\PSNotes -Filter '*.md' | ForEach-Object { + Add-ExamplePowerShellFence -Path $_.FullName +} + +Get-ChildItem .\Documentation\PSNotes -Filter 'PSNotes.md' | Move-Item -Destination '.\Documentation\Commands.md' -Force + +Get-ChildItem .\Documentation\PSNotes -Filter '*.md' | Move-Item -Destination .\Documentation -Force +Remove-Item -Path .\Documentation\PSNotes -Force + +Set-Location -LiteralPath $currentPath +#> \ No newline at end of file diff --git a/tools/build.ps1 b/tools/build.ps1 new file mode 100644 index 0000000..33f3d78 --- /dev/null +++ b/tools/build.ps1 @@ -0,0 +1,123 @@ +[CmdletBinding()] +param( + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] $OutDir = (Join-Path (Split-Path $PSScriptRoot) 'bin'), + + [Parameter()] + [string] $Version +) + +$ErrorActionPreference = 'Stop' +# This can some times cause old versions to load in the current session, so we capture the directory and then clean up anything that might have been added. +$homePath = if (Get-Variable -Name IsLinux -Scope Global -ValueOnly -ErrorAction SilentlyContinue) { + '/home/PSNotes' +} +else { + Join-Path $env:APPDATA 'PSNotes' +} +$beforeFiles = if (Test-Path $homePath) { + Get-ChildItem -Path $homePath -File +} + +# --- Ensure ModuleBuilder is available (build-time dependency) --- +if (-not (Get-Module -ListAvailable -Name ModuleBuilder)) { + throw "ModuleBuilder is required to build. Install it (e.g. Install-Module ModuleBuilder -Scope CurrentUser) and re-run." +} +Import-Module ModuleBuilder -ErrorAction Stop +if (-not (Get-Module -ListAvailable -Name EZOut)) { + throw "EZOut is required to build. Install it (e.g. Install-Module EZOut -Scope CurrentUser) and re-run." +} +Import-Module EZOut -ErrorAction Stop + +$currentPath = (Get-Location).Path +$sourceRoot = Join-Path (Split-Path $PSScriptRoot -Parent) 'src' +Set-Location -LiteralPath $sourceRoot +$destRoot = Join-Path $OutDir 'PSNotes' + +if (Test-Path -LiteralPath $destRoot) { + Remove-Item -LiteralPath $destRoot -Recurse -Force +} +if (Test-Path -LiteralPath $OutDir) { + Get-ChildItem -LiteralPath $OutDir -Filter '*.nupkg' | Remove-Item -Force +} + +..\tools\PSNotes.ezformat.ps1 -formatPath (Join-Path $sourceRoot 'PSNotes.format.ps1xml') | Out-Null + +$linter = . '..\tests\ScriptAnalyzer\ScriptAnalyzer.Linter.ps1' +if ($linter) { + $linter + throw "Failed linter tests" +} + +$buildParams = @{ + SourcePath = $sourceRoot + OutputDirectory = $destRoot + Encoding = 'UTF8Bom' # consistent cross-platform +} + +$sourceManifest = Join-Path $sourceRoot 'PSNotes.psd1' + +$Version = "v1.0.0" +if (-not $PSBoundParameters.ContainsKey('Version') -or [string]::IsNullOrEmpty($Version)) { + $Version = (Import-PowerShellDataFile -Path $sourceManifest).ModuleVersion +} +if($Version -match '^v'){ + $Version = $Version.Replace('v','') +} + +# Ensure version has 4 parts (major.minor.patch.revision) +$versionParts = $Version -split '\.' +while ($versionParts.Count -lt 4) { + $versionParts += '0' +} +$Version = $versionParts -join '.' + +$testVersion = [version]$Version + +$buildParams['Version'] = $testVersion + + + +Write-Host "Building PSNotes module..." -ForegroundColor Cyan +Write-Host " SourcePath : $sourceRoot" +Write-Host " SourceManifest : $sourceManifest" +Write-Host " OutputDir : $destRoot" +Write-Host " Version : $testVersion" + +Build-Module @buildParams | Out-Null + +Write-Host "Build complete: $destRoot" -ForegroundColor Green +Get-ChildItem -LiteralPath $destRoot -Filter 'PSNotes.psd1' -Recurse | ForEach-Object { + Write-Host "Validating manifest: $($_.FullName)" -ForegroundColor Cyan + Test-ModuleManifest -Path $_.FullName | Out-Null + Write-Host "Manifest valid." -ForegroundColor Green +} + +Get-ChildItem -LiteralPath $destRoot -Filter 'PSNotes.psm1' -Recurse | ForEach-Object { + Out-File -Append -FilePath $_.FullName -Encoding UTF8 -InputObject "`n`nInitialize-PSNoteStore`nSet-Alias -Name psnote -Value Get-PSNoteMenu -Force`n" +} + +# Create NuGet Package +Set-Location -Path $PSScriptRoot +$psd1 = Get-ChildItem $OutDir -Filter '*.psd1' -Recurse | Select-Object -Last 1 +$nuspec = Get-ChildItem $PSScriptRoot -Filter '*.nuspec' -Recurse | Foreach-Object { + Copy-Item -Path $_.FullName -Destination $psd1.DirectoryName -PassThru +} + +.\nuget.exe pack "$($nuspec.FullName)" -OutputDirectory $OutDir -Version "$($Version)" + +Get-ChildItem $psd1.DirectoryName -Filter '*.nuspec' | Remove-Item -Force + +Set-Location -LiteralPath $currentPath + + +# Clean up any loaded files that were not present before. +if (Test-Path $homePath) { + Get-ChildItem -Path $homePath -File | Where-Object { + $beforeFiles.Name -notcontains $_.Name + } | ForEach-Object { + Remove-Item -Path $_.FullName -Force + } +} \ No newline at end of file diff --git a/tools/load-development.ps1 b/tools/load-development.ps1 new file mode 100644 index 0000000..cbb6902 --- /dev/null +++ b/tools/load-development.ps1 @@ -0,0 +1,62 @@ +param( + [switch]$Refresh +) +$Path = Join-Path (Split-Path $PSScriptRoot) 'src' +Get-Module PSNotes | Remove-Module -Force +$psd1 = Join-Path -Path $Path -ChildPath 'PSNotes.psd1' +$psm1 = Join-Path -Path $Path -ChildPath 'PSNotes.psm1' + +# Create a temporary directory for test files +$script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\MyTests" +$null = New-Item -Path $script:TestDir -ItemType Directory -Force + +# Set up test environment variable +$script:OriginalPSNotesHome = $env:PSNOTES_HOME +$env:PSNOTES_HOME = $script:TestDir +if ($Refresh -and (Test-Path $env:PSNOTES_HOME)) { + Remove-Item -Path $env:PSNOTES_HOME -Recurse -Force +} +$null = New-Item -Path $env:PSNOTES_HOME -ItemType Directory -Force + +# Create the .psm1 file by dot sourcing all .ps1 files in the src folder and subfolders +$psm1Script = { + $Path = $PSScriptRoot + # Import the functions + foreach ($folder in @('classes', 'private', 'public')) { + $root = Join-Path -Path $Path -ChildPath $folder + if (Test-Path -Path $root) { + Write-Verbose "processing folder $root" + $files = Get-ChildItem -Path $root -Filter *.ps1 -Recurse + + # dot source each file + $files | where-Object { $_.name -NotLike '*.Tests.ps1' } | + ForEach-Object { Write-Verbose $_.name; . $_.FullName } + } + } + + # Load all commands to noteObjects + Initialize-PSNoteStore +} + +# Create the .psm1 file, import the module, and then remove the .psm1 file +$psm1Script.ToString() | Out-File -FilePath $psm1 -Encoding UTF8 -Force +Import-Module -Name $psd1 -Force -Verbose +Remove-Item -Path $psm1 -Force + +# Import the classes into the current session so they can be used to debug outside of the module +$root = Join-Path -Path $Path -ChildPath 'classes' +if (Test-Path -Path $root) { + Write-Verbose "processing folder $root" + $files = Get-ChildItem -Path $root -Filter *.ps1 -Recurse + + # dot source each file + $files | where-Object { $_.name -NotLike '*.Tests.ps1' } | + ForEach-Object { Write-Verbose $_.name; . $_.FullName } +} + +# Initialize the note store for use in the current session, this allows us to use the PSNotes commands outside of the module for testing and debugging +# This is the same code that is used in the Initialize-PSNoteStore command, but we need to run it here to initialize the note store for the current session +$script:_noteStore = [NoteStore]::new() +Write-Verbose "User PSNotes Path: $env:PSNOTES_HOME" + +Set-Alias -Name psnote -Value Get-PSNoteMenu -Force \ No newline at end of file diff --git a/tools/nuget.exe b/tools/nuget.exe new file mode 100644 index 0000000..8db00e2 Binary files /dev/null and b/tools/nuget.exe differ diff --git a/tools/tests-build.ps1 b/tools/tests-build.ps1 new file mode 100644 index 0000000..4bf1e8b --- /dev/null +++ b/tools/tests-build.ps1 @@ -0,0 +1,26 @@ +$Parent = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Parent 'src'))) { + $Parent = Split-Path $Parent -Parent +} + +$TestPath = Join-Path $Parent 'tests' +$binPath = Join-Path $Parent 'bin' +if(-not (Test-Path $binPath)){ + New-Item -ItemType Directory -Force -Path $binPath | Out-Null +} + +$psd1 = Get-ChildItem -Path $binPath -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName +if(-not $psd1){ + Write-Host "PSNotes.psd1 not found in $binPath. Please build the module before running tests." + exit 1 +} + +# Run Unit Tests +$config = New-PesterConfiguration +$config.Output.Verbosity = 'Detailed' +$config.Run.Throw = $false +$config.TestResult.Enabled = $true +$config.TestResult.OutputFormat = 'JUnitXml' +$config.Run.Path = (Join-Path $TestPath 'Build') +$config.TestResult.OutputPath = (Join-Path $binPath 'Build.TestResults.xml') +Invoke-Pester -Configuration $config \ No newline at end of file diff --git a/tools/tests.ps1 b/tools/tests.ps1 new file mode 100644 index 0000000..ae78dc9 --- /dev/null +++ b/tools/tests.ps1 @@ -0,0 +1,41 @@ +$Parent = $PSScriptRoot +while ( -not (Test-Path (Join-Path $Parent 'src'))) { + $Parent = Split-Path $Parent -Parent +} + +$TestPath = Join-Path $Parent 'tests' +$binPath = Join-Path $Parent 'bin' +if(-not (Test-Path $binPath)){ + New-Item -ItemType Directory -Force -Path $binPath | Out-Null +} + +$psd1 = Get-ChildItem -Path $binPath -Recurse -Filter 'PSNotes.psd1' | Select-Object -Last 1 -ExpandProperty FullName +if(-not $psd1){ + Write-Host "PSNotes.psd1 not found in $binPath. Please build the module before running tests." + exit 1 +} + +# Set test parameters for all tests +$config = New-PesterConfiguration +$config.Output.Verbosity = 'Detailed' +$config.Run.Throw = $false +$config.TestResult.Enabled = $true +$config.TestResult.OutputFormat = 'JUnitXml' + +# Run Script Analyzer Tests +$config.Run.Path = (Join-Path $TestPath 'ScriptAnalyzer') +$config.TestResult.OutputPath = (Join-Path $binPath 'ScriptAnalyzer.TestResults.xml') +Invoke-Pester -Configuration $config + +# Run Unit Tests +$config.Run.Path = (Join-Path $TestPath 'UnitTests') +$config.TestResult.OutputPath = (Join-Path $binPath 'Pester.TestResults.xml') +Invoke-Pester -Configuration $config + + + +<# Run Build Tests +$config.Run.Path = (Join-Path $TestPath 'Build') +$config.TestResult.OutputPath = (Join-Path $binPath 'Build.TestResults.xml') +Invoke-Pester -Configuration $config +#> \ No newline at end of file