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..8d6fa1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ Publish/APIKey.json Publish/PSNotes/ -*.code-workspace \ No newline at end of file +*.code-workspace +bin/ +Documentation/demos/* +.vscode/* 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/ConvertTo-Splatting.MD b/Documentation/ConvertTo-Splatting.MD index 7d3f081..3b7b065 100644 --- a/Documentation/ConvertTo-Splatting.MD +++ b/Documentation/ConvertTo-Splatting.MD @@ -1,96 +1,185 @@ -# ConvertTo-Splatting +--- +external help file: PSNotes-help.xml +Module Name: PSNotes +online version: +schema: 2.0.0 +--- - -## ConvertTo-Splatting +# ConvertTo-Splatting -![ConvertTo-Splatting01](media/ConvertTo-Splatting01.png) +## SYNOPSIS -### 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 + +## SYNTAX + +### string + +``` +ConvertTo-Splatting [[-Command ]] [-ProgressAction ] [] +``` + +### scriptblock + +``` +ConvertTo-Splatting [[-ScriptBlock ]] [-ProgressAction ] [] +``` + +## 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] ] [] +## EXAMPLES + +### Example 1: EXAMPLE 1 + ``` -### 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" -'@ +$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" -} + +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 2: EXAMPLE 2 + ``` -#### Example 2: Converts the scriptblock splatme to splatting -```powershell -$splatme = { -Copy-Item -Path "test.txt" -Destination "test2.txt" -WhatIf -} +$splatme = { + Copy-Item -Path "test.txt" -Destination "test2.txt" -WhatIf +} ConvertTo-Splatting $splatme ``` -###### Output -``` -$CopyItemParam = @{ - Path = "test.txt" - Destination = "test2.txt" - WhatIf = $true -} + +Converts the scriptblock splatme to splatting + +--- Output ---- +$CopyItemParam = @{ + Path = "test.txt" + Destination = "test2.txt" + WhatIf = $true +} Copy-Item @CopyItemParam + + + + + +### Example 3: EXAMPLE 3 + ``` -#### Example 3: Removed backticks and converts the scriptblock splatme to splatting -```powershell -$splatme = { -Get-AzVM ` - -ResourceGroupName "ResourceGroup11" ` - -Name "VirtualMachine07" ` - -Status -} +$splatme = { + Get-AzVM ` + -ResourceGroupName "ResourceGroup11" ` + -Name "VirtualMachine07" ` + -Status +} ConvertTo-Splatting $splatme ``` -###### Output -``` -$GetAzVMParam = @{ - ResourceGroupName = "ResourceGroup11" - Name = "VirtualMachine07" - Status = $true -} + +Removed backticks and converts the scriptblock splatme to splatting + +--- Output ---- +$GetAzVMParam = @{ + ResourceGroupName = "ResourceGroup11" + Name = "VirtualMachine07" + Status = $true +} Get-AzVM @GetAzVMParam + + + + + + +## PARAMETERS + +### -Command + +The command string you want to convert to using splatting + +```yaml +Type: String +Parameter Sets: string +Aliases: +Accepted values: + +Required: True (None) False (string) +Position: 0 +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + +### -ProgressAction + +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga +Accepted values: + +Required: True (None) False (All) +Position: Named +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + +### -ScriptBlock + +The command scriptblock you want to convert to using splatting + +```yaml +Type: ScriptBlock +Parameter Sets: scriptblock +Aliases: +Accepted values: + +Required: True (None) False (scriptblock) +Position: 0 +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False ``` + + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## NOTES + +about_Splatting - https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting + + +## RELATED LINKS + +Fill Related Links Here + 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 index 163e549..4900c6e 100644 Binary files a/Documentation/Export-PSNote.MD and b/Documentation/Export-PSNote.MD differ diff --git a/Documentation/Get-CommandSplatting.MD b/Documentation/Get-CommandSplatting.MD index 0e6ba20..ea7aacc 100644 --- a/Documentation/Get-CommandSplatting.MD +++ b/Documentation/Get-CommandSplatting.MD @@ -1,186 +1,337 @@ -# Get-CommandSplatting +--- +external help file: PSNotes-help.xml +Module Name: PSNotes +online version: +schema: 2.0.0 +--- - -## Get-CommandSplatting +# Get-CommandSplatting -![Get-CommandSplatting01](media/Get-CommandSplatting01.png) +## SYNOPSIS -### Synopsis Use to output the parameters for a command in splatting format -### Description + +## SYNTAX + +### ParameterSet (Default) + +``` +Get-CommandSplatting [-Command] [[-ParameterSet ]] [-IncludeCommon] [-Copy] [-ProgressAction ] [] +``` + +### ListParameterSets + +``` +Get-CommandSplatting [-Command] [-ListParameterSets] [-IncludeCommon] [-Copy] [-ProgressAction ] [] +``` + +### All + +``` +Get-CommandSplatting [-Command] [-All] [-IncludeCommon] [-Copy] [-ProgressAction ] [] +``` + +## 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 + +## EXAMPLES + +### Example 1: EXAMPLE 1 + +``` 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 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 2: EXAMPLE 2 + ``` -#### 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 + +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 3: EXAMPLE 3 + ``` -#### 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 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 4: EXAMPLE 4 + ``` -#### 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 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 + + + + + + +## PARAMETERS + +### -All + +Use to return full splatting for all parameter sets + +```yaml +Type: SwitchParameter +Parameter Sets: All +Aliases: +Accepted values: + +Required: True (None) False (All) +Position: 1 +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False ``` + +### -Command + +The command to get the parameters for + +```yaml +Type: String +Parameter Sets: (All) +Aliases: +Accepted values: + +Required: True (All) False (None) +Position: 0 +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + +### -Copy + +{{ Fill Copy Description }} + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: +Accepted values: + +Required: True (None) False (All) +Position: 3 +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + +### -IncludeCommon + +Use to include the PowerShell common parameters in the splatting output. +(e.g. +Verbose, ErrorAction, etc.) + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: +Accepted values: + +Required: True (None) False (All) +Position: 2 +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + +### -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. + +```yaml +Type: SwitchParameter +Parameter Sets: ListParameterSets +Aliases: +Accepted values: + +Required: True (None) False (ListParameterSets) +Position: 1 +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + +### -ParameterSet + +Use to specify a specific parameter set. +Use the -ListParameterSets to get a quick +view of all the different Parameter Set names. + +```yaml +Type: String +Parameter Sets: ParameterSet +Aliases: +Accepted values: + +Required: True (None) False (ParameterSet) +Position: 1 +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + +### -ProgressAction + +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga +Accepted values: + +Required: True (None) False (All) +Position: Named +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## NOTES + +General notes + + +## RELATED LINKS + +Fill Related Links Here + diff --git a/Documentation/Get-PSNote.MD b/Documentation/Get-PSNote.MD index 5e452e8..afddec9 100644 Binary files a/Documentation/Get-PSNote.MD and b/Documentation/Get-PSNote.MD differ diff --git a/Documentation/Get-PSNoteAlias.MD b/Documentation/Get-PSNoteAlias.MD index 3f70b1d..0808e04 100644 Binary files a/Documentation/Get-PSNoteAlias.MD and b/Documentation/Get-PSNoteAlias.MD differ diff --git a/Documentation/Import-PSNote.MD b/Documentation/Import-PSNote.MD index c2bd57b..8f9b9aa 100644 Binary files a/Documentation/Import-PSNote.MD and b/Documentation/Import-PSNote.MD differ diff --git a/Documentation/Initialize-PSNoteStore.md b/Documentation/Initialize-PSNoteStore.md new file mode 100644 index 0000000..8a9a49b --- /dev/null +++ b/Documentation/Initialize-PSNoteStore.md @@ -0,0 +1,78 @@ +--- +external help file: PSNotes-help.xml +Module Name: PSNotes +online version: +schema: 2.0.0 +--- + +# Initialize-PSNoteStore + +## SYNOPSIS + +{{ Fill in the Synopsis }} + +## SYNTAX + +### __AllParameterSets + +``` +Initialize-PSNoteStore [-ProgressAction ] [] +``` + +## DESCRIPTION + +{{ Fill in the Description }} + +## EXAMPLES + +### Example 1: Example 1 + +``` +PS C:\> {{ Add example code here }} +``` + +{{ Add example description here }} + +## PARAMETERS + +### -ProgressAction + +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga +Accepted values: + +Required: True (None) False (All) +Position: Named +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + + + ## OUTPUTS + +### System.Object + + + ## NOTES + +{{ Fill in the Notes }} + +## RELATED LINKS + +Fill Related Links Here + 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/New-PSNote.MD b/Documentation/New-PSNote.MD index f743977..23c7e97 100644 Binary files a/Documentation/New-PSNote.MD and b/Documentation/New-PSNote.MD differ diff --git a/Documentation/Release Notes.md b/Documentation/Release Notes.md new file mode 100644 index 0000000..5220152 --- /dev/null +++ b/Documentation/Release Notes.md @@ -0,0 +1,100 @@ +## Release Notes + +This release represents a major step forward for **PSNotes**, expanding it from a simple snippet store into a more structured, interactive, and execution-friendly tool. + +### ✨ What’s New + +**Note Catalogs** + +* Introduces first-class support for **Note Catalogs**, allowing notes to be logically grouped. +* Enables cleaner organization, easier discovery, and better scalability as libraries grow. +* Allow notes to be created without aliases, allowing for larger growth + +**Default Snippet Execution** + +* Notes can now be configured to **execute by default** instead of requiring explicit flags. +* Reduces friction for frequently used snippets and supports a more command-like workflow. + +**File Execution via Note Alias** + +* Adds support for **executing script files directly** through a note alias. +* Enables PSNotes to act as a lightweight command launcher for local scripts and utilities. + +**Improved Import / Export** + +* Enhancements to import and export improve reliability and usability. +* Better handling of metadata and structure to support catalogs and future extensibility. + +**PSNotes Terminal App** + +* Introduces an interactive, terminal-based UI via `Start-PSNote`. +* Provides a nostalgic, menu-driven experience with modern usability: + + * Browse notes and catalogs + * Execute snippets or scripts + * Designed to run directly in the current console session + +### 🔧 Why This Matters + +These changes collectively move PSNotes toward being: + +* More **discoverable** +* More **interactive** +* More **automation-friendly** + +While remaining lightweight, scriptable, and PowerShell-native. + +--- + +## Release Notes + +### 🚀 New Features + +* **Note Catalogs** + + * Organize notes into logical collections + * Improves navigation and long-term maintainability + +* **Default Execution Behavior** + + * Notes can now be configured to run immediately when invoked + * Ideal for commonly used commands and scripts + +* **File Execution via Alias** + + * Execute local script files directly through PSNotes aliases + * Expands PSNotes beyond snippets into lightweight task execution + +* **Interactive Terminal App** + + * New `Start-PSNote` command launches a terminal-based UI + * Menu-driven navigation inspired by classic console tools + * Designed for fast recall, execution, and exploration + +### 🔄 Improvements + +* **Import / Export Enhancements** + + * Improved structure and resilience + * Better support for catalogs and note metadata + * Smoother portability across environments + +### 🧭 Looking Ahead + +This release lays the groundwork for: + +* Richer metadata and filtering +* Favorites and scoped views +* More advanced execution workflows + +As always, feedback and contributions are welcome. + +--- + +If you want, I can also: + +* Tighten this down into a **short “What changed?” summary** +* Rewrite it in a more **marketing / announcement tone** +* Or tailor it specifically for a **PowerShell Gallery release blurb** + +Just say the word. diff --git a/Documentation/Remove-PSNote.MD b/Documentation/Remove-PSNote.MD index 838a29d..9840857 100644 Binary files a/Documentation/Remove-PSNote.MD and b/Documentation/Remove-PSNote.MD differ diff --git a/Documentation/Set-PSNote.MD b/Documentation/Set-PSNote.MD index 9430ae6..39e1370 100644 Binary files a/Documentation/Set-PSNote.MD and b/Documentation/Set-PSNote.MD differ diff --git a/Documentation/Start-PSNote.md b/Documentation/Start-PSNote.md new file mode 100644 index 0000000..ae772d0 --- /dev/null +++ b/Documentation/Start-PSNote.md @@ -0,0 +1,78 @@ +--- +external help file: PSNotes-help.xml +Module Name: PSNotes +online version: +schema: 2.0.0 +--- + +# Start-PSNote + +## SYNOPSIS + +{{ Fill in the Synopsis }} + +## SYNTAX + +### __AllParameterSets + +``` +Start-PSNote [-ProgressAction ] [] +``` + +## DESCRIPTION + +{{ Fill in the Description }} + +## EXAMPLES + +### Example 1: Example 1 + +``` +PS C:\> {{ Add example code here }} +``` + +{{ Add example description here }} + +## PARAMETERS + +### -ProgressAction + +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga +Accepted values: + +Required: True (None) False (All) +Position: Named +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + + + ## OUTPUTS + +### System.Object + + + ## NOTES + +{{ Fill in the Notes }} + +## RELATED LINKS + +Fill Related Links Here + diff --git a/Documentation/Update-PSNoteStore.md b/Documentation/Update-PSNoteStore.md new file mode 100644 index 0000000..d922d75 --- /dev/null +++ b/Documentation/Update-PSNoteStore.md @@ -0,0 +1,96 @@ +--- +external help file: PSNotes-help.xml +Module Name: PSNotes +online version: +schema: 2.0.0 +--- + +# Update-PSNoteStore + +## SYNOPSIS + +{{ Fill in the Synopsis }} + +## SYNTAX + +### __AllParameterSets + +``` +Update-PSNoteStore [[-DefaultBehavior ]] [-ProgressAction ] [] +``` + +## DESCRIPTION + +{{ Fill in the Description }} + +## EXAMPLES + +### Example 1: Example 1 + +``` +PS C:\> {{ Add example code here }} +``` + +{{ Add example description here }} + +## PARAMETERS + +### -DefaultBehavior + +{{ Fill DefaultBehavior Description }} + +```yaml +Type: String +Parameter Sets: (All) +Aliases: +Accepted values: + +Required: True (None) False (All) +Position: 0 +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + +### -ProgressAction + +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga +Accepted values: + +Required: True (None) False (All) +Position: Named +Default value: +Accept pipeline input: False +Accept wildcard characters: False +DontShow: False +``` + + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + + + ## OUTPUTS + +### System.Object + + + ## NOTES + +{{ Fill in the Notes }} + +## RELATED LINKS + +Fill Related Links Here + 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/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-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/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/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 index 22845b9..ca1fe9e 100644 --- a/Publish/PublishToPSGallery.ps1 +++ b/Publish/PublishToPSGallery.ps1 @@ -8,15 +8,15 @@ $psd1File = Get-ChildItem -path $ModulesFolder -Filter "*.psd1" | Select-Object $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) +#$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'" +#Write-Verbose "New version '$version'" -Update-ModuleManifest -Path $psd1File -ModuleVersion $newVersion +#Update-ModuleManifest -Path $psd1File -ModuleVersion $newVersion # create the release folder -$releaseFolder = Join-Path $PSScriptRoot "\PSNotes\$($newVersion.ToString())" +$releaseFolder = Join-Path $PSScriptRoot "\PSNotes\$($psd1.Version.ToString())" If (-not(Test-Path $releaseFolder)){ New-Item -type Directory -Path $releaseFolder | Out-Null } diff --git a/README.md b/README.md index b0e0e8f..852e997 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,211 @@ -# 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 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 v7+ +```powershell +Install-Module PSNotes +``` + +[top](#psnotes) +# Commands + +| Cmdlet | Synopsis | +| ------ | -------- | +| [ConvertTo-Splatting](Documentation/ConvertTo-Splatting.md) | Use to convert an existing PowerShell command to splatting | +| [Export-PSNote](Documentation/Export-PSNote.md) | Use to export your PSNotes to copy to another machine or share with others | +| [Get-CommandSplatting](Documentation/Get-CommandSplatting.md) | Use to output the parameters for a command in splatting format | +| [Get-PSNote](Documentation/Get-PSNote.md) | Use to search for or list the different PSNotes | +| [Get-PSNoteAlias](Documentation/Get-PSNoteAlias.md) | Use display snippet and copy to clipboard using an Alias | +| [Import-PSNote](Documentation/Import-PSNote.md) | Use to import a PSNotes JSON fiile | +| [Initialize-PSNoteStore](Documentation/Initialize-PSNoteStore.md) | {{ Fill in the Synopsis }} | +| [New-PSNote](Documentation/New-PSNote.md) | Use to add or update a PSNote object | +| [Remove-PSNote](Documentation/Remove-PSNote.md) | Remove one or more PSNotes from the note store. | +| [Set-PSNote](Documentation/Set-PSNote.md) | Use to add or update a PSNote object | +| [Start-PSNote](Documentation/Start-PSNote.md) | {{ Fill in the Synopsis }} | +| [Update-PSNoteStore](Documentation/Update-PSNoteStore.md) | {{ Fill in the Synopsis }} | + +[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) 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..718bbc0 --- /dev/null +++ b/src/Classes/NoteStore.class.ps1 @@ -0,0 +1,1018 @@ +enum PSNoteKind { + Snippet + Script +} + +class PSNoteMenuItem { + + [string] $Name + [string] $Key + [char] $Alt + [string] $Label + + hidden PSNoteMenuItem( + [string] $name, + [string] $key, + [char] $alt, + [string] $label + ) { + $this.Name = $name + $this.Key = $key + $this.Alt = $alt + $this.Label = $label + } + + hidden PSNoteMenuItem( + [string] $name, + [string] $label + ) { + $this.Name = $name + $this.Key = $null + $this.Alt = $null + $this.Label = $label + } + + # --- Static "enum-like" instances --- + static [PSNoteMenuItem] $Main = [PSNoteMenuItem]::new('Main', '^M', 'M', 'Main') + static [PSNoteMenuItem] $Catalogs = [PSNoteMenuItem]::new('Catalogs', '^G', 'G', 'Catalogs') + static [PSNoteMenuItem] $Tags = [PSNoteMenuItem]::new('Tags', '^T', 'T', 'Tags') + static [PSNoteMenuItem] $Favorites = [PSNoteMenuItem]::new('Favorites', '^F', 'F', 'Favorites') + static [PSNoteMenuItem] $Settings = [PSNoteMenuItem]::new('Settings', '^O', 'O', 'Settings') + static [PSNoteMenuItem] $Search = [PSNoteMenuItem]::new('Search', '^W', 'W', 'Search') + static [PSNoteMenuItem] $Help = [PSNoteMenuItem]::new('Help', '^H', 'H', 'Help') + static [PSNoteMenuItem] $Back = [PSNoteMenuItem]::new('Back', '^B', 'B', 'Back') + static [PSNoteMenuItem] $Quit = [PSNoteMenuItem]::new('Quit', '^Q', 'Q', 'Quit') + static [PSNoteMenuItem] $Welcome = [PSNoteMenuItem]::new('Welcome', '^W', 'W', 'Welcome') + static [PSNoteMenuItem] $AllCatalogs = [PSNoteMenuItem]::new('AllCatalogs', '^A', 'A', 'All') + static [PSNoteMenuItem] $Preview = [PSNoteMenuItem]::new('Preview', '^P', 'P', 'Preview') + static [PSNoteMenuItem] $NewNote = [PSNoteMenuItem]::new('NewNote', '^N', 'N', 'New') + static [PSNoteMenuItem] $NoteList = [PSNoteMenuItem]::new('NoteList', 'NoteList') + static [PSNoteMenuItem] $NoteActions = [PSNoteMenuItem]::new('NoteActions', 'NoteActions') + static [PSNoteMenuItem] $Previous = [PSNoteMenuItem]::new('Previous', '^Y', 'Y', 'Previous') + static [PSNoteMenuItem] $Next = [PSNoteMenuItem]::new('Next', '^U', 'U', 'Next') + static [PSNoteMenuItem] $Copy = [PSNoteMenuItem]::new('Copy', '^C', 'C', 'Copy') + static [PSNoteMenuItem] $Execute = [PSNoteMenuItem]::new('Execute', '^E', 'E', 'Execute') + static [PSNoteMenuItem] $ToggleFav = [PSNoteMenuItem]::new('ToggleFav', '^T', 'T', 'Toggle Favorite') + + static [PSNoteMenuItem[]] GetAll() { + return @( + [PSNoteMenuItem]::Main + [PSNoteMenuItem]::Catalogs + [PSNoteMenuItem]::Tags + [PSNoteMenuItem]::Favorites + [PSNoteMenuItem]::Settings + [PSNoteMenuItem]::Search + [PSNoteMenuItem]::Help + [PSNoteMenuItem]::Back + [PSNoteMenuItem]::Quit + [PSNoteMenuItem]::Welcome + [PSNoteMenuItem]::AllCatalogs + [PSNoteMenuItem]::Preview + [PSNoteMenuItem]::NewNote + [PSNoteMenuItem]::Copy + [PSNoteMenuItem]::Execute + [PSNoteMenuItem]::ToggleFav + ) + } + + static [PSNoteMenuItem[]] GetMain() { + return @( + # Top Row + [PSNoteMenuItem]::Main + [PSNoteMenuItem]::NewNote + [PSNoteMenuItem]::AllCatalogs + [PSNoteMenuItem]::Search + [PSNoteMenuItem]::Help + # Bottom Row + [PSNoteMenuItem]::Quit + [PSNoteMenuItem]::Favorites + [PSNoteMenuItem]::Catalogs + [PSNoteMenuItem]::Back + [PSNoteMenuItem]::Settings + ) + } + + static [PSNoteMenuItem[]] GetNoteActions() { + return @( + # Top Row + [PSNoteMenuItem]::Copy + [PSNoteMenuItem]::Execute + [PSNoteMenuItem]::ToggleFav + [PSNoteMenuItem]::Back + ) + } + + static [PSNoteMenuItem] FromKey([string]$key) { + $from = [PSNoteMenuItem]::GetAll() | + Where-Object { $_.Key -eq $key -and -not [string]::IsNullOrEmpty($_.Key) } | Select-Object -First 1 + if (-not $from) { + $from = [PSNoteMenuItem]::GetAll() | + Where-Object { $_.Alt -eq $key.TrimStart('^') -and -not [string]::IsNullOrEmpty($_.Alt) } | Select-Object -First 1 + } + return $from + } + + static [PSNoteMenuItem] FromString([string]$name) { + return [PSNoteMenuItem]::GetAll() | + Where-Object { $_.Name -eq $name } | Select-Object -First 1 + } + + [string] ToString() { + return $this.Name + } +} + + +# Create the PSNote class +class PSNote { + [string]$Note + [string]$Snippet + [string]$Details + [string]$Alias + [string[]]$Tags + [string]$Catalog + [bool]$Run = $false + [PSNoteKind]$Kind = [PSNoteKind]::Snippet + + 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.Catalog = 'Default' + + $this.Kind = [PSNoteKind]::Snippet + } + + PSNote( + [string]$Note, + [string]$Snippet, + [string]$Details, + [string]$Alias, + [string[]]$Tags, + [string]$Catalog, + [bool]$Run + ) { + $this.Note = $Note + $this.Snippet = $Snippet + $this.Details = $Details + $this.Alias = $Alias + $this.Tags = $Tags + $this.Catalog = $Catalog + $this.Run = $Run + + $this.Kind = [PSNoteKind]::Snippet + } + + PSNote( + [string]$Note, + [PSNoteKind]$Kind, + [string]$Snippet, + [string]$Details, + [string]$Alias, + [string[]]$Tags, + [string]$Catalog, + [bool]$Run + ) { + $this.Note = $Note + $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.Note = $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 + + 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() + $this.Path = [NoteCatalog]::ResolvePath($Catalog, $env:PSNOTES_HOME) + $this.Catalog = $Catalog + $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.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 + + # 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 Note property + $noteValue = $null + if ($null -ne $note.PSObject.Properties['Note']) { + $noteValue = [string]$note.Note + } + if ([string]::IsNullOrWhiteSpace($noteValue)) { + $noteErrors.Add("Note[$index]: Missing or empty 'Note' 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 Note) + 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 Note 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 { $_.Note -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 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 NoteConfigStore { + static [int] $CurrentVersion = 1 + + [string] $Path + [int] $Version + [PSNoteMenuItem] $Main + [bool] $ExitOnCopy + [ConsoleColor] $ForegroundColor + [ConsoleColor] $BackgroundColor + + NoteConfigStore() { + $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 'psnoteconfig.json') + $this.SetDefaults() + $this.Open() + } + + NoteConfigStore([string] $path) { + $this.Path = $path + $this.SetDefaults() + $this.Open() + } + + [void] SetDefaults() { + $this.Version = [NoteConfigStore]::CurrentVersion + $this.Main = [PSNoteMenuItem]::Welcome + $this.ExitOnCopy = $true + $this.ForegroundColor = [ConsoleColor]::Black + $this.BackgroundColor = [ConsoleColor]::Gray + } + + [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 (-not [string]::IsNullOrWhiteSpace([string]$data.Main)) { + $tryFg = [PSNoteMenuItem]::Welcome + if ([Enum]::TryParse([string]$data.Main, [ref]$tryFg)) { + $this.Main = $tryFg + } + } + if ($null -ne $data.ExitOnCopy) { + $tryExitOnCopy = $false + if ([bool]::TryParse($data.ExitOnCopy, [ref]$tryExitOnCopy)) { + $this.ExitOnCopy = $tryExitOnCopy + } + } + if ($null -ne $data.ForegroundColor) { + $tryFg = [ConsoleColor]::Black + if ([Enum]::TryParse([string]$data.ForegroundColor, [ref]$tryFg)) { + $this.ForegroundColor = $tryFg + } + } + if ($null -ne $data.BackgroundColor) { + $tryBg = [ConsoleColor]::Gray + if ([Enum]::TryParse([string]$data.BackgroundColor, [ref]$tryBg)) { + $this.BackgroundColor = $tryBg + } + } + } + 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 NoteConsoleState { + static [int] $CurrentVersion = 1 + + [PSNoteMenuItem] $Mode + [PSNoteMenuItem[]] $ReturnMode + [string] $ScopeLabel + [System.Collections.Generic.List[PSNote]] $ScopeNotes + [System.Collections.Generic.List[PSNote]] $LastList + [NoteCatalog]$Catalog + [string] $Tag + [PSNote] $Note + [PSNote] $ExecuteOnExit + [NoteConfigStore] $Settings + + NoteConsoleState() { + $this.SetDefaults() + } + + NoteConsoleState([NoteStore] $Store) { + $this.SetDefaults() + $this.ScopeNotes = @($Store.Notes) + $this.Settings = $Store.Config + $tryMode = [PSNoteMenuItem]::FromString($Store.Config.Main) + if ($tryMode) { + $this.Mode = $tryMode + } + } + + [void] SetDefaults() { + $this.Mode = [PSNoteMenuItem]::Welcome + $this.ReturnMode = @() + $this.ScopeLabel = 'All' + $this.ScopeNotes = @() + $this.LastList = @() + $this.Catalog = $null + $this.Tag = $null + $this.Note = $null + $this.ExecuteOnExit = $null + $this.Settings = [NoteConfigStore]::new() + } +} +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() + + $defaultStore = [NoteCatalog]::new() + $this.LoadCatalog($defaultStore) + + $this.Metadata = [NoteMetadataStore]::new() + $this.Config = [NoteConfigStore]::new() + + $this.InitializeAliases() + } + + 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' + } + } + } + + [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 { $_.Note -eq $newNote.Note } + } + if ($dup -and $dup.Catalog -ne $newNote.Catalog) { + Write-Warning "Duplicate Alias found: $($newNote.Alias). Skipping note: $($newNote.Note)" + } + 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 '$( $_.Note )' 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) { + $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) { + $remove = $this.Notes | Where-Object { $_.Note -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) { + $noteNote = if ($null -ne $note.PSObject.Properties['Note']) { + [string]$note.Note + } + $noteCatalog = if ($null -ne $note.PSObject.Properties['Kind']) { + [string]$note.Catalog + } + $update = $this.Notes | Where-Object { $_.Note -eq $noteNote } + + if (-not $update) { + Write-Warning "Note '$($noteNote)' not found in catalog '$($noteCatalog)'. No action taken." + return + } + elseif ($update.Catalog -eq $noteCatalog) { + $this.RemoveNote($noteNote, $noteCatalog, $false) + $this.AddNote($note) + } + else { + Write-Warning "Note '$($noteNote)' exists in catalog '$($update.Catalog)'. Cannot update note in different catalog at this time '$($noteCatalog)'. No action taken." + } + } + + [string] GetNoteKey([PSNote] $note) { + return "$($note.Catalog)::$($note.Note)::$($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/PSNotes.psd1 b/src/PSNotes.psd1 similarity index 84% rename from PSNotes.psd1 rename to src/PSNotes.psd1 index 74ef0e0..ca95d7e 100644 --- a/PSNotes.psd1 +++ b/src/PSNotes.psd1 @@ -3,7 +3,7 @@ # # Generated by: Matthew Dowst # -# Generated on: 12/5/2019 +# Generated on: 6/2/2022 # @{ @@ -12,7 +12,7 @@ RootModule = '.\PSNotes.psm1' # Version number of this module. -ModuleVersion = '0.2.0.1' +ModuleVersion = '0.9.9.9' # Supported PSEditions # CompatiblePSEditions = @() @@ -27,28 +27,28 @@ Author = 'Matthew Dowst' CompanyName = 'dowst.dev' # Copyright statement for this module -Copyright = '(c) 2019 Matthew Dowst. All rights reserved.' +Copyright = '(c) 2026 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 +# Minimum version of the PowerShell engine required by this module PowerShellVersion = '2.0' -# Name of the Windows PowerShell host required by this module +# Name of the PowerShell host required by this module # PowerShellHostName = '' -# Minimum version of the Windows PowerShell host required by this module +# 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 = '' +# ClrVersion = '' # Processor architecture (None, X86, Amd64) required by this module -ProcessorArchitecture = 'Amd64' +ProcessorArchitecture = 'None' # Modules that must be imported into the global environment prior to importing this module # RequiredModules = @() @@ -57,20 +57,19 @@ ProcessorArchitecture = 'Amd64' # RequiredAssemblies = @() # Script files (.ps1) that are run in the caller's environment prior to importing this module. -ScriptsToProcess = '.\Resources\PSNote_Classes.ps1' +# ScriptsToProcess = '' # 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' +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 = 'Export-PSNote', 'Get-PSNote', 'Get-PSNoteAlias', 'Import-PSNote', 'Invoke-PSNote', 'Copy-PSNote', - 'New-PSNote', 'Remove-PSNote', 'Set-PSNote','Get-CommandSplatting','ConvertTo-Splatting' +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 = @() diff --git a/src/Private/Console/PSNotesFooter.ps1 b/src/Private/Console/PSNotesFooter.ps1 new file mode 100644 index 0000000..7fdc774 --- /dev/null +++ b/src/Private/Console/PSNotesFooter.ps1 @@ -0,0 +1,100 @@ +function Write-PSNotesFooter { + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param( + [Parameter(Mandatory)] + $State, + [Parameter(Mandatory = $false)] + $Menu = $null + ) + + $Items = [PSNoteMenuItem]::GetMain() + + [int] $Rows = 2 + $width = $Host.UI.RawUI.WindowSize.Width + if ( $width -lt 78) { $width = 78 } + $perRow = [Math]::Ceiling($Items.Count / $Rows) + + $footerTop = (Get-PSNotesViewportRow -FromBottom ($Rows)) - 1 # top row of footer block + + if($Menu) { + [Console]::SetCursorPosition(0, $footerTop - 2) + Write-Host $Menu -ForegroundColor Yellow + } + + # (optional) clear footer area first + for ($i = 0; $i -lt $Rows-1; $i++) { + [Console]::SetCursorPosition(0, $footerTop + $i) + Write-Host (' ' * $width) -NoNewline + } + + for ($r = 0; $r -lt $Rows; $r++) { + $rowItems = $Items | Select-Object -Skip ($r * $perRow) -First $perRow + if (-not $rowItems -or $rowItems.Count -eq 0) { continue } + + $colWidth = [Math]::Floor($width / $rowItems.Count) + if ($colWidth -lt 12) { $colWidth = 12 } + + [Console]::SetCursorPosition(0, $footerTop + $r) + foreach ($it in $rowItems) { + $key = [string]$it.Key + $label = [string]$it.Label + + # Build column, but render in two parts: + # [ key ] label.... (all on footer bg, except key block) + $colInnerMax = $colWidth + + # Key chunk (ensure it fits) + $keyChunk = " $key" + if ($keyChunk.Length -gt 4) { $keyChunk = $keyChunk.Substring(0, 4) } + $keyChunk = $keyChunk.PadRight(4) + + # Remaining space for " label" + $remaining = $colInnerMax - 4 + if ($remaining -lt 1) { $remaining = 1 } + + $labelChunk = (" " + $label) + if ($labelChunk.Length -gt $remaining) { + $labelChunk = $labelChunk.Substring(0, $remaining) + } + $labelChunk = $labelChunk.PadRight($remaining) + + # Render + Write-Host $keyChunk -NoNewline -ForegroundColor $state.Settings.BackgroundColor -BackgroundColor $state.Settings.ForegroundColor + Write-Host $labelChunk -NoNewline -ForegroundColor $state.Settings.ForegroundColor -BackgroundColor $state.Settings.BackgroundColor + } + + # Finish line, and make sure the whole line is footer background + Write-Host "" -ForegroundColor $state.Settings.ForegroundColor -BackgroundColor $state.Settings.BackgroundColor + } + + # Reset colors after footer + Write-Host "" -NoNewline +} + +function Invoke-PSNotesFooterCombo { + <# + Maps '^X' tokens to your footer actions. + Returns $true if handled (meaning caller should continue loop), else $false. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] $State, + [Parameter(Mandatory)] [string] $Combo + ) + + switch ($Combo) { + '^M' { $State.Mode = $State.Settings.Main; return $true } + '^A' { $State.Mode = [PSNoteMenuItem]::AllCatalogs; return $true } + '^S' { $State.Mode = [PSNoteMenuItem]::Search; return $true } + '^G' { $State.Mode = [PSNoteMenuItem]::Catalogs; return $true } + '^T' { $State.Mode = [PSNoteMenuItem]::Tags; return $true } + '^H' { $State.Mode = [PSNoteMenuItem]::Help; return $true } + '^F' { $State.Mode = [PSNoteMenuItem]::Favorites; return $true } + '^O' { $State.Mode = [PSNoteMenuItem]::Settings; return $true } + '^Q' { $State.Mode = [PSNoteMenuItem]::Quit; return $true } + '^C' { $State.Mode = [PSNoteMenuItem]::Quit; return $true } + '^N' { $State.Mode = [PSNoteMenuItem]::NewNote; return $true } + default { return $false } + } +} \ No newline at end of file diff --git a/src/Private/Console/Pop-PSNotesMode.ps1 b/src/Private/Console/Pop-PSNotesMode.ps1 new file mode 100644 index 0000000..c62bcb5 --- /dev/null +++ b/src/Private/Console/Pop-PSNotesMode.ps1 @@ -0,0 +1,13 @@ +function Pop-PSNotesMode { + [CmdletBinding()] + param([Parameter(Mandatory)]$State) + + if ($State.ReturnMode.Count -ge 2) { + $State.Mode = $State.ReturnMode[-2] + $State.ReturnMode = $State.ReturnMode[0..($State.ReturnMode.Count - 2)] + } + else { + $State.Mode = $State.Settings.Main + $State.ReturnMode = @() + } +} diff --git a/src/Private/Console/Read-PSNotesCommand.ps1 b/src/Private/Console/Read-PSNotesCommand.ps1 new file mode 100644 index 0000000..d4ca8c6 --- /dev/null +++ b/src/Private/Console/Read-PSNotesCommand.ps1 @@ -0,0 +1,78 @@ +function Read-PSNotesCommand { + <# + Reads input like a tiny line editor, BUT fires Ctrl+Letter immediately. + + Returns: + - '^N' for Ctrl+N (etc) + - 'ESC' for Escape + - Otherwise: the typed line when Enter is pressed (may be empty) + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param( + [string] $Prompt = 'PSNotes', + [int] $PromptRow = -1, # -1 means "bottom line" + [switch] $Echo # if set, it will display typed characters + ) + + $buffer = [System.Text.StringBuilder]::new() + + # Pick a row (bottom line by default) + $row = if ($PromptRow -ge 0) { $PromptRow } else { [Console]::WindowHeight - 2 } + #$colPrompt = 0 + + function Write-Prompt { + param( + [string] $text, + [string] $Prompt + ) + $width = [Console]::WindowWidth + [Console]::SetCursorPosition(0, $row) + + $line = "{0}: {1}" -f $Prompt, $text + if ($line.Length -gt $width) { $line = $line.Substring(0, $width) } + + # Clear line and redraw + Write-Host ($line.PadRight($width)) -NoNewline + # Put cursor at end of typed text + $cursorCol = [Math]::Min($width - 1, ("$($Prompt): ").Length + $text.Length) + [Console]::SetCursorPosition($cursorCol, $row) + } + + if ($Echo) { Write-Prompt -text '' -Prompt $Prompt } + + while ($true) { + $key = [Console]::ReadKey($true) + + # Ctrl+ => return token immediately + if ($key.Modifiers -band [ConsoleModifiers]::Control) { + # normalize to uppercase letter keys + if ($key.Key -match '^[A-Z]$') { + return '^' + $key.Key.ToString().ToUpperInvariant() + } + continue + } + + switch ($key.Key) { + 'Enter' { + $text = $buffer.ToString() + if ($Echo) { Write-Prompt -text $text -Prompt $Prompt; Write-Host "" } + return $text + } + 'Escape' { return 'ESC' } + 'Backspace' { + if ($buffer.Length -gt 0) { + [void]$buffer.Remove($buffer.Length - 1, 1) + if ($Echo) { Write-Prompt -text $buffer.ToString() -Prompt $Prompt } + } + } + default { + # Only add printable chars + if ($key.KeyChar -and -not [char]::IsControl($key.KeyChar)) { + [void]$buffer.Append($key.KeyChar) + if ($Echo) { Write-Prompt -text $buffer.ToString() -Prompt $Prompt } + } + } + } + } +} \ No newline at end of file diff --git a/src/Private/Console/Start-PSNote.Help.ps1 b/src/Private/Console/Start-PSNote.Help.ps1 new file mode 100644 index 0000000..1831a93 --- /dev/null +++ b/src/Private/Console/Start-PSNote.Help.ps1 @@ -0,0 +1,30 @@ +function Write-PSNoteWelcome{ + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param() +$welcomeMessage = @" +PSNotes is your personal command-and-snippet vault. +Store the things you run all the time (or forget just often enough), +organize them with catalogs and tags, then quickly preview, copy, or run them. + +HOW TO DRIVE THE UI + • Bottom bar shortcuts: press Ctrl+[Key] (shown as ^Key), or type the Key and press Enter. + Example: ^F (Ctrl+F) opens Favorites, or type F then press Enter. + • Items shown in [square brackets] use Key + Enter. + Example: [P] Preview means type P then press Enter. + +SEARCH ANYTIME + • Type a search term at any prompt and press Enter to search across your entire library. + • Search works no matter what view you’re in (Favorites, Catalogs, Tags, etc.). + +TIP + • Use number selections (e.g. 1, 2, 3…) to open the item shown in the list. + +HIDE THIS SCREEN + • You can suppress this welcome message in the future + by changing the Default Screen in Settings (Ctrl+O). +"@ + +Write-Host $welcomeMessage + +} \ No newline at end of file diff --git a/src/Private/Console/Start-PSNote.Helpers.ps1 b/src/Private/Console/Start-PSNote.Helpers.ps1 new file mode 100644 index 0000000..86b5372 --- /dev/null +++ b/src/Private/Console/Start-PSNote.Helpers.ps1 @@ -0,0 +1,454 @@ +# ----------------------------- +# State setters +# ----------------------------- + +function Set-PSNotesCatalogScope { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + param($State, [NoteCatalog]$Catalog) + + $State.Catalog = $Catalog + $State.ScopeNotes = @($Catalog.Notes) + $State.LastList = @($State.ScopeNotes | Sort-Object Alias) + $State.Mode = [PSNoteMenuItem]::NoteList +} + +function Set-PSNotesTagScope { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + param($State, [string]$Tag, [PSNote[]]$AllNotes) + + $State.Tag = $Tag + $State.ScopeNotes = @($AllNotes | Where-Object { $_.Tags -contains $Tag }) + $State.LastList = @($State.ScopeNotes | Sort-Object Catalog, Alias) + $State.Mode = [PSNoteMenuItem]::NoteList +} + +function Invoke-PSNotesSearch { + param($State, [string]$Prefill) + + $term = if ($Prefill) { $Prefill } else { (Read-Host "Search").Trim() } + if ([string]::IsNullOrWhiteSpace($term)) { return } + + $rx = [regex]::new([regex]::Escape($term), 'IgnoreCase') + + $results = @( + $State.ScopeNotes | Where-Object { + $rx.IsMatch($_.Alias) -or + $rx.IsMatch($_.Note) -or + $rx.IsMatch($_.Details) -or + $rx.IsMatch($_.Snippet) -or + ($_.Tags -and ($_.Tags | Where-Object { $rx.IsMatch($_) })) + } | Sort-Object Catalog, Alias + ) + + $State.LastList = $results + $State.Mode = [PSNoteMenuItem]::NoteList +} + +# ----------------------------- +# Rendering helpers +# ----------------------------- + +function Write-PSNotesHeaderBar { + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param( + [Parameter(Mandatory)] + $State, + + [string] $Title = 'PSNOTES SNIPPET LIBRARY' + ) + $scopeText = Get-PSNotesScopeText -State $state + $width = $Host.UI.RawUI.WindowSize.Width + if ( $width -lt 78) { $width = 78 } + $time = (Get-Date).ToString('HH:mm') + + # Line 1: Title + time (right aligned) + $left = " $Title" + $right = $time + $pad = $width - ($left.Length + $right.Length) + if ($pad -lt 1) { $pad = 1 } + Write-Host ($left + (' ' * $pad) + $right) -ForegroundColor $state.Settings.ForegroundColor -BackgroundColor $state.Settings.BackgroundColor + + # Line 2: Scope (left), padded + $pad = [math]::Floor(($width - $ScopeText.Length) / 2) + if (($width - $ScopeText.Length) % 2 -ne 0) { + $pad-- + } + Write-Host (" " * $pad) -NoNewline + Write-Host $ScopeText -ForegroundColor $state.Settings.ForegroundColor -BackgroundColor $state.Settings.BackgroundColor -NoNewline + Write-Host (" " * $pad) + + Write-Host "" + Write-Host "" + <# Line 3: Stats + $catalogCount = $Store.Catalogs.Count + $noteCount = $Store.Notes.Count + + # If you implemented Store.Metadata or similar, this is fastest: + $favoriteCount = + if ($Store.PSObject.Properties.Name -contains 'Metadata' -and $Store.Metadata) { $Store.Metadata.Favorites.Count } + else { 0 } + + $stats = " Catalogs: $catalogCount Notes: $noteCount Favorites: $favoriteCount" + Write-Host ($stats.PadRight($width)) -ForegroundColor $state.Settings.ForegroundColor -BackgroundColor $state.Settings.BackgroundColor + #> +} + +function Get-PSNotesScopeText { + param($State) + + $c = 'All' + $t = 'All' + if ($State.Catalog) { + $c = $State.Catalog.Catalog + } + if ($State.Tag) { + $t = 'Tag:' + $State.Tag + } + + + $r = "Catalog: $c | Tag: $t" + $r = "$($State.Mode.ToString())" + return $r +} + +function Write-PSNotesFavoriteList { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param( + [Parameter(Mandatory)][NoteStore] $Store + ) + + $favorites = @($Store.GetFavorites() | Sort-Object Catalog, Alias) + + if (-not $favorites -or $favorites.Count -eq 0) { + Write-Host "No favorites yet." -ForegroundColor DarkYellow + Write-Host "" + } + else { + # show list + $max = [Math]::Min(30, $favorites.Count) + $notes = for ($i = 0; $i -lt $max; $i++) { + $n = $favorites[$i] + $label = "[$($n.Note)] $($n.Alias)" + "{0,2}) {1}" -f ($i + 1), $label | Write-Host + $n + } + + if ($favorites.Count -gt $max) { + Write-Host "" + Write-Host ("Showing first {0} of {1}. (Favorites)" -f $max, $favorites.Count) -ForegroundColor DarkGray + } + } + return $notes +} + +function Write-PSNotesAllCatalogsList { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param( + [Parameter(Mandatory)][NoteStore] $Store + ) + + $allNotes = @($Store.Notes | Sort-Object Catalog, Alias) + + if (-not $allNotes -or $allNotes.Count -eq 0) { + Write-Host "No notes available." -ForegroundColor DarkYellow + Write-Host "" + } + else { + # show list + $max = [Math]::Min(30, $allNotes.Count) + $notes = for ($i = 0; $i -lt $max; $i++) { + $n = $allNotes[$i] + $label = "[$($n.Note)] $($n.Alias)" + "{0,2}) {1}" -f ($i + 1), $label | Write-Host + $n + } + + if ($allNotes.Count -gt $max) { + Write-Host "" + Write-Host ("Showing first {0} of {1}. (All Notes)" -f $max, $allNotes.Count) -ForegroundColor DarkGray + } + } + return $notes +} +function Write-PSNotesCatalogList { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param([NoteStore]$Store) + + $cats = @($Store.Catalogs | Sort-Object Catalog) + Write-Host "Catalogs" -ForegroundColor Cyan + Write-Host "" + + for ($i = 0; $i -lt $cats.Count; $i++) { + $c = $cats[$i] + $count = @($c.Notes).Count + "{0,2}) {1} ({2} notes)" -f ($i + 1), $c.Catalog, $count | Write-Host + } +} + +function Get-PSNotesTag { + param([PSNote[]]$Notes) + + @( + $Notes | + Where-Object { $_.Tags } | + ForEach-Object { $_.Tags } | + ForEach-Object { $_ } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Sort-Object -Unique + ) +} + +function Write-PSNotesTagList { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param([PSNote[]]$Notes) + + $tags = Get-PSNotesTag -Notes $Notes + Write-Host "Tags" -ForegroundColor Cyan + Write-Host "" + + for ($i = 0; $i -lt $tags.Count; $i++) { + "{0,2}) {1}" -f ($i + 1), $tags[$i] | Write-Host + } +} + +function Write-PSNotesNoteList { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param( + [PSNote[]]$Notes, + [string]$Title, + [Parameter(Mandatory)][NoteStore] $Store + ) + + Write-Host $Title -ForegroundColor Cyan + Write-Host "" + + if (-not $Notes -or $Notes.Count -eq 0) { + Write-Host "No notes found." -ForegroundColor DarkYellow + return + } + + # Simple list; easy to add paging later + $max = [Math]::Min(30, $Notes.Count) + for ($i = 0; $i -lt $max; $i++) { + $n = $Notes[$i] + $star = if ($Store.IsFavorite($n)) { '*' } else { ' ' } + "{0,2}){1} [{2}] {3}" -f ($i + 1), $star, $n.Note, $n.Alias | Write-Host + } + + if ($Notes.Count -gt $max) { + Write-Host "" + Write-Host ("Showing first {0} of {1}. Refine search to narrow results." -f $max, $Notes.Count) -ForegroundColor DarkGray + } +} + +function Write-PSNotePreview { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param([PSNote]$Note) + + if (-not $Note) { return } + + Write-Host "Catalog : " -NoNewline + Write-Host $Note.Catalog -ForegroundColor Cyan + Write-Host "Alias : " -NoNewline + Write-Host $Note.Alias -ForegroundColor Cyan + if ($Note.Note) { Write-Host ("Title : {0}" -f $Note.Note) -ForegroundColor Gray } + if ($Note.Tags) { Write-Host ("Tags : {0}" -f ($Note.Tags -join ', ')) -ForegroundColor DarkGray } + + if ($Note.Details) { Write-Host $Note.Details -ForegroundColor Gray } + Write-Host "" + Write-Host "Snippet:" -ForegroundColor Yellow + Write-Host $Note.Snippet +} + +# ----------------------------- +# Copy / Execute (prefer existing cmdlets) +# ----------------------------- + +function Invoke-PSNotesExecution { + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '')] + param([PSNote]$Note) + + if (-not $Note) { return } + + #$invokeCmd = Get-Command -Name Invoke-PSNote -ErrorAction SilentlyContinue + #if ($invokeCmd) { + # Invoke-PSNote -Alias $Note.Alias + # return + #} + + Invoke-Expression -Command $Note.Snippet +} + +function Invoke-PSNotesNewNoteWizard { + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + param( + [Parameter(Mandatory)] + $Store + ) + + Clear-Host + Write-Host "PSNotes - New Note Wizard" -ForegroundColor Yellow + Write-Host "----------------------------------------" -ForegroundColor DarkGray + Write-Host "" + + # --- Catalog selection --- + $catalogs = @($Store.Catalogs | Sort-Object Catalog) + + Write-Host "Select a catalog:" -ForegroundColor Cyan + + for ($c = 0; $c -lt $catalogs.Count; $c++) { + Write-Host (" {0}) {1}" -f ($c + 1), $catalogs[$c].Catalog) + } + + Write-Host "Enter a number (1..$($catalogs.Count)). To create a new catalog, enter a name instead of a number." -ForegroundColor DarkGray + + $catalogName = $null + while ([string]::IsNullOrWhiteSpace($catalogName)) { + $catSel = (Read-Host "Catalog").Trim() + + if ($catSel -match '^\d+$') { + $idx = [int]$catSel + + if ($idx -ge 1 -and $idx -le $catalogs.Count) { + $catalogName = $catalogs[$idx - 1].Catalog + } + else { + Write-Host "Invalid catalog selection." -ForegroundColor Red + } + } + elseif (-not [string]::IsNullOrWhiteSpace($catSel)) { + $newCat = $catSel + # If it already exists (case-insensitive), just use existing + $existing = $catalogs | Where-Object { $_.Catalog -ieq $newCat } | Select-Object -First 1 + $catalogName = if ($existing) { $existing.Catalog } else { $newCat } + } + + if ([string]::IsNullOrWhiteSpace($catalogName)) { + Write-Host "Invalid catalog selection." -ForegroundColor Red + } + } + + Write-Host "" + Write-Host ("Catalog: {0}" -f $catalogName) -ForegroundColor DarkYellow + Write-Host "" + + # --- Note name --- + $noteName = $null + while ([string]::IsNullOrWhiteSpace($noteName)) { + $noteName = (Read-Host "Note name").Trim() + if ([string]::IsNullOrWhiteSpace($noteName)) { + Write-Host "Note name is required." -ForegroundColor Red + } + } + + # --- Kind (Snippet vs Script) --- + Write-Host "" + Write-Host "Note type:" -ForegroundColor Cyan + Write-Host " 1) Snippet (inline code)" -ForegroundColor Gray + Write-Host " 2) Script (path to .ps1)" -ForegroundColor Gray + + $kind = $null + while ($kind -notin 'Snippet', 'Script') { + $k = (Read-Host "Type #").Trim() + switch ($k) { + '1' { $kind = 'Snippet' } + '2' { $kind = 'Script' } + default { Write-Host "Choose 1 or 2." -ForegroundColor Red } + } + } + + # --- Snippet or ScriptPath --- + $snippet = $null + $scriptPath = $null + + if ($kind -eq 'Snippet') { + Write-Host "" + Write-Host "Enter snippet text. (Tip: you can paste multi-line text.)" -ForegroundColor Cyan + $snippet = Read-Host "Snippet" + if ([string]::IsNullOrWhiteSpace($snippet)) { + Write-Host "Snippet cannot be empty." -ForegroundColor Red + Read-Host "Press Enter to cancel" + return + } + } + else { + Write-Host "" + $scriptPath = (Read-Host "Script path (.ps1)").Trim() + if ([string]::IsNullOrWhiteSpace($scriptPath)) { + Write-Host "Script path cannot be empty." -ForegroundColor Red + Read-Host "Press Enter to cancel" + return + } + + if (-not (Test-Path -Path $scriptPath)) { + Write-Host ("Script file not found: {0}" -f $scriptPath) -ForegroundColor Red + Read-Host "Press Enter to cancel" + return + } + } + + # --- Optional alias --- + Write-Host "" + $alias = (Read-Host "Alias (optional; leave blank for none)").Trim() + if ($alias -and -not $alias -match '^[a-zA-Z0-9_-]+$') { + Write-Host "Alias can only contain letters, numbers, dashes, and underscores." -ForegroundColor Red + Read-Host "Press Enter to cancel" + return + } + + # --- Optional tags --- + Write-Host "" + do { + $tagInput = (Read-Host "Tags (optional; comma-separated) enter 'L' to list current tags").Trim() + if ($tagInput -eq 'L') { + Write-Host "$($Store.Notes | ForEach-Object { $_.Tags } | Where-Object { $_ } | Sort-Object -Unique | Select-Object @{l='Tag';e={$_}} | Format-Wide -AutoSize | Out-String)" -ForegroundColor DarkGray + } + } while ($tagInput -eq 'L') + $tags = @() + if (-not [string]::IsNullOrWhiteSpace($tagInput)) { + $tags = @( + $tagInput -split ',' | + ForEach-Object { $_.Trim() } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Select-Object -Unique + ) + } + + Write-Host "" + Write-Host "Confirm note:" -ForegroundColor Green + Write-Host (" Catalog : {0}" -f $catalogName) + Write-Host (" Note : {0}" -f $noteName) + Write-Host (" Alias : {0}" -f $alias) + if ($tags.Count -gt 0) { + Write-Host (" Tags : {0}" -f ($tags -join ', ')) + } + Write-Host "" + $confirm = Read-Host "Press Enter to create the note, or 'C' to cancel" + if ($confirm -eq 'C') { + Write-Host "Note creation cancelled." -ForegroundColor Yellow + return + } + # --- Create note --- + try { + if ($kind -eq 'Snippet') { + New-PSNote -Note $noteName -Snippet $snippet -Catalog $catalogName -Alias $alias -Tags $tags | Out-Null + } + else { + New-PSNote -Note $noteName -ScriptPath $scriptPath -Catalog $catalogName -Alias $alias -Tags $tags | Out-Null + } + } + catch { + Write-Host "" + Write-Host ("Failed to create note: {0}" -f $_.Exception.Message) -ForegroundColor Red + Write-Host "" + Read-Host "Press Enter to return to PSNotes" + } +} + +function Get-PSNotesViewportRow { + param([int]$FromBottom = 0) # 0 = very bottom visible row + return [Console]::WindowTop + [Console]::WindowHeight - 1 - $FromBottom +} diff --git a/src/Private/Console/Update-PSNoteSetting.ps1 b/src/Private/Console/Update-PSNoteSetting.ps1 new file mode 100644 index 0000000..faa7fde --- /dev/null +++ b/src/Private/Console/Update-PSNoteSetting.ps1 @@ -0,0 +1,235 @@ +function Update-PSNoteSetting { + <# + .SYNOPSIS + Guided settings editor for NoteConfigStore. + + .DESCRIPTION + Presents a simple Read-Host driven menu that lets the user update values on a + NoteConfigStore instance (Main, ExitOnCopy, ForegroundColor, BackgroundColor), + then optionally save changes back to disk. + + .PARAMETER Config + Optional existing NoteConfigStore instance. If not provided, a new one is created. + + .PARAMETER NoSave + Do not persist changes automatically. (Still allows user to choose Save in the menu.) + + .OUTPUTS + NoteConfigStore (the updated instance) + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + param( + [Parameter()] + [NoteConfigStore] $Config, + + [switch] $NoSave + ) + + if (-not $Config) { + $Config = [NoteConfigStore]::new() + } + + function Write-SettingsHeader { + param([NoteConfigStore] $Cfg) + + Clear-Host + Write-Host "PSNotes Settings" -ForegroundColor Yellow + Write-Host ("=" * 60) + Write-Host (" 1) Default Screen (Main) : {0}" -f $Cfg.Main) + Write-Host (" 2) Exit On Copy : {0}" -f $Cfg.ExitOnCopy) + Write-Host (" 3) Foreground Color : {0}" -f $Cfg.ForegroundColor) + Write-Host (" 4) Background Color : {0}" -f $Cfg.BackgroundColor) + Write-Host "" + Write-Host " S) Save" + Write-Host " R) Reset to defaults" + Write-Host " Q) Quit" + Write-Host "" + } + + function Read-Choice { + param([string]$Prompt) + return (Read-Host $Prompt).Trim() + } + + function Select-EnumValue { + param( + [Parameter(Mandatory)] [Type] $EnumType, + [Parameter(Mandatory)] [string] $Title, + [Parameter(Mandatory)] [string] $CurrentValue + ) + + $values = [Enum]::GetNames($EnumType) + + while ($true) { + Clear-Host + Write-Host $Title -ForegroundColor Yellow + Write-Host ("Current: {0}" -f $CurrentValue) + Write-Host ("-" * 60) + + for ($i = 0; $i -lt $values.Count; $i++) { + Write-Host (" {0,2}) {1}" -f ($i + 1), $values[$i]) + } + + Write-Host "" + Write-Host " B) Back" + Write-Host "" + + $sel = (Read-Host "Select number (or B)").Trim() + + if ($sel -match '^(B|b)$') { return $null } + + $n = 0 + if ([int]::TryParse($sel, [ref]$n)) { + if ($n -ge 1 -and $n -le $values.Count) { + return $values[$n - 1] + } + } + } + } + + function Select-ConsoleColor { + param( + [string] $Title, + [ConsoleColor] $Current + ) + $picked = Select-EnumValue -EnumType ([ConsoleColor]) -Title $Title -CurrentValue $Current.ToString() + if ($null -eq $picked) { return $null } + + $try = [ConsoleColor]::Black + if ([Enum]::TryParse([string]$picked, [ref]$try)) { + return $try + } + return $null + } + + function Select-PSNoteMenuItem { + param( + [string] $Title, + [PSNoteMenuItem] $Current + ) + $picked = Select-EnumValue -EnumType ([PSNoteMenuItem]) -Title $Title -CurrentValue $Current.ToString() + if ($null -eq $picked) { return $null } + + $try = [PSNoteMenuItem]::Welcome + if ([Enum]::TryParse([string]$picked, [ref]$try)) { + return $try + } + return $null + } + + function Read-Bool { + param( + [string] $Title, + [bool] $Current + ) + + while ($true) { + Clear-Host + Write-Host $Title -ForegroundColor Yellow + Write-Host ("Current: {0}" -f $Current) + Write-Host "" + Write-Host " Y) True" + Write-Host " N) False" + Write-Host " B) Back" + Write-Host "" + + $sel = (Read-Host "Choose (Y/N/B)").Trim() + + switch -Regex ($sel) { + '^(B|b)$' { return $null } + '^(Y|y)$' { return $true } + '^(N|n)$' { return $false } + } + } + } + + $dirty = $false + $run = $true + while ($run) { + Write-SettingsHeader -Cfg $Config + + if ($dirty) { + Write-Host "Unsaved changes." -ForegroundColor Yellow + Write-Host "" + } + + $choice = Read-Choice -Prompt "Select option" + + switch -Regex ($choice) { + '^(1)$' { + $new = Select-PSNoteMenuItem -Title "Default Screen (Main)" -Current $Config.Main + if ($null -ne $new -and $new -ne $Config.Main) { + $Config.Main = $new + $dirty = $true + } + } + '^(2)$' { + $new = Read-Bool -Title "Exit On Copy" -Current $Config.ExitOnCopy + if ($null -ne $new -and $new -ne $Config.ExitOnCopy) { + $Config.ExitOnCopy = [bool]$new + $dirty = $true + } + } + '^(3)$' { + $new = Select-ConsoleColor -Title "Foreground Color" -Current $Config.ForegroundColor + if ($null -ne $new -and $new -ne $Config.ForegroundColor) { + $Config.ForegroundColor = $new + $dirty = $true + } + } + '^(4)$' { + $new = Select-ConsoleColor -Title "Background Color" -Current $Config.BackgroundColor + if ($null -ne $new -and $new -ne $Config.BackgroundColor) { + $Config.BackgroundColor = $new + $dirty = $true + } + } + '^(S|s)$' { + if (-not $NoSave) { + $Config.Save() + $dirty = $false + Write-Host "`nSaved to: $($Config.Path)" -ForegroundColor Green + } + else { + $Config.Save() + $dirty = $false + Write-Host "`nSaved to: $($Config.Path)" -ForegroundColor Green + } + Start-Sleep -Milliseconds 650 + } + '^(R|r)$' { + $confirm = Read-Choice -Prompt "Reset to defaults? (Y/N)" + if ($confirm -match '^(Y|y)$') { + $Config.SetDefaults() + $dirty = $true + } + } + '^(Q|q)$' { + if ($dirty) { + $confirm = Read-Choice -Prompt "You have unsaved changes. Save before quitting? (Y/N)" + if ($confirm -match '^(Y|y)$') { + $Config.Save() + $dirty = $false + } + } + $run = $false + } + default { + # If user typed something like "welcome" for Main, accept it as a convenience + if (-not [string]::IsNullOrWhiteSpace($choice)) { + $tryMain = [PSNoteMenuItem]::Welcome + if ([Enum]::TryParse($choice, [ref]$tryMain)) { + if ($tryMain -ne $Config.Main) { + $Config.Main = $tryMain + $dirty = $true + } + } + } + } + } + } + + return $Config +} diff --git a/src/Private/Import-PSNoteCatalog.ps1 b/src/Private/Import-PSNoteCatalog.ps1 new file mode 100644 index 0000000..50f52d7 --- /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.Note)" + "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.Note)" + $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.Note, $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/Public/Invoke-PSNote.ps1 b/src/Private/Invoke-PSNote.ps1 similarity index 54% rename from Public/Invoke-PSNote.ps1 rename to src/Private/Invoke-PSNote.ps1 index b23270b..a07e90a 100644 --- a/Public/Invoke-PSNote.ps1 +++ b/src/Private/Invoke-PSNote.ps1 @@ -1,4 +1,4 @@ -Function Invoke-PSNote{ +Function Invoke-PSNote { <# .SYNOPSIS Use to display a list of notes in a selectable menu so you can choose which to run @@ -50,22 +50,33 @@ Function Invoke-PSNote{ .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 + [cmdletbinding(DefaultParameterSetName = "Note")] + param( + [parameter(Mandatory = $true, ParameterSetName = "Note", Position = 0)] + [PSNote]$Note ) - - $NoteSelection = @(Get-PSNote @PSBoundParameters) - $noteSnippet = Write-NoteSnippet $NoteSelection - - if(-not [string]::IsNullOrEmpty($noteSnippet)){ - $ScriptBlock = $executioncontext.invokecommand.NewScriptBlock($noteSnippet) - Invoke-Command -ScriptBlock $ScriptBlock + Test-PSNotesInitalize + + switch ($Note.Kind) { + Script { + if ([string]::IsNullOrWhiteSpace($Note.Snippet)) { + throw "Cannot invoke Script note '$($Note.Note)': Path is empty." + } + if (-not (Test-Path -LiteralPath $Note.Snippet)) { + throw "Cannot invoke Script note '$($Note.Note)': Script file not found at path: $($Note.Snippet)" + } + & $Note.Snippet + } + Snippet { + if ([string]::IsNullOrWhiteSpace($Note.Snippet)) { + throw "Cannot invoke Snippet note '$($Note.Note)': Snippet is empty." + } + $scriptBlock = [ScriptBlock]::Create($Note.Snippet) + Invoke-Command -ScriptBlock $scriptBlock + } + default { + throw "Cannot invoke note '$($Note.Note)': 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/Console/Start-PSNote.ps1 b/src/Public/Console/Start-PSNote.ps1 new file mode 100644 index 0000000..8fb0c48 --- /dev/null +++ b/src/Public/Console/Start-PSNote.ps1 @@ -0,0 +1,196 @@ +function Start-PSNote { + <# + .SYNOPSIS + Launch the PSNotes interactive UI + + .DESCRIPTION + Starts the interactive PSNotes terminal UI for browsing, searching, + previewing, copying, and executing notes across catalogs, tags, + and favorites. + + .EXAMPLE + Start-PSNote + + Opens the PSNotes interactive UI. + + .EXAMPLE + Initialize-PSNoteStore + Start-PSNote + + Initializes the note store and then opens the UI. + + .LINK + https://github.com/mdowst/PSNotes + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + param() + + # ---------- Init ---------- + $Store = if ($script:_noteStore -is [NoteStore]) { + $script:_noteStore + } + else { + [NoteStore]::new() + } + + # ---------- State ---------- + $state = [NoteConsoleState]::new($Store) + + # Default view: Favorites if any exist; otherwise Catalogs + $favorites = @($Store.GetFavorites()) + + + $exitUI = $false + + while (-not $exitUI) { + Clear-Host + Write-PSNotesHeaderBar -State $state + $menu = "" + $skipRead = "" + $skipUI = $false + switch -Exact ($state.Mode) { + ([PSNoteMenuItem]::Main) { Write-Host "Main Menu" -ForegroundColor Cyan } + ([PSNoteMenuItem]::Catalogs) { Write-Host "Catalog Menu" -ForegroundColor Cyan } + ([PSNoteMenuItem]::Tags) { Write-Host "Tags Menu" -ForegroundColor Cyan } + ([PSNoteMenuItem]::Favorites) { + Write-Host "Favorites Menu" -ForegroundColor Cyan + $pageIndex = 0 + $state.LastList = Write-PSNotesFavoriteList -Store $Store + } + ([PSNoteMenuItem]::Settings) { Write-Host "Settings Menu" -ForegroundColor Cyan } + ([PSNoteMenuItem]::Search) { Write-Host "Search Menu" -ForegroundColor Cyan } + ([PSNoteMenuItem]::Help) { Write-Host "Help Menu" -ForegroundColor Cyan } + ([PSNoteMenuItem]::Back) { + Pop-PSNotesMode -State $state + $skipUI = $true + } + ([PSNoteMenuItem]::Quit) { Write-Host "Quit Menu" -ForegroundColor Cyan } + ([PSNoteMenuItem]::Welcome) { Write-Host "Welcome Menu" -ForegroundColor Cyan } + ([PSNoteMenuItem]::AllCatalogs) { + Write-Host "All Catalogs Menu" -ForegroundColor Cyan + $pageIndex = 0 + $state.LastList = Write-PSNotesAllCatalogsList -Store $Store + } + ([PSNoteMenuItem]::Preview) { + Write-Host "Preview Menu" -ForegroundColor Cyan + $note = $state.LastList[$pageIndex] + $f = if ($Store.IsFavorite($note)) { '[U] UnFav' } else { '[U] Fav' } + Write-PSNotePreview -Note $note + $menu = "[C] Copy [X] Execute $f [N] Next [P] Previous" + } + ([PSNoteMenuItem]::NewNote) { Write-Host "New Note Menu" -ForegroundColor Cyan } + ([PSNoteMenuItem]::NoteActions) { + Write-Host "Note Actions Menu" -ForegroundColor Cyan + Write-PSNotePreview -Note $state.Note + $f = if ($Store.IsFavorite($state.Note)) { '[U] UnFav' } else { '[F] Fav' } + $menu = "[C] Copy [X] Execute $f " + } + ([PSNoteMenuItem]::Quit) { + $exitUI = $true + } + } + + if (-not $exitUI -and -not $skipUI) { + if ( $state.Mode -notin [PSNoteMenuItem]::NoteActions -and $state.Note ) { + # Clear selected note when not in NoteActions + $state.Note = $null + } + + Write-PSNotesFooter -State $state -Menu $menu + + if ([string]::IsNullOrWhiteSpace($skipRead)) { + $sel = Read-PSNotesCommand -Prompt 'PSNotes' -Echo + } + else { + $sel = $skipRead + } + + if ($state.ReturnMode[-1] -ne $state.Mode) { + $state.ReturnMode += $state.Mode + } + + # Keep your existing Preview default-next behavior + if ([string]::IsNullOrWhiteSpace($sel) -and $state.Mode -eq [PSNoteMenuItem]::Preview) { + $sel = 'N' + } + if($state.Mode -eq [PSNoteMenuItem]::Preview -and $sel -match '^\s*([Nn])\s*$') { + $pageIndex++ + continue + } + + # Ctrl combos fire immediately + $fromKey = [PSNoteMenuItem]::FromKey($sel) + if ($fromKey) { + $state.Mode = $fromKey + continue + } + + # Optional: ESC behaves like Back + if ($sel -eq 'ESC') { + $state.Mode = [PSNoteMenuItem]::Quit + continue + } + + + + if ($sel -match '^\s*O?\s*(\d+)\s*$') { + $idx = [int]$Matches[1] - 1 + switch -Exact ($state.Mode) { + ([PSNoteMenuItem]::Catalogs) { + $cats = @($Store.Catalogs | Sort-Object Catalog) + if ($idx -ge 0 -and $idx -lt $cats.Count) { + Set-PSNotesCatalogScope -State $state -Catalog $cats[$idx] + } + } + ([PSNoteMenuItem]::Tags) { + $tags = Get-PSNotesTag -Notes @($Store.Notes) + if ($idx -ge 0 -and $idx -lt $tags.Count) { + Set-PSNotesTagScope -State $state -Tag $tags[$idx] -AllNotes @($Store.Notes) + } + } + ([PSNoteMenuItem]::Favorites) { + $pageIndex = 0 + $notes = $state.LastList + if ($idx -ge 0 -and $idx -lt $max) { + $state.Note = $favorites[$idx] + $state.Mode = [PSNoteMenuItem]::NoteActions + } + } + ([PSNoteMenuItem]::NoteList) { + $pageIndex = 0 + $notes = $state.LastList + if ($idx -ge 0 -and $idx -lt $notes.Count) { + $state.Note = $notes[$idx] + $state.Mode = [PSNoteMenuItem]::NoteActions + } + } + ([PSNoteMenuItem]::AllCatalogs) { + $pageIndex = 0 + $notes = $state.LastList + if ($idx -ge 0 -and $idx -lt $notes.Count) { + $state.Note = $notes[$idx] + $state.Mode = [PSNoteMenuItem]::NoteActions + } + } + } + } + # Free-text search from main menu + elseif (-not [string]::IsNullOrWhiteSpace($sel)) { + Invoke-PSNotesSearch -State $state -Prefill $sel + } + } + } + + # UI is fully exited at this point + Clear-Host + + if ($state.ExecuteOnExit) { + Write-Host "" + Write-Host ("Executing: {0}" -f $state.Note.Alias) -ForegroundColor Yellow + Start-Sleep -Milliseconds 300 + Invoke-PSNotesExecution -Note $state.ExecuteOnExit + } +} \ 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..5bef4ba --- /dev/null +++ b/src/Public/Export-PSNote.ps1 @@ -0,0 +1,91 @@ +Function Export-PSNote { + <# + .SYNOPSIS + Export PSNotes to a JSON file + + .DESCRIPTION + Exports PSNotes to a JSON file for sharing or importing on another machine. + You can export notes by catalog or by piping PSNote objects into this command. + + .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 -Note '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. + + + + .LINK + https://github.com/mdowst/PSNotes + #> + [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..7b8a769 --- /dev/null +++ b/src/Public/Get-PSNote.ps1 @@ -0,0 +1,153 @@ +Function Get-PSNote{ + <# + .SYNOPSIS + Search for or list PSNotes + + .DESCRIPTION + Searches notes by name, tag, or text across all note properties. You can also + filter by catalog and optionally copy or run the returned snippet. + + .PARAMETER Note + The note name to return. Accepts wildcards. + + .PARAMETER Tag + Return notes that contain the specified tag. + + .PARAMETER Catalog + Filter notes by catalog name. Accepts wildcards and multiple values. + + .PARAMETER Copy + Copy the selected snippet to the clipboard. + + .PARAMETER Run + Run the selected snippet via Invoke-PSNote. + + .PARAMETER SearchString + Search for text in the note's name, details, snippet, alias, or tags. + + .EXAMPLE + Get-PSNote + + Returns all notes. + + .EXAMPLE + Get-PSNote -Note 'Creds' + + Returns the note named 'Creds'. + + .EXAMPLE + Get-PSNote -Note 'Cred*' + + Returns all notes with names that start with 'Cred'. + + .EXAMPLE + Get-PSNote -Tag 'AD' + + Returns all notes with the tag 'AD'. + + .EXAMPLE + Get-PSNote -Note '*User*' -Tag 'AD' + + Returns notes with 'User' in the name and the tag 'AD'. + + .EXAMPLE + Get-PSNote -SearchString 'day' + + Returns notes where 'day' appears in the name, details, snippet, alias, or tags. + + .EXAMPLE + Get-PSNote -Catalog 'Default' + + Returns all notes in the Default catalog. + + .EXAMPLE + Get-PSNote -SearchString 'day' -Catalog 'Work*','Personal*' + + Searches only within the matching catalogs. + + .EXAMPLE + Get-PSNote -Note 'CpuUsage' -Copy + + Copies the snippet for the selected note to the clipboard. + + .EXAMPLE + Get-PSNote -SearchString 'token' -Run + + Runs the selected note; prompts to choose if multiple notes match. + + .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")] + [parameter(Mandatory=$false, ParameterSetName="Search")] + [switch]$Run, + [parameter(Mandatory=$false, ParameterSetName="Note")] + [parameter(Mandatory=$false, ParameterSetName="Search")] + [string[]]$Catalog, + [parameter(Mandatory=$false, ParameterSetName="Search")] + [string]$SearchString + ) + 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($SearchString){ + $returned = $notes | Where-Object { + $_.Note -like "*$SearchString*" -or + $_.Alias -like "*$SearchString*" -or + $_.Details -like "*$SearchString*" -or + $_.Snippet -like "*$SearchString*" -or + ($_.Tags | Where-Object { $_ -like "*$SearchString*" } | Select-Object -First 1) + } + } elseif($Tag){ + $returned = $notes | Where-Object{$_.Note -like $note -and $_.Tags -contains $Tag} + } else { + $returned = $notes | Where-Object{$_.Note -like $note} + } + + 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/Public/Get-PSNoteAlias.ps1 b/src/Public/Get-PSNoteAlias.ps1 similarity index 50% rename from Public/Get-PSNoteAlias.ps1 rename to src/Public/Get-PSNoteAlias.ps1 index d52b6fd..43d1ffa 100644 --- a/Public/Get-PSNoteAlias.ps1 +++ b/src/Public/Get-PSNoteAlias.ps1 @@ -12,7 +12,13 @@ .LINK https://github.com/mdowst/PSNotes - + .NOTES + This function is designed to be called via an alias created for each note. + The alias name matches the note name by default but can be customized when creating the note. + When invoked, it retrieves the corresponding note and either executes the snippet or copies it to the clipboard based on parameters and note settings. + - If the note is set to run by default or the -Run switch is used, it executes the snippet. + - If the -Copy switch is used, it copies the snippet to the clipboard instead of executing it. + Eventhough the function cannot be called directly, it has to be public to be accessible via the aliases. #> [cmdletbinding()] param( @@ -21,16 +27,18 @@ [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 = $noteObjects | Where-Object{$_.Alias -eq $Alias} - if($Run){ - Get-PSNote -Note $aliasObject.Note -Run + $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){ - $returned | Select-Object -First 1 -ExpandProperty Snippet | Set-Clipboard + $aliasObject | Select-Object -First 1 -ExpandProperty Snippet | Set-Clipboard } else { Write-Debug "Cmdlet 'Set-Clipboard' not found." } diff --git a/src/Public/Import-PSNote.ps1 b/src/Public/Import-PSNote.ps1 new file mode 100644 index 0000000..153361f --- /dev/null +++ b/src/Public/Import-PSNote.ps1 @@ -0,0 +1,68 @@ +Function Import-PSNote { + <# + .SYNOPSIS + Import a PSNotes JSON file + + .DESCRIPTION + Imports PSNotes from a JSON catalog file into your local note store. You can import into the default + catalog or a named catalog, and control how existing notes are handled. + + .PARAMETER Path + The path to the PSNotes JSON catalog file to import. + + .PARAMETER Catalog + The destination catalog name to import into. Defaults to 'Default'. + + .PARAMETER DefaultBehavior + Determines how to handle existing notes when conflicts are detected. + Valid values: Prompt, SkipMigratedNotes, OverwriteExistingNotes. + + .EXAMPLE + Import-PSNote -Path C:\Import\MyPSNotes.json + + Imports the contents of MyPSNotes.json into the Default catalog. + + .EXAMPLE + Import-PSNote -Path C:\Export\MyPSNotes.json -Catalog 'ADNotes' + + Imports the contents of MyPSNotes.json into the ADNotes catalog. + + .EXAMPLE + Import-PSNote -Path C:\Export\MyPSNotes.json -Catalog 'Work' -DefaultBehavior OverwriteExistingNotes + + Imports into the Work catalog and overwrites existing notes when conflicts occur. + + .LINK + https://github.com/mdowst/PSNotes + #> + [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..8e9ee40 --- /dev/null +++ b/src/Public/Initialize-PSNoteStore.ps1 @@ -0,0 +1,53 @@ +Function Initialize-PSNoteStore { + <# + .SYNOPSIS + Initialize the PSNotes store + + .DESCRIPTION + Loads all PSNotes catalogs from $env:PSNOTES_HOME into the in-memory note store + and verifies clipboard support. This is typically called internally by other + commands, but can be invoked to refresh the store after external changes. + + .EXAMPLE + Initialize-PSNoteStore + + Initializes the PSNotes store by loading catalogs from $env:PSNOTES_HOME. + + .EXAMPLE + $env:PSNOTES_HOME = 'C:\Users\Me\AppData\Roaming\PSNotes' + Initialize-PSNoteStore + + Initializes the store using a custom PSNotes home path. + + .LINK + https://github.com/mdowst/PSNotes + #> + [CmdletBinding()] + param() + + # Load all commands to noteObjects + #Initialize-PSNoteStoreRemoteJsonFile + $script:_noteStore = [NoteStore]::new() + Write-Verbose "User PSNotes Path: $env:PSNOTES_HOME" + Get-ChildItem -Path $env:PSNOTES_HOME -Filter '*.json' | Where-Object{ $_.BaseName -notin 'Default' } | ForEach-Object { + $script:_noteStore.LoadCatalog($_.BaseName) + } + $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/New-PSNote.ps1 b/src/Public/New-PSNote.ps1 new file mode 100644 index 0000000..37d79d7 --- /dev/null +++ b/src/Public/New-PSNote.ps1 @@ -0,0 +1,243 @@ +Function New-PSNote { + <# + .SYNOPSIS + Creates a new PSNote for storing and reusing code snippets or script references. + + .DESCRIPTION + Creates a new PSNote to store code snippets, script blocks, or references to script files. + PSNotes can be stored in different catalogs and tagged for easy retrieval. Each note + has an alias that can be used to quickly access it. + + If a note with the same name already exists in the specified catalog, you must supply + the Force switch to overwrite its properties. + + The note Kind is automatically set based on the parameter used: + - Snippet or ScriptBlock: Creates a note of Kind 'Snippet' (inline code) + - ScriptPath: Creates a note of Kind 'Script' (file reference) + + .PARAMETER Note + The unique name of the note to create within the specified catalog. + + .PARAMETER Snippet + The text of the code snippet to store. This is typically a single line or small block + of PowerShell code. Use this parameter for simple snippets. + + .PARAMETER ScriptBlock + A PowerShell script block containing the code to save. Enclose the commands in braces { } + to create a script block. This is useful for multi-line code with proper syntax highlighting. + + .PARAMETER ScriptPath + The file path to a PowerShell script (.ps1) file. When specified, the note will reference + this external script file and the note Kind will be set to 'Script'. The script file must + exist at the specified path. + + .PARAMETER Details + A description or additional information about the note. This helps document what the + note does and when to use it. + + .PARAMETER Alias + The alias to create for this note. If not supplied, it will use the Note name as the alias. + The alias can only contain letters, numbers, dashes (-), and underscores (_). + The alias is set as a global alias that invokes Get-PSNoteAlias. + + .PARAMETER Tags + A string array of tags to associate with the note. Tags help categorize and search for + notes. Multiple tags can be specified. + + .PARAMETER Catalog + The catalog to add the note to. Catalogs are used to organize notes into different + collections (e.g., 'Personal', 'Work', 'Team'). Defaults to 'Default'. + + .PARAMETER Run + Indicates whether the note should be executed automatically when retrieved. + When set to $true, the note will run when accessed. When set to $false (default), + the note content will be returned without execution. + + .PARAMETER Force + Forces the creation of the note even if a note with the same name already exists in + the catalog. Without this switch, an error will be thrown if the note already exists. + + .EXAMPLE + New-PSNote -Note 'GetServices' -Snippet 'Get-Service | Where-Object Status -eq Running' -Alias 'running' + + Creates a simple note with a one-line snippet. The snippet can be retrieved or executed using the alias 'running'. + + .EXAMPLE + New-PSNote -Note 'DayOfWeek' -Alias 'today' -Snippet '(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__)' -Details "Returns the abbreviated name of the current day" -Tags 'date','time' + + Creates a note with a snippet, description, and multiple tags for easy searching. + + .EXAMPLE + New-PSNote -Note 'CpuUsage' -Tags 'perf','monitoring' -Alias 'cpu' -ScriptBlock { + Get-CimInstance Win32_Processor | + Measure-Object -Property LoadPercentage -Average | + Select-Object -ExpandProperty Average + } + + Creates a note using a script block with multi-line code and a custom alias 'cpu'. + + .EXAMPLE + $MultiLineSnippet = @' + $sb = [System.Text.StringBuilder]::new() + for ($i = 1; $i -le 10; $i++) { + [void]$sb.AppendLine("Item $i") + } + $sb.ToString() + '@ + New-PSNote -Note 'StringBuilder' -Snippet $MultiLineSnippet -Details "Demonstrates StringBuilder usage" -Tags 'string','performance' + + Creates a note with a multi-line snippet stored in a here-string variable. + + .EXAMPLE + New-PSNote -Note 'GetDateIso' -Snippet 'Get-Date -Format "yyyy-MM-dd"' -Catalog 'Work' -Tags 'date','formatting' + + Creates a new note in the 'Work' catalog instead of the default catalog. + + .EXAMPLE + New-PSNote -Note 'TestConnection' -Alias 'test-conn' -Snippet 'Test-Connection -ComputerName 8.8.8.8 -Count 2 -Quiet' -Run $true -Details "Quick connectivity test" + + Creates a note that will automatically execute when retrieved (Run = $true). + + .EXAMPLE + New-PSNote -Note 'DeploymentScript' -ScriptPath 'C:\Scripts\Deploy-Application.ps1' -Details "Main deployment script for production" -Tags 'deployment','production','automation' -Catalog 'Work' + + Creates a note that references an external script file. The note Kind will be 'Script'. + The script file must exist at the specified path. + + .EXAMPLE + New-PSNote -Note 'BackupScript' -ScriptPath '\\FileServer\Scripts\Backup.ps1' -Alias 'backup' -Details "Automated backup script" -Tags 'backup','maintenance' + + Creates a note referencing a script on a network share with a custom alias. + + .EXAMPLE + New-PSNote -Note 'GetServices' -Snippet 'Get-Service | Sort-Object Status' -Force + + Updates an existing note named 'GetServices' with new snippet content using the -Force switch. + Without -Force, this would throw an error if the note already exists. + + .EXAMPLE + New-PSNote -Note 'QuickTest' -Snippet 'Write-Host "Test"' -Details "Original" + New-PSNote -Note 'QuickTest' -Snippet 'Write-Host "Updated"' -Details "Modified version" -Force + + Demonstrates creating a note and then updating it with the -Force parameter. + + .NOTES + The note alias is created as a global alias pointing to Get-PSNoteAlias. + This allows you to simply type the alias to retrieve or run the note. + + .LINK + https://github.com/mdowst/PSNotes + + .LINK + Get-PSNote + + .LINK + Set-PSNote + + .LINK + Remove-PSNote + #> + [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, 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)) { + Write-Error "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 { $_.Note -eq $Note -and $_.Catalog -eq $Catalog } + if ($newNote -and -not $force) { + Write-Error "The note '$Note' already exists. Use -force to overwrite existing properties" + break + } + elseif ($newNote -and $force) { + $toUpdate = $script:_noteStore.Notes | Where-Object { $_.Note -eq $Note -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: $($_.Note)" + $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($Note, $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/Remove-PSNote.ps1 b/src/Public/Remove-PSNote.ps1 new file mode 100644 index 0000000..b8eb9c7 --- /dev/null +++ b/src/Public/Remove-PSNote.ps1 @@ -0,0 +1,137 @@ +Function Remove-PSNote { + <# + .SYNOPSIS + Remove one or more PSNotes from the note store. + + .DESCRIPTION + You can remove notes by piping results from Get-PSNote, or by using the same + discovery parameters (Note/Tag/Catalog/SearchString) to select notes to remove. + + .PARAMETER InputObject + Pipeline input (typically from Get-PSNote). + + .PARAMETER Note + Note name pattern (wildcards supported). Defaults to '*'. + + .PARAMETER Tag + Filter by tag (exact match, consistent with Get-PSNote). + + .PARAMETER Catalog + Filter by catalog name (wildcards supported; accepts multiple values). + + .PARAMETER SearchString + Free-text search across Note/Alias/Details/Snippet/Tags. + + .PARAMETER Force + Suppress confirmation prompts (still honors -WhatIf). + + .EXAMPLE + Get-PSNote -SearchString 'cred' -Catalog 'Work*' | Remove-PSNote + + Removes notes matched by search string from catalogs that start with 'Work'. + + .EXAMPLE + Remove-PSNote -Note 'cred*' -Catalog 'Default' + + Removes notes with names starting with 'cred' from the Default catalog. + + .EXAMPLE + Remove-PSNote -SearchString 'token' -Catalog 'Work*','Personal*' -Force + + Removes notes matching 'token' in the Work and Personal catalogs without confirmation. + + .EXAMPLE + Get-PSNote -Tag 'deprecated' | Remove-PSNote -Force + + Removes all notes tagged 'deprecated' via pipeline without confirmation. + + .LINK + https://github.com/mdowst/PSNotes + #> + [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]$Note = '*', + + [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['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['Note'] = $Note + 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 } + $key = "{0}::{1}" -f $n.Catalog, $n.Note + 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) { + $desc = "Removing note '{0}' from catalog '{1}'" -f $n.Note, $n.Catalog + if ($PSCmdlet.ShouldProcess($desc)) { + $script:_noteStore.RemoveNote($n.Note, $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..53fe641 --- /dev/null +++ b/src/Public/Set-PSNote.ps1 @@ -0,0 +1,214 @@ +Function Set-PSNote { + <# + .SYNOPSIS + Updates an existing PSNote or creates a new one if it doesn't exist. + + .DESCRIPTION + Modifies the properties of an existing PSNote. If the note does not exist in the + specified catalog, a warning is displayed and the note will be created. + + This function updates only the properties you specify, leaving other properties unchanged. + It internally calls New-PSNote with the -Force parameter to update the note. + + The note Kind is automatically set based on the parameter used: + - Snippet or ScriptBlock: Updates to Kind 'Snippet' (inline code) + - ScriptPath: Updates to Kind 'Script' (file reference) + + Supports pipeline input by property name, allowing you to pipe objects with Note, Catalog, + Snippet, Details, Alias, Tags, or Run properties. + + .PARAMETER Note + The name of the note to update. Must match an existing note name in the specified catalog. + Accepts input from pipeline by property name. + + .PARAMETER Catalog + The catalog where the note is located. Defaults to 'Default'. If the note doesn't exist + in the specified catalog, it will be created there. + Accepts input from pipeline by property name. + + .PARAMETER Snippet + The new snippet text to store in the note. This replaces the existing snippet content. + Use this for simple one-line or small code snippets. + Accepts input from pipeline by property name. + + .PARAMETER ScriptBlock + A PowerShell script block containing the new code to save. Enclose commands in braces { }. + This is useful for multi-line code with proper syntax highlighting. + Accepts input from pipeline by property name. + + .PARAMETER ScriptPath + The file path to a PowerShell script (.ps1) file. When specified, the note will be + updated to reference this external script file and the note Kind will be set to 'Script'. + The script file must exist at the specified path. + Accepts input from pipeline by property name. + + .PARAMETER Details + New description or additional information about the note. Replaces the existing details. + Accepts input from pipeline by property name. + + .PARAMETER Alias + The new alias for this note. The alias can only contain letters, numbers, dashes (-), + and underscores (_). The alias is set as a global alias that invokes Get-PSNoteAlias. + Accepts input from pipeline by property name. + + .PARAMETER Tags + A new string array of tags to associate with the note. This replaces all existing tags. + Accepts input from pipeline by property name. + + .PARAMETER Run + Updates whether the note should be executed automatically when retrieved. + Set to $true to enable auto-execution, $false to disable it. + Accepts input from pipeline by property name. + + .EXAMPLE + Set-PSNote -Note 'ADUser' -Tags 'AD','Users','Updated' + + Updates the tags for the note 'ADUser' in the Default catalog, replacing any existing tags. + + .EXAMPLE + $NewSnippet = '(Get-Culture).DateTimeFormat.GetAbbreviatedDayName((Get-Date).DayOfWeek.value__)' + Set-PSNote -Note 'DayOfWeek' -Snippet $NewSnippet + + Updates the snippet content for the note 'DayOfWeek' while preserving other properties. + + .EXAMPLE + Set-PSNote -Note '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 using modern cmdlets. + + .EXAMPLE + Set-PSNote -Note 'CpuUsage' -Details "Returns average CPU usage percentage" -Alias 'cpu' + + Updates only the Details and Alias properties for the note 'CpuUsage', leaving the snippet unchanged. + + .EXAMPLE + Set-PSNote -Note 'ADUser' -Catalog 'Work' -Tags 'AD','Users','Production' + + Updates the tags for the note 'ADUser' that exists in the 'Work' catalog. + + .EXAMPLE + Set-PSNote -Note 'GetDate' -Snippet 'Get-Date -Format "yyyy-MM-dd"' -Details "Returns current date in ISO format" -Tags 'date','formatting' + + Updates multiple properties (snippet, details, and tags) of the note 'GetDate' in a single command. + + .EXAMPLE + Set-PSNote -Note 'TestConnection' -Run $true -Details "Auto-run connectivity test" + + Enables auto-execution for the note 'TestConnection'. When retrieved, it will run automatically. + + .EXAMPLE + Set-PSNote -Note 'BackupScript' -ScriptPath 'D:\Scripts\Backup-Database.ps1' -Details "Updated backup script location" + + Updates an existing note to reference a different script file, changing its Kind to 'Script'. + + .EXAMPLE + Set-PSNote -Note 'NewFeature' -Snippet 'Get-Service -Name "MyService"' -Details "Check service status" + + Creates a new note named 'NewFeature' because it doesn't exist yet. A warning will be displayed. + + .EXAMPLE + Get-PSNote -Note 'ADUser' | Set-PSNote -Tags 'AD','Users','Updated' + + Retrieves the note 'ADUser' and updates its tags via pipeline by property name. + + .EXAMPLE + Get-PSNote -Tag 'deprecated' | Set-PSNote -Tags 'archived','old' + + Updates all notes tagged 'deprecated' to have tags 'archived' and 'old' instead. + Uses pipeline to process multiple notes at once. + + .EXAMPLE + Get-PSNote -Catalog 'Work' | Where-Object { $_.Tags -contains 'legacy' } | + Set-PSNote -Tags 'archived','legacy','review' + + Finds all notes in the Work catalog with the 'legacy' tag and updates their tags. + Demonstrates filtering and bulk updating via pipeline. + + .EXAMPLE + [PSCustomObject]@{ + Note = 'MyNote' + Snippet = 'Get-Process | Select-Object -First 10' + Details = 'Top 10 processes' + Tags = @('process','monitoring') + } | Set-PSNote + + Creates or updates a note using a custom object via pipeline by property name. + + .EXAMPLE + Import-Csv .\notes.csv | Set-PSNote + + Bulk creates or updates notes from a CSV file with columns matching parameter names + (Note, Snippet, Details, Tags, Catalog, etc.). Processes each row via pipeline. + + .EXAMPLE + Get-PSNote | Where-Object { $_.Catalog -eq 'Default' } | + Set-PSNote -Catalog 'Personal' + + Moves all notes from the Default catalog to the Personal catalog via pipeline. + + .NOTES + This function is a convenience wrapper around New-PSNote with the -Force parameter. + All property updates replace the existing values rather than merging with them. + + The function supports pipeline input by property name, making it easy to: + - Update multiple notes from Get-PSNote results + - Bulk import/update notes from CSV or other structured data + - Chain with Where-Object for conditional updates + + If you need to append tags rather than replace them, retrieve the note first: + $note = Get-PSNote -Note 'MyNote' + $newTags = $note.Tags + 'NewTag' + Set-PSNote -Note 'MyNote' -Tags $newTags + + .LINK + https://github.com/mdowst/PSNotes + + .LINK + New-PSNote + + .LINK + Get-PSNote + + .LINK + Remove-PSNote + #> + [cmdletbinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + param( + [parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$True)] + [string]$Note, + [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 { $_.Note -eq $Note -and $_.Catalog -eq $Catalog } + if (-not $check) { + Write-Warning "The note '$Note' 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 100% rename from Public/ConvertTo-Splatting.ps1 rename to src/Public/Splatting/ConvertTo-Splatting.ps1 diff --git a/Public/Get-CommandSplatting.ps1 b/src/Public/Splatting/Get-CommandSplatting.ps1 similarity index 100% rename from Public/Get-CommandSplatting.ps1 rename to src/Public/Splatting/Get-CommandSplatting.ps1 diff --git a/src/Public/Update-PSNoteStore.ps1 b/src/Public/Update-PSNoteStore.ps1 new file mode 100644 index 0000000..e272249 --- /dev/null +++ b/src/Public/Update-PSNoteStore.ps1 @@ -0,0 +1,57 @@ +function Update-PSNoteStore { + <# + .SYNOPSIS + Update PSNotes catalogs to the latest format + + .DESCRIPTION + Scans the PSNotes home directory for JSON catalogs and migrates any + catalogs that are not in the current format. Migration results are + imported back into the store using the specified conflict behavior. + + .PARAMETER DefaultBehavior + Determines how to handle existing notes when conflicts are detected. + Valid values: Prompt, SkipMigratedNotes, OverwriteExistingNotes. + + .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 + #> + [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..609165e --- /dev/null +++ b/tests/Build/BuildValidation.Tests.ps1 @@ -0,0 +1,55 @@ +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 = @( + 'ConvertTo-Splatting', + 'Export-PSNote', + 'Get-CommandSplatting', + 'Get-PSNote', + 'Get-PSNoteAlias', + 'Import-PSNote', + 'Initialize-PSNoteStore', + 'New-PSNote', + 'Remove-PSNote', + 'Set-PSNote', + 'Start-PSNote', + 'Update-PSNoteStore' + ) + $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..c497caf --- /dev/null +++ b/tests/ScriptAnalyzer/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,4 @@ +@{ + Severity=@('Error','Warning') + #ExcludeRules=@('PSAvoidUsingInvokeExpression','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..b27153f --- /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 -Note '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 -Note '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 -Note '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.Note | Should -Be 'az-login' + $exportedNote.Snippet | Should -Be 'Connect-AzAccount' + } + + It "excludes the Path property from exported JSON" { + $note = Get-PSNote -Note '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 -Note 'az-login' + $note | Export-PSNote -Path $exportPath + + # Attempt to overwrite without -Force + { Get-PSNote -Note '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 -Note 'az-login' + $note1 | Export-PSNote -Path $exportPath + + # Overwrite with -Force + $note2 = Get-PSNote -Note 'day-one' + $note2 | Export-PSNote -Path $exportPath -Force + + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes[0].Note | Should -Be 'day-one' + } + + It "creates JSON file with UTF8 encoding without BOM" { + $exportPath = Join-Path $script:ExportDir 'encoding.json' + $note = Get-PSNote -Note '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 -Note 'az-login' | Export-PSNote -Path $exportPath + + Test-Path $exportPath | Should -Be $true + $json = Get-Content $exportPath | ConvertFrom-Json + $json.Notes[0].Note | 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 -Note '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 -Note '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 -Note 'SpecialChars' -Snippet 'Test' -Details 'Text with "quotes" and special chars: @#$%' -Force + $exportPath = Join-Path $script:ExportDir 'special-chars.json' + + Get-PSNote -Note '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 -Note 'Multiline' -Snippet $multilineSnippet -Force + $exportPath = Join-Path $script:ExportDir 'multiline.json' + + Get-PSNote -Note '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 -Note 'MultiTag' -Snippet 'Test' -Tags 'Tag1', 'Tag2', 'Tag3' -Force + $exportPath = Join-Path $script:ExportDir 'multi-tag.json' + + Get-PSNote -Note '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 -Note '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 -Note '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..846bbfe --- /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 -Note wildcard" { + $r = Get-PSNote -Note 'cred*' + @($r).Count | Should -Be 2 + $r.Note | Should -Contain 'creds' + $r.Note | Should -Contain 'creds2' + } + + It "filters notes by -Tag (exact match)" { + $r = Get-PSNote -Tag 'Azure' + @($r).Count | Should -Be 1 + $r[0].Note | Should -Be 'az-login' + } + + It "filters notes by -Note and -Tag together" { + $r = Get-PSNote -Note '*cred*' -Tag 'AD' + @($r).Count | Should -Be 1 + $r[0].Note | 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 -Note '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 -Note 'cred*' -Run | Out-Null + + Assert-MockCalled -CommandName Invoke-PSNote -Times 1 -Exactly -ModuleName PSNotes -ParameterFilter { + $Note.Note -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.Note | 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].Note | 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..4d81f04 --- /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 -Note '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 -Note '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 -Note '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/Import-PSNote.Tests.ps1 b/tests/UnitTests/Import-PSNote.Tests.ps1 new file mode 100644 index 0000000..e8010ac --- /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 -Note 'test-import' + $imported.Note | 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 -Note 'legacy-note' + $imported | Should -Not -BeNullOrEmpty + $imported.Note | Should -Be 'legacy-note' + } + } + + Context "Duplicate handling with DefaultBehavior parameter" { + + It "skips migrated notes when DefaultBehavior is SkipMigratedNotes" { + # Create initial note + New-PSNote -Note '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.Note | Should -Be 'duplicate-test' + $note.Details | Should -Be 'Original note' + } + + It "overwrites existing notes when DefaultBehavior is OverwriteExistingNotes" { + # Create initial note + New-PSNote -Note '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.Note | 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 -Note 'note-one' | Should -Not -BeNullOrEmpty + Get-PSNote -Note 'note-two' | Should -Not -BeNullOrEmpty + Get-PSNote -Note '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 -Note '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 -Note '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/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..1c9a767 --- /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 -Note 'TestSnippet' -Snippet 'Get-Process' -Details 'Test snippet' -Tags 'Test' + + $result = Get-PSNote -Note 'TestSnippet' + $result.Note | 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 -Note 'TestScriptBlock' -ScriptBlock $scriptBlock -Details 'Test scriptblock' + + $result = Get-PSNote -Note 'TestScriptBlock' + $result.Note | 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 -Note 'TestScriptPath' -ScriptPath $scriptFile -Details 'Test script path' + + $result = Get-PSNote -Note 'TestScriptPath' + $result.Note | 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 -Note 'TestMultiTags' -Snippet 'Get-ChildItem' -Tags 'Files', 'Test', 'PowerShell' + + $result = Get-PSNote -Note '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 -Note 'TestAlias' -Snippet 'Test-Connection' -Alias 'ping-test' + + $result = Get-PSNote -Note 'TestAlias' + $result.Alias | Should -Be 'ping-test' + } + + It "leave Alias blank when Alias is not specified" { + New-PSNote -Note 'TestDefaultAlias' -Snippet 'Get-Date' + + $result = Get-PSNote -Note '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 -Note 'TestMultiline' -Snippet $multilineSnippet -Details 'Multiline test' + + $result = Get-PSNote -Note 'TestMultiline' + $result.Snippet | Should -Be $multilineSnippet + } + } + + Context "Run and Alias properties" { + + It "creates a note without an Alias and without Run" { + New-PSNote -Note 'TestNoAliasNoRun' -Snippet 'Write-Output "Test snippet"' -Details 'Test snippet' -Tags 'Test' -Catalog 'TestCatalog' + + { TestNoAliasNoRun } | Should -Throw + + $result = Get-PSNote -Note 'TestNoAliasNoRun' + $result.Run | Should -Be $false + $result.Alias | Should -Be '' + + Get-PSNote -Note 'TestNoAliasNoRun' -Run | Should -Be "Test snippet" + } + + It "creates a note with an Alias and without Run" { + New-PSNote -Note '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 -Note 'TestAliasNoRun' + $result.Run | Should -Be $false + $result.Alias | Should -Be 'testaliasnorun' + + Get-PSNote -Note 'TestAliasNoRun' -Run | Should -Be 'Test Alias and without Run' + } + + It "creates a note with an Alias and with Run" { + New-PSNote -Note '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 -Note 'TestRunAliasRun' + $result.Run | Should -Be $true + $result.Alias | Should -Be 'testrunaliasrun' + + Get-PSNote -Note 'TestRunAliasRun' -Run | Should -Be 'Test Alias and with Run' + } + + It "creates a note without an Alias and with Run" { + New-PSNote -Note '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 -Note 'TestRunNoAliasRun' + $result = Get-PSNote -Note 'TestRunNoAliasRun' + $result.Run | Should -Be $true + $result.Alias | Should -Be '' + + Get-PSNote -Note '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 -Note 'UpdateTest' -Snippet 'Get-Process' -Details 'Original' -Tags 'Test' -Force + } + + It "throws an error when trying to overwrite without -Force" { + New-PSNote -Note '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 -Note 'UpdateTest' -Snippet 'Get-Service' -Force + + $result = Get-PSNote -Note 'UpdateTest' + $result.Snippet | Should -Be 'Get-Service' + } + + It "updates only specified properties with -Force" { + New-PSNote -Note 'UpdateTest' -Details 'Updated details' -Force + + $result = Get-PSNote -Note 'UpdateTest' + $result.Details | Should -Be 'Updated details' + $result.Snippet | Should -Be 'Get-Process' # Original snippet should remain + } + + It "updates Tags with -Force" { + New-PSNote -Note 'UpdateTest' -Tags 'Updated', 'NewTag' -Force + + $result = Get-PSNote -Note 'UpdateTest' + $result.Tags | Should -Contain 'Updated' + $result.Tags | Should -Contain 'NewTag' + } + + It "updates Alias with -Force" { + New-PSNote -Note 'UpdateTest' -Alias 'new-alias' -Force + + $result = Get-PSNote -Note '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 -Note 'UpdateTest' -ScriptPath $scriptFile -Force + + $result = Get-PSNote -Note '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 -Note 'ValidAlias1' -Snippet 'Test' -Alias 'valid-alias_123' } | Should -Not -Throw + } + + It "throws an error for alias with spaces" { + { New-PSNote -Note 'InvalidAlias1' -Snippet 'Test' -Alias 'invalid alias' } | Should -Throw + } + + It "throws an error for alias with special characters" { + { New-PSNote -Note 'InvalidAlias2' -Snippet 'Test' -Alias 'invalid@alias' } | Should -Throw + } + + It "throws an error for alias with dots" { + { New-PSNote -Note 'InvalidAlias3' -Snippet 'Test' -Alias 'invalid.alias' } | Should -Throw + } + } + + Context "Parameter sets" { + + It "accepts Snippet parameter" { + { New-PSNote -Note 'SnippetParam' -Snippet 'Get-Date' } | Should -Not -Throw + } + + It "accepts ScriptBlock parameter" { + { New-PSNote -Note '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 -Note 'ScriptPathParam' -ScriptPath $scriptFile } | Should -Not -Throw + } + + It "converts ScriptBlock to string for storage" { + $sb = { Get-Process | Select-Object -First 5 } + New-PSNote -Note 'ScriptBlockConversion' -ScriptBlock $sb + + $result = Get-PSNote -Note 'ScriptBlockConversion' + $result.Snippet | Should -Be $sb.ToString() + } + + It "throws when ScriptPath does not exist" { + $missingFile = Join-Path $script:TestDir 'MissingScript.ps1' + { New-PSNote -Note 'MissingScriptPath' -ScriptPath $missingFile } | Should -Throw + } + } + + Context "Global alias creation" { + + It "creates a global alias for the note" { + New-PSNote -Note '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..e0063c9 --- /dev/null +++ b/tests/UnitTests/NoteStore.class.Tests.ps1 @@ -0,0 +1,1090 @@ +# Pester tests for NoteStore class +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 +} +. "$Global:TopLevel\src\Classes\NoteStore.class.ps1" +BeforeAll { + Set-StrictMode -Version Latest + # Create a temporary directory for test files + $script:TestDir = Join-Path ([System.IO.Path]::GetTempPath()) "PSNotesTests\NoteStore" + 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 + # Load the class file + . "$Global:TopLevel\src\Classes\NoteStore.class.ps1" +} + +AfterAll { + # Restore original environment + $env:PSNOTES_HOME = $script:OriginalPSNotesHome +} + +Describe 'PSNote Class' { + Context 'Constructor with 5 parameters' { + It 'creates a PSNote object with all properties set' { + $note = [PSNote]::new( + 'MyNote', + 'Write-Host "Hello"', + 'A greeting snippet', + 'MyAlias', + @('test', 'example') + ) + + $note.Note | Should -Be 'MyNote' + $note.Snippet | Should -Be 'Write-Host "Hello"' + $note.Details | Should -Be 'A greeting snippet' + $note.Alias | Should -Be 'MyAlias' + $note.Tags | Should -Be @('test', 'example') + $note.Catalog | Should -Be 'Default' + } + + It 'sets Alias to Blank when Alias is empty' { + $note = [PSNote]::new( + 'MyNote', + 'Write-Host "Hello"', + 'A greeting snippet', + '', + @('test') + ) + + $note.Alias | Should -Be '' + } + } + + Context 'Constructor with 6 parameters' { + It 'creates a PSNote object with custom catalog' { + $note = [PSNote]::new( + 'MyNote', + 'Write-Host "Hello"', + 'A greeting snippet', + 'MyAlias', + @('test'), + 'CustomCatalog', + $false + ) + + $note.Note | Should -Be 'MyNote' + $note.Catalog | Should -Be 'CustomCatalog' + } + + It 'sets Alias to Blank when Alias is empty with custom catalog' { + $note = [PSNote]::new( + 'MyNote', + 'Write-Host "Hello"', + 'A greeting snippet', + '', + @('test'), + 'CustomCatalog', + $false + ) + + $note.Alias | Should -Be '' + } + } + + Context 'Constructor with object parameter' { + It 'creates a PSNote from a PSCustomObject' { + $obj = [pscustomobject]@{ + Note = 'TestNote' + Snippet = '$x = 1' + Details = 'A test note' + Alias = 'tn' + Tags = @('tag1', 'tag2') + Catalog = 'TestCatalog' + Run = $false + } + + $note = [PSNote]::new($obj) + + $note.Note | Should -Be 'TestNote' + $note.Snippet | Should -Be '$x = 1' + $note.Details | Should -Be 'A test note' + $note.Alias | Should -Be 'tn' + $note.Tags | Should -Be @('tag1', 'tag2') + $note.Catalog | Should -Be 'TestCatalog' + } + + It 'sets Alias to Blank when Alias is empty' { + $obj = [pscustomobject]@{ + Note = 'TestNote' + Snippet = '$x = 1' + Details = 'A test note' + Alias = '' + Tags = @('tag1') + Catalog = 'TestCatalog' + Run =$false + } + + $note = [PSNote]::new($obj) + + $note.Alias | Should -Be '' + } + } + + + Context 'Kind and Snippet behavior' { + It 'defaults Kind to Snippet (5-parameter constructor)' { + $note = [PSNote]::new( + 'MyNote', + 'Write-Host "Hello"', + 'A greeting snippet', + 'MyAlias', + @('test') + ) + + $note.Kind | Should -Be ([PSNoteKind]::Snippet) + $note.Snippet | Should -Be 'Write-Host "Hello"' + # Back-compat: Snippet remains populated + $note.Snippet | Should -Be $note.Snippet + } + + It 'supports Script kind via Kind/Snippet constructor' { + $note = [PSNote]::new( + 'RunScript', + [PSNoteKind]::Script, + '.\Scripts\Do-The-Thing.ps1', + 'Runs a script', + 'runscript', + @('script'), + 'Default', + $true + ) + + $note.Kind | Should -Be ([PSNoteKind]::Script) + $note.Snippet | Should -Be '.\Scripts\Do-The-Thing.ps1' + } + + It 'parses Script Kind and Snippet from object data' { + $obj = [pscustomobject]@{ + Note = 'ScriptNote' + Kind = 'Script' + Snippet = 'C:\Temp\Do.ps1' + Details = 'script note' + Alias = 'do' + Tags = @('script') + Catalog = 'TestCatalog' + Run = $true + } + + $note = [PSNote]::new($obj) + + $note.Kind | Should -Be ([PSNoteKind]::Script) + $note.Snippet | Should -Be 'C:\Temp\Do.ps1' + $note.Run | Should -BeTrue + } + + It 'falls back to Snippet kind when Kind is invalid' { + $obj = [pscustomobject]@{ + Note = 'BadKind' + Kind = 'NotARealKind' + Snippet = 'Whatever' + Details = 'bad kind' + Alias = 'bk' + Tags = @() + Catalog = 'TestCatalog' + Run = $false + } + + $note = [PSNote]::new($obj) + + $note.Kind | Should -Be ([PSNoteKind]::Snippet) + } + + + It 'GetDisplayText labels Script notes and leaves Snippet notes unchanged' { + $snippetNote = [PSNote]::new( + 'MyNote', + 'Get-Process', + 'details', + 'gp', + @() + ) + $snippetNote.GetDisplayText() | Should -Be 'gp' + + $scriptNote = [PSNote]::new( + 'RunScript', + [PSNoteKind]::Script, + 'C:\Temp\Run.ps1', + 'details', + 'rs', + @(), + 'Default', + $false + ) + $scriptNote.GetDisplayText() | Should -Be 'rs (Script)' + } + } +} + +Describe 'NoteCatalog Static Methods' { + + Context 'ResolvePath' { + It 'resolves path with no parameters (default catalog)' { + $path = [NoteCatalog]::ResolvePath() + + $path | Should -Match 'Default\.json$' + (Split-Path -Parent $path) | Should -Exist + } + + It 'resolves path with only catalog name parameter' { + $path = [NoteCatalog]::ResolvePath('MyNotes') + + $path | Should -Match 'MyNotes\.json$' + } + + It 'resolves path with custom catalog name and root path' { + $path = [NoteCatalog]::ResolvePath('CustomNote', $script:TestDir) + + $path | Should -Match 'CustomNote\.json$' + $path | Should -Match ([regex]::Escape($script:TestDir)) + } + + It 'resolves path when catalog name already has .json extension' { + $path = [NoteCatalog]::ResolvePath('MyNotes.json') + + $path | Should -Match 'MyNotes\.json$' + $path | Should -Not -Match 'MyNotes\.json\.json$' + } + + It 'creates root path directory if it does not exist' { + $testPath = Join-Path $script:TestDir 'subdir' + + $path = [NoteCatalog]::ResolvePath('test', $testPath) + + (Split-Path -Parent $path) | Should -Exist + } + } + + Context 'ReadUtf8NoBom' { + It 'reads UTF8 content from file without BOM' { + $testFile = Join-Path $script:TestDir 'test.json' + $content = '{"test": "value"}' + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($testFile, $content, $utf8NoBom) + + $result = [NoteCatalog]::ReadUtf8NoBom($testFile) + + $result | Should -Be $content + } + + It 'returns null when file does not exist' { + $result = [NoteCatalog]::ReadUtf8NoBom('C:\NonExistent\file.json') + + $result | Should -BeNullOrEmpty + } + } + <# + Context 'WriteUtf8NoBomToLockedStream' { + It 'writes content to stream with UTF8 no BOM encoding' { + $testFile = Join-Path $script:TestDir 'write_test.json' + $content = '{"data": "test"}' + + $stream = [System.IO.File]::Open( + $testFile, + [System.IO.FileMode]::Create, + [System.IO.FileAccess]::ReadWrite, + [System.IO.FileShare]::None + ) + + try { + [NoteCatalog]::WriteUtf8NoBomToLockedStream($stream, $content) + + $readBack = [NoteCatalog]::ReadUtf8NoBom($testFile) + $readBack | Should -Be $content + } + finally { + $stream.Dispose() + } + } + + It 'clears existing stream content before writing' { + $testFile = Join-Path $script:TestDir 'clear_test.json' + $content1 = '{"first": "content"}' + $content2 = '{"second": "data"}' + + # Write initial content + [System.IO.File]::WriteAllText($testFile, $content1) + + # Open and overwrite + $stream = [System.IO.File]::Open( + $testFile, + [System.IO.FileMode]::Open, + [System.IO.FileAccess]::ReadWrite, + [System.IO.FileShare]::None + ) + + try { + [NoteCatalog]::WriteUtf8NoBomToLockedStream($stream, $content2) + + $readBack = [NoteCatalog]::ReadUtf8NoBom($testFile) + $readBack | Should -Be $content2 + $readBack.Length | Should -BeLess $content1.Length + } + finally { + $stream.Dispose() + } + } + } + + Context 'AcquireLock' { + It 'acquires lock on file' { + $testFile = Join-Path $script:TestDir 'lock_test.json' + $null = New-Item -Path $testFile -ItemType File -Force + + $lockStream = $null + try { + $lockStream = [NoteCatalog]::AcquireLock($testFile) + $lockStream | Should -Not -BeNullOrEmpty + $lockStream.CanRead | Should -Be $true + $lockStream.CanWrite | Should -Be $true + } + finally { + $lockStream.Dispose() + } + } + + It 'times out when file is locked by another stream' { + $testFile = Join-Path $script:TestDir 'locked_test.json' + $null = New-Item -Path $testFile -ItemType File -Force + + $lock1 = [System.IO.File]::Open( + $testFile, + [System.IO.FileMode]::Open, + [System.IO.FileAccess]::ReadWrite, + [System.IO.FileShare]::None + ) + + try { + { [NoteCatalog]::AcquireLock($testFile, 100) } | Should -Throw + } + finally { + $lock1.Dispose() + } + } + } + #> +} + +Describe 'NoteCatalog Instance Methods' { + Context 'Constructor without parameters' { + It 'creates a default catalog' { + $catalog = [NoteCatalog]::new() + $catalog.Catalog | Should -Be 'Default' + $catalog.StoreVersion | Should -Be 1 + + $catalog.Save() + #$catalog.Notes | Should -BeOfType 'System.Collections.Generic.List[PSNote]' + $catalog.Path | Should -Exist -Because 'directory should be created' + } + } + + Context 'Constructor with catalog name' { + It 'creates catalog with custom name' { + $catalog = [NoteCatalog]::new('MyCatalog') + + $catalog.Catalog | Should -Be 'MyCatalog' + $catalog.Path | Should -Match 'MyCatalog\.json$' + } + } + + Context 'Constructor with blank parameter' { + It 'creates blank catalog without loading from disk' { + $catalog = [NoteCatalog]::new($true) + + $catalog.StoreVersion | Should -Be 1 + $catalog.Notes.Count | Should -Be 0 + $catalog.Path | Should -Not -BeNullOrEmpty + } + + It 'blank catalog does not auto-load existing file' { + # Create and save a catalog with notes + $existingCatalog = [NoteCatalog]::new('BlankTest') + $existingCatalog.Notes.Add([PSNote]::new('ExistingNote', 'code', 'details', 'en', @('tag'))) + $existingCatalog.Save() + + # Create blank catalog - should not load the existing file + $blankCatalog = [NoteCatalog]::new($true) + $blankCatalog.Notes.Count | Should -Be 0 + } + } + + Context 'Open method' { + It 'opens and loads existing catalog' { + # Create a catalog with test data + $testNote = [PSNote]::new( + 'TestNote', + 'Write-Host "test"', + 'Test details', + 'tn', + @('tag1') + ) + + $catalog = [NoteCatalog]::new('TestCatalog') + $catalog.Notes.Add($testNote) + $catalog.Save() + + # Create a new instance and load + $catalog2 = [NoteCatalog]::new('TestCatalog') + $catalog2.Notes.Count | Should -Be 1 + $catalog2.Notes[0].Note | Should -Be 'TestNote' + } + + It 'handles empty catalog file' { + $testFile = Join-Path $script:TestDir 'empty_catalog.json' + $null = New-Item -Path $testFile -ItemType File -Force + + $catalog = [NoteCatalog]::Open($testFile) + + $catalog.Notes.Count | Should -Be 0 + } + + It 'loads legacy catalog format (array of notes)' { + $testFile = Join-Path $script:TestDir 'legacy_catalog.json' + + # Legacy format is just an array + $legacyJson = @( + [pscustomobject]@{ + Note = 'Legacy1' + Snippet = 'code' + Details = 'details' + Alias = 'l1' + Tags = @('old') + Catalog = '' + } + ) | ConvertTo-Json + + Set-Content -Path $testFile -Value $legacyJson + + $catalog = [NoteCatalog]::Migrate($testFile, $true) + + $catalog.Notes.Count | Should -Be 1 + $catalog.Notes[0].Note | Should -Be 'Legacy1' + } + + It 'loads legacy catalog format (array of notes)' { + $testFile = Join-Path $script:TestDir 'legacy_catalog.json' + + # Legacy format is just an array + $legacyJson = @( + [pscustomobject]@{ + Note = 'Legacy1' + Snippet = 'code' + Details = 'details' + Alias = 'l1' + Tags = @('old') + Catalog = '' + } + ) | ConvertTo-Json + + Set-Content -Path $testFile -Value $legacyJson + $warnings = & { + $catalog = [NoteCatalog]::Open($testFile) + } 3>&1 + $warnings | Should -Match "Note catalog store version mismatch" + + # Migration should be handled elsewhere + } + } + + Context 'ToJson method' { + It 'serializes catalog to JSON' { + $catalog = [NoteCatalog]::new('TestCatalog') + $testNote = [PSNote]::new( + 'TestNote', + 'Write-Host "test"', + 'Test details', + 'tn', + @('tag1') + ) + $catalog.Notes.Add($testNote) + + $json = $catalog.ToJson() + + $json | Should -Not -BeNullOrEmpty + $json | Should -Match '"StoreVersion"' + $json | Should -Match '"Notes"' + + # Verify it's valid JSON + { $json | ConvertFrom-Json } | Should -Not -Throw + } + + It 'includes all notes in JSON' { + $catalog = [NoteCatalog]::new('TestCatalogAdd') + $catalog.Notes.Add([PSNote]::new('Note1', 'code1', 'det1', 'n1', @('t1'))) + $catalog.Notes.Add([PSNote]::new('Note2', 'code2', 'det2', 'n2', @('t2'))) + + $json = $catalog.ToJson() + $obj = $json | ConvertFrom-Json + + $obj.Notes.Count | Should -Be 2 + } + } + + Context 'Save method' { + It 'saves catalog to file' { + $catalog = [NoteCatalog]::new('SaveTest') + $testNote = [PSNote]::new( + 'SaveNote', + 'Write-Host "save"', + 'Save test', + 'sn', + @('save') + ) + $catalog.Notes.Add($testNote) + + $catalog.Save() + + Test-Path $catalog.Path | Should -Be $true + + # Verify content + $content = Get-Content $catalog.Path -Raw + $content | Should -Match '"SaveNote"' + } + + It 'updates StoreVersion before saving' { + $catalog = [NoteCatalog]::new('VersionTest') + $initialVersion = $catalog.StoreVersion + + $catalog.Save() + + $json = Get-Content $catalog.Path -Raw | ConvertFrom-Json + $json.StoreVersion | Should -Be $initialVersion + } + } +} + +Describe 'NoteStore Class' { + + Context 'InitializeEnvironment' { + It 'sets PSNOTES_HOME when not already set' { + $tempEnv = $env:PSNOTES_HOME + Remove-Item env:PSNOTES_HOME -ErrorAction SilentlyContinue + + [NoteStore]::InitializeEnvironment() + + $env:PSNOTES_HOME | Should -Not -BeNullOrEmpty + + # Restore + $env:PSNOTES_HOME = $tempEnv + } + } + + Context 'Constructor' { + It 'creates a NoteStore with default catalog' { + $store = [NoteStore]::new() + + $store.Catalogs | Should -Not -BeNullOrEmpty + $store.Catalogs.Count | Should -Be 1 + $store.Catalogs[0].Catalog | Should -Be 'Default' + #$store.Notes | Should -BeOfType 'System.Collections.Generic.List[PSNote]' + } + } + + Context 'LoadCatalog with string' { + It 'loads a catalog by name' { + # Create a test catalog with notes + $testCatalog = [NoteCatalog]::new('LoadTest') + $testNote = [PSNote]::new( + 'LoadedNote', + 'code', + 'details', + 'ln', + @('tag1') + ) + $testCatalog.Notes.Add($testNote) + $testCatalog.Save() + + # Create store and load catalog + $store = [NoteStore]::new() + $initialCount = $store.Notes.Count + $store.LoadCatalog('LoadTest') + + $store.Catalogs.Count | Should -Be 2 + $store.Notes.Count | Should -BeGreaterThan $initialCount + } + } + + Context 'LoadCatalog with NoteCatalog object' { + It 'loads a NoteCatalog object' { + $testCatalog = [NoteCatalog]::new('ObjectLoadTest') + $testNote = [PSNote]::new( + 'ObjectNote', + 'code', + 'details', + 'on', + @('tag') + ) + $testCatalog.Notes.Add($testNote) + + $store = [NoteStore]::new() + $initialCount = $store.Catalogs.Count + $store.LoadCatalog($testCatalog) + + $store.Catalogs.Count | Should -Be ($initialCount + 1) + $store.Notes.Count | Should -BeGreaterThan 0 + } + + It 'prevents duplicate aliases when loading catalog' { + $store = [NoteStore]::new() + + # Create a note with same alias as one that might exist + $testCatalogA = [NoteCatalog]::new('DuplicateTestA') + $dupNote = [PSNote]::new( + 'DupNote', + 'code', + 'details', + 'Default', + @('tag'), + 'DuplicateTestA', + $false + ) + $testCatalogA.Notes.Add($dupNote) + $store.LoadCatalog($testCatalogA) + $testCatalogB = [NoteCatalog]::new('DuplicateTestB') + $dupNoteB = [PSNote]::new( + 'DupNote', + 'code', + 'details', + 'Default', + @('tag'), + 'DuplicateTestB', + $false + ) + $testCatalogB.Notes.Add($dupNoteB) # Add duplicate + + # This should warn but not throw + $warnings = & { + $store.LoadCatalog($testCatalogB) + } 3>&1 + $warnings | Should -Be "Duplicate Alias found: Default. Skipping note: DupNote" + } + } + + Context 'Integration tests' { + It 'loads multiple catalogs and consolidates notes' { + # Create first catalog + $cat1 = [NoteCatalog]::new('IntegrationCat1') + $cat1.Notes.Add([PSNote]::new('Note1', 'c1', 'd1', 'n1', @('t1'))) + $cat1.Notes.Add([PSNote]::new('Note2', 'c2', 'd2', 'n2', @('t2'))) + $cat1.Save() + + # Create second catalog + $cat2 = [NoteCatalog]::new('IntegrationCat2') + $cat2.Notes.Add([PSNote]::new('Note3', 'c3', 'd3', 'n3', @('t3'))) + $cat2.Save() + + # Load into store + $store = [NoteStore]::new() + $store.LoadCatalog('IntegrationCat1') + $store.LoadCatalog('IntegrationCat2') + + $store.Catalogs.Count | Should -Be 3 # default + 2 loaded + $store.Notes.Count | Should -BeGreaterOrEqual 3 + } + } + + Context 'AddNote method' { + It 'adds a note to both store and catalog' { + # Create and save a catalog first + $catalog = [NoteCatalog]::new('AddNoteTest') + $catalog.Save() + + $store = [NoteStore]::new() + $store.LoadCatalog('AddNoteTest') + + $newNote = [PSNote]::new( + 'AddedNote', + 'Write-Host "added"', + 'A newly added note', + 'an', + @('added'), + 'AddNoteTest', + $false + ) + + $initialCount = $store.Notes.Count + $store.AddNote($newNote) + + # Verify note is in store + $store.Notes.Count | Should -Be ($initialCount + 1) + $store.Notes | Where-Object { $_.Note -eq 'AddedNote' } | Should -Not -BeNullOrEmpty + + # Verify note is in catalog + $catalog = $store.Catalogs | Where-Object { $_.Catalog -eq 'AddNoteTest' } + $catalog.Notes | Where-Object { $_.Note -eq 'AddedNote' } | Should -Not -BeNullOrEmpty + } + + It 'saves catalog after adding note' { + $catalog = [NoteCatalog]::new('AddNoteSaveTest') + $catalog.Save() + + $store = [NoteStore]::new() + $store.LoadCatalog('AddNoteSaveTest') + + $newNote = [PSNote]::new( + 'SavedNote', + 'code', + 'details', + 'sn', + @('test'), + 'AddNoteSaveTest', + $false + ) + + $store.AddNote($newNote) + + # Reload from disk to verify persistence + $store2 = [NoteStore]::new() + $store2.LoadCatalog('AddNoteSaveTest') + $store2.Notes | Where-Object { $_.Note -eq 'SavedNote' } | Should -Not -BeNullOrEmpty + } + } + + Context 'RemoveNote method' { + It 'removes a note from store and catalog' { + # Create catalog with notes + $catalog = [NoteCatalog]::new('RemoveNoteTest') + $noteToRemove = [PSNote]::new('RemoveMe', 'code', 'details', 'rm', @('remove'),'RemoveNoteTest', $false) + $noteToKeep = [PSNote]::new('KeepMe', 'code', 'details', 'km', @('keep'),'RemoveNoteTest', $false) + $catalog.Notes.Add($noteToRemove) + $catalog.Notes.Add($noteToKeep) + $catalog.Save() + + $store = [NoteStore]::new() + $store.LoadCatalog('RemoveNoteTest') + + $initialCount = $store.Notes.Count + $store.RemoveNote('RemoveMe', 'RemoveNoteTest') + + # Verify note is removed from store + $store.Notes.Count | Should -Be ($initialCount - 1) + $store.Notes | Where-Object { $_.Note -eq 'RemoveMe' } | Should -BeNullOrEmpty + + # Verify other note still exists + $store.Notes | Where-Object { $_.Note -eq 'KeepMe' } | Should -Not -BeNullOrEmpty + + # Verify removal persisted to catalog + $catalog = $store.Catalogs | Where-Object { $_.Catalog -eq 'RemoveNoteTest' } + $catalog.Notes | Where-Object { $_.Note -eq 'RemoveMe' } | Should -BeNullOrEmpty + } + + It 'persists removal to disk' { + $catalog = [NoteCatalog]::new('RemoveNotePersistTest') + $noteToRemove = [PSNote]::new('TempNote', 'code', 'details', 'tn', @('temp'),'RemoveNotePersistTest', $false) + $catalog.Notes.Add($noteToRemove) + $catalog.Save() + + $store = [NoteStore]::new() + $store.LoadCatalog('RemoveNotePersistTest') + $store.RemoveNote('TempNote', 'RemoveNotePersistTest') + + # Reload from disk to verify persistence + $store2 = [NoteStore]::new() + $store2.LoadCatalog('RemoveNotePersistTest') + $store2.Notes | Where-Object { $_.Note -eq 'TempNote' } | Should -BeNullOrEmpty + } + + It 'handles removing non-existent note gracefully' { + $catalog = [NoteCatalog]::new('RemoveNonExistentTest') + $catalog.Save() + + $store = [NoteStore]::new() + $store.LoadCatalog('RemoveNonExistentTest') + + $initialCount = $store.Notes.Count + + # Should not throw or modify store + { $store.RemoveNote('NonExistent', 'RemoveNonExistentTest') } | Should -Not -Throw + $store.Notes.Count | Should -Be $initialCount + } + } + + Context 'UpdateNote method' { + It 'updates a note in store and catalog' { + # Create catalog with note + $catalog = [NoteCatalog]::new('UpdateNoteTest') + $originalNote = [PSNote]::new('MyNote', 'old code', 'old details', 'mn', @('old'), 'UpdateNoteTest', $false) + $catalog.Notes.Add($originalNote) + $catalog.Save() + + $store = [NoteStore]::new() + $store.LoadCatalog('UpdateNoteTest') + + # Update the note + $updatedNote = [PSNote]::new( + 'MyNote', + 'new code', + 'new details', + 'mn', + @('updated'), + 'UpdateNoteTest', + $false + ) + + $store.UpdateNote($updatedNote) + + # Verify update in store + $noteInStore = $store.Notes | Where-Object { $_.Note -eq 'MyNote' } + $noteInStore.Snippet | Should -Be 'new code' + $noteInStore.Details | Should -Be 'new details' + $noteInStore.Tags | Should -Be @('updated') + + # Verify update in catalog + $catalog = $store.Catalogs | Where-Object { $_.Catalog -eq 'UpdateNoteTest' } + $noteInCatalog = $catalog.Notes | Where-Object { $_.Note -eq 'MyNote' } + $noteInCatalog.Snippet | Should -Be 'new code' + } + + It 'persists updates to disk' { + $catalog = [NoteCatalog]::new('UpdateNotePersistTest') + $originalNote = [PSNote]::new('UpdateMe', 'v1', 'version 1', 'um', @('v1'),'UpdateNotePersistTest', $false) + $catalog.Notes.Add($originalNote) + $catalog.Save() + + $store = [NoteStore]::new() + $store.LoadCatalog('UpdateNotePersistTest') + + $updatedNote = [PSNote]::new( + 'UpdateMe', + 'v2', + 'version 2', + 'um', + @('v2'), + 'UpdateNotePersistTest', + $false + ) + + $store.UpdateNote($updatedNote) + + # Reload from disk to verify persistence + $store2 = [NoteStore]::new() + $store2.LoadCatalog('UpdateNotePersistTest') + $noteOnDisk = $store2.Notes | Where-Object { $_.Note -eq 'UpdateMe' } + $noteOnDisk.Snippet | Should -Be 'v2' + $noteOnDisk.Details | Should -Be 'version 2' + } + + It 'handles updating non-existent note' { + $catalog = [NoteCatalog]::new('UpdateNonExistentTest') + $catalog.Save() + + $store = [NoteStore]::new() + $store.LoadCatalog('UpdateNonExistentTest') + + $nonExistentNote = [PSNote]::new('NonExistent', 'code', 'details', 'ne', @('test'),'UpdateNonExistentTest', $false) + + # Should not throw, but also should not add the note + { $store.UpdateNote($nonExistentNote) } | Should -Not -Throw + $store.Notes | Where-Object { $_.Note -eq 'NonExistent' } | Should -BeNullOrEmpty + } + } + + Context 'Save method' { + It 'saves all catalogs' { + # Create and populate multiple catalogs + $cat1 = [NoteCatalog]::new('SaveAllCat1') + $cat1.Notes.Add([PSNote]::new('Note1', 'c1', 'd1', 'n1', @('t1'),'SaveAllCat1', $false)) + + $cat2 = [NoteCatalog]::new('SaveAllCat2') + $cat2.Notes.Add([PSNote]::new('Note2', 'c2', 'd2', 'n2', @('t2'),'SaveAllCat2', $false)) + + $store = [NoteStore]::new() + $store.LoadCatalog($cat1) + $store.LoadCatalog($cat2) + + # Add new notes + $store.Notes | ForEach-Object { + $_.Snippet = 'modified' + } + + # Save all catalogs + $store.Save() + + # Reload and verify all changes persisted + $store2 = [NoteStore]::new() + $store2.LoadCatalog('SaveAllCat1') + $store2.LoadCatalog('SaveAllCat2') + + $store2.Catalogs.Count | Should -Be 3 # default + 2 + } + } +} + + +Describe 'NoteMetadataStore Class' { + Context 'Constructor and defaults' { + It 'creates config directory and initializes defaults' { + $store = [NoteMetadataStore]::new() + + $store.Version | Should -Be ([NoteMetadataStore]::CurrentVersion) + + $expectedDir = Join-Path $env:PSNOTES_HOME 'config' + (Test-Path $expectedDir) | Should -BeTrue + + $expectedPath = Join-Path $expectedDir 'psnotemetadatastore.json' + $store.Path | Should -Be $expectedPath + } + + It 'does not throw when metadata file does not exist' { + $path = Join-Path $env:PSNOTES_HOME 'config\does-not-exist.json' + { [NoteMetadataStore]::new($path) } | Should -Not -Throw + } + } + + Context 'Persistence' { + It 'round-trips Favorites via Save/Open' { + $path = Join-Path $env:PSNOTES_HOME 'config\meta-roundtrip.json' + $store = [NoteMetadataStore]::new($path) + + $null = $store.Favorites.Add('Catalog1::alias1') + $null = $store.Favorites.Add('Catalog2::alias2') + $store.Save() + + $store2 = [NoteMetadataStore]::new($path) + $store2.Favorites.Contains('Catalog1::alias1') | Should -BeTrue + $store2.Favorites.Contains('Catalog2::alias2') | Should -BeTrue + } + + It 'fails safe on corrupt JSON' { + $path = Join-Path $env:PSNOTES_HOME 'config\meta-corrupt.json' + $null = New-Item -ItemType Directory -Path (Split-Path $path) -Force + Set-Content -LiteralPath $path -Value '{ not json' -Encoding UTF8 + + { [NoteMetadataStore]::new($path) } | Should -Not -Throw + + $store = [NoteMetadataStore]::new($path) + $store.Favorites.Count | Should -Be 0 + } + } +} + +Describe 'NoteConfigStore Class' { + Context 'Defaults' { + It 'initializes defaults when no config exists' { + $store = [NoteConfigStore]::new() + + $store.Version | Should -Be ([NoteConfigStore]::CurrentVersion) + $store.Main | Should -Be 'Favorites' + $store.ExitOnCopy | Should -BeTrue + $store.ForegroundColor | Should -Be ([ConsoleColor]::Black) + $store.BackgroundColor | Should -Be ([ConsoleColor]::Gray) + + $expectedDir = Join-Path $env:PSNOTES_HOME 'config' + $expectedPath = Join-Path $expectedDir 'psnoteconfig.json' + $store.Path | Should -Be $expectedPath + } + } + + Context 'Persistence and parsing' { + It 'round-trips values via Save/Open' { + $path = Join-Path $env:PSNOTES_HOME 'config\config-roundtrip.json' + $store = [NoteConfigStore]::new($path) + $store.Main = 'Catalogs' + $store.ExitOnCopy = $false + $store.ForegroundColor = [ConsoleColor]::Yellow + $store.BackgroundColor = [ConsoleColor]::Blue + + $store.Save() + + $store2 = [NoteConfigStore]::new($path) + $store2.Main | Should -Be 'Catalogs' + $store2.ExitOnCopy | Should -BeFalse + $store2.ForegroundColor | Should -Be ([ConsoleColor]::Yellow) + $store2.BackgroundColor | Should -Be ([ConsoleColor]::Blue) + } + + It 'keeps defaults when values are invalid' { + $path = Join-Path $env:PSNOTES_HOME 'config\config-invalid.json' + $null = New-Item -ItemType Directory -Path (Split-Path $path) -Force + $bad = @{ + Main = '' + ExitOnCopy = 'notabool' + ForegroundColor = 'NotAColor' + BackgroundColor = 'AlsoNotAColor' + } | ConvertTo-Json + + Set-Content -LiteralPath $path -Value $bad -Encoding UTF8 + + $store = [NoteConfigStore]::new($path) + $store.Main | Should -Be 'Favorites' + $store.ExitOnCopy | Should -BeTrue + $store.ForegroundColor | Should -Be ([ConsoleColor]::Black) + $store.BackgroundColor | Should -Be ([ConsoleColor]::Gray) + } + + It 'fails safe on corrupt JSON' { + $path = Join-Path $env:PSNOTES_HOME 'config\config-corrupt.json' + $null = New-Item -ItemType Directory -Path (Split-Path $path) -Force + Set-Content -LiteralPath $path -Value '{ not json' -Encoding UTF8 + + { [NoteConfigStore]::new($path) } | Should -Not -Throw + + $store = [NoteConfigStore]::new($path) + $store.Main | Should -Be 'Favorites' + } + } +} + +Describe 'Error Handling and Edge Cases' { + Context 'PSNote edge cases' { + It 'handles null tags array' { + { $note = [PSNote]::new('Note', 'code', 'details', 'alias', $null) } | Should -Not -Throw + } + + It 'handles empty string for Note name' { + { $note = [PSNote]::new('', 'code', 'details', 'alias', @()) } | Should -Not -Throw + #$note.psobject.Properties['Note'].Value | Should -Be '' + } + } + + Context 'NoteCatalog edge cases' { + It 'handles catalog with very large note count' { + $catalog = [NoteCatalog]::new('LargeTest') + + for ($i = 1; $i -le 100; $i++) { + $note = [PSNote]::new( + "Note$i", + "code$i", + "details$i", + "alias$i", + @("tag$i") + ) + $catalog.Notes.Add($note) + } + + $catalog.Save() + $catalog2 = [NoteCatalog]::new('LargeTest') + + $catalog2.Notes.Count | Should -Be 100 + } + + It 'handles special characters in note properties' { + $catalog = [NoteCatalog]::new('SpecialCharTest') + $note = [PSNote]::new( + 'Note "with" quotes', + 'code with @#$% special chars', + 'details with `n newlines', + 'alias_with-dashes', + @('tag@special', 'tag#2') + ) + $catalog.Notes.Add($note) + + { $catalog.Save() } | Should -Not -Throw + + $catalog2 = [NoteCatalog]::new('SpecialCharTest') + $catalog2.Notes[0].Note | Should -Be 'Note "with" quotes' + } + } +} diff --git a/tests/UnitTests/Remove-PSNote.Tests.ps1 b/tests/UnitTests/Remove-PSNote.Tests.ps1 new file mode 100644 index 0000000..44a2b62 --- /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 -Note 'creds' -Catalog 'Work' + Get-PSnote -Note 'az-login' -Catalog 'Personal' + ) + + $r = $toRemove | Remove-PSNote -Confirm:$false + + @($r).Count | Should -Be 2 + Get-PSnote -Note 'creds' -Catalog 'Work' | Should -Be $null + Get-PSnote -Note 'az-login' -Catalog 'Personal' | Should -Be $null + } + + It "honors -WhatIf (does not call RemoveNote)" { + $toRemove = @( + Get-PSnote -Note 'creds2' -Catalog 'Work' + ) + + $null = $toRemove | Remove-PSNote -WhatIf + + Get-PSnote -Note 'creds2' -Catalog 'Work' | Should -Not -Be $null + } + + It "de-dupes piped notes by Catalog+Note" { + $toRemove = @( + Get-PSnote -Note 'creds2' -Catalog 'Work' + Get-PSnote -Note '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 Note/Tag/Catalog when using Note parameter set" { + $r = Remove-PSNote -Note 'cred*' -Tag 'AD' -Catalog 'Work' -Confirm:$false + Write-Host $r.Note + @($r).Count | Should -Be 1 + Get-PSnote -Note '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 -Note '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 -Note 'nope*' -Catalog 'Work' -Confirm:$false + + @($r).Count | Should -Be 0 + } + } +} diff --git a/tests/UnitTests/Set-PSNote.Tests.ps1 b/tests/UnitTests/Set-PSNote.Tests.ps1 new file mode 100644 index 0000000..2bd155e --- /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 -Note 'SetTestNote' -Snippet 'Get-Process' -Details 'Original details' -Tags 'Test' -Catalog 'Personal' + } + + It "updates Snippet of an existing note" { + Set-PSNote -Note 'SetTestNote' -Snippet 'Get-Service' -Catalog 'Personal' + + $result = Get-PSNote -Note 'SetTestNote' + $result.Snippet | Should -Be 'Get-Service' + } + + It "updates Details of an existing note" { + Set-PSNote -Note 'SetTestNote' -Details 'Updated details' -Catalog 'Personal' + + $result = Get-PSNote -Note '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 -Note 'SetTestNote' -Tags 'Updated', 'NewTag' -Catalog 'Personal' + + $result = Get-PSNote -Note 'SetTestNote' + $result.Tags | Should -Contain 'Updated' + $result.Tags | Should -Contain 'NewTag' + } + + It "updates Alias of an existing note" { + Set-PSNote -Note 'SetTestNote' -Alias 'new-alias' -Catalog 'Personal' + + $result = Get-PSNote -Note 'SetTestNote' + $result.Alias | Should -Be 'new-alias' + } + + It "updates with ScriptBlock parameter" { + $scriptBlock = { Get-ChildItem | Measure-Object } + Set-PSNote -Note 'SetTestNote' -ScriptBlock $scriptBlock -Catalog 'Personal' + + $result = Get-PSNote -Note '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 -Note 'SetTestNote' -ScriptPath $scriptFile -Catalog 'Personal' + + $result = Get-PSNote -Note 'SetTestNote' + $result.Snippet | Should -Be $scriptFile + $result.Kind | Should -Be 'Script' + } + + It "updates multiple properties at once" { + Set-PSNote -Note 'SetTestNote' -Snippet 'Get-Date' -Details 'New details' -Tags 'Updated','Time' -Alias 'date-alias' -Catalog 'Personal' + + $result = Get-PSNote -Note '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 -Note 'NonExistentNote' -Snippet 'Get-Date' -Details 'New note' -Catalog 'Personal' -WarningVariable warn -WarningAction SilentlyContinue + + $result = Get-PSNote -Note 'NonExistentNote' + $result.Note | 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 -Note 'CreatedNoteBlank' -Snippet 'Test' -Catalog 'Personal' + + $result = Get-PSNote -Note 'CreatedNoteBlank' + $result.Alias | Should -Be '' + } + + It "creates a new note with alias when creating non-existent note" { + Set-PSNote -Note 'CreatedNoteAlias' -Snippet 'Test' -Catalog 'Personal' -Alias 'CreatedNoteAlias' + + $result = Get-PSNote -Note 'CreatedNoteAlias' + $result.Alias | Should -Be 'CreatedNoteAlias' + } + + It "creates a new note with custom alias when creating non-existent note" { + Set-PSNote -Note 'CreatedWithAlias' -Snippet 'Test' -Alias 'custom-alias' -Catalog 'Personal' + + $result = Get-PSNote -Note '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 -Note 'CreatedFromScriptPath' -ScriptPath $scriptFile -Catalog 'Personal' + + $result = Get-PSNote -Note 'CreatedFromScriptPath' + $result.Snippet | Should -Be $scriptFile + $result.Kind | Should -Be 'Script' + } + } + + Context "Pipeline input" { + It "accepts Note from pipeline by property name" { + New-PSNote -Note 'PipelineTestNote' -Snippet 'Get-Process' -Catalog 'Personal' -Force + $noteObject = [PSCustomObject]@{ + Note = 'PipelineTestNote' + Snippet = 'Get-Service' + Catalog = 'Personal' + } + + $noteObject | Set-PSNote + + $result = Get-PSNote -Note 'PipelineTestNote' + $result.Snippet | Should -Be 'Get-Service' + } + + It "accepts Catalog from pipeline by property name" { + New-PSNote -Note 'PipelineTestNote' -Snippet 'Get-Process' -Catalog 'Personal' -Force + $noteObject = [PSCustomObject]@{ + Note = 'PipelineTestNote' + Snippet = 'Get-Content' + Catalog = 'Personal' + } + + $noteObject | Set-PSNote + + $result = Get-PSNote -Note 'PipelineTestNote' + $result.Snippet | Should -Be 'Get-Content' + } + + It "accepts ScriptPath from pipeline by property name" { + New-PSNote -Note 'PipelineScriptPathNote' -Snippet 'Get-Date' -Catalog 'Personal' -Force + $scriptFile = Join-Path $script:TestDir 'PipelineScriptPath.ps1' + Set-Content -Path $scriptFile -Value 'Get-ChildItem' -Force + + $noteObject = [PSCustomObject]@{ + Note = 'PipelineScriptPathNote' + ScriptPath = $scriptFile + Catalog = 'Personal' + } + + $noteObject | Set-PSNote + + $result = Get-PSNote -Note '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 -Note 'ValidAliasSet' -Alias 'valid-alias_123' -Snippet 'Test' -Catalog 'Personal' } | Should -Not -Throw + } + + It "throws an error for alias with spaces" { + { Set-PSNote -Note 'InvalidAliasSet1' -Alias 'invalid alias' -Snippet 'Test' -Catalog 'Personal' } | Should -Throw + } + + It "throws an error for alias with special characters" { + { Set-PSNote -Note 'InvalidAliasSet2' -Alias 'invalid@alias' -Snippet 'Test' -Catalog 'Personal' } | Should -Throw + } + } + + Context "Parameter sets" { + + It "accepts Snippet parameter" { + { Set-PSNote -Note 'SnippetParamSet' -Snippet 'Get-Date' -Catalog 'Personal' } | Should -Not -Throw + } + + It "accepts ScriptBlock parameter" { + { Set-PSNote -Note '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 -Note 'ScriptPathParamSet' -ScriptPath $scriptFile -Catalog 'Personal' } | Should -Not -Throw + } + + It "converts ScriptBlock to string for storage" { + $sb = { Get-Process | Select-Object -First 5 } + Set-PSNote -Note 'ScriptBlockConversionSet' -ScriptBlock $sb -Catalog 'Personal' + + $result = Get-PSNote -Note 'ScriptBlockConversionSet' + $result.Snippet | Should -Be $sb.ToString() + } + + It "throws when ScriptPath does not exist" { + $missingFile = Join-Path $script:TestDir 'MissingScriptPath.ps1' + { Set-PSNote -Note '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 -Note 'MultilineSetTest' -Snippet $multilineSnippet -Catalog 'Personal' + + $result = Get-PSNote -Note 'MultilineSetTest' + $result.Snippet | Should -Be $multilineSnippet + } + } + + Context "Global alias creation" { + + It "creates or updates a global alias for the note" { + Set-PSNote -Note '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..e047d26 --- /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]@{ + Note = 'MigrateDup01' + Snippet = 'blah' + Details = '' + Alias = 'dup1' + Tags = $null + Run = $false + Kind = 0 + }, + [pscustomobject]@{ + Note = '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/build-docs.ps1 b/tools/build-docs.ps1 new file mode 100644 index 0000000..40f84ca --- /dev/null +++ b/tools/build-docs.ps1 @@ -0,0 +1,46 @@ +$currentPath = (Get-Location).Path +$sourceRoot = Split-Path $PSScriptRoot -Parent +Set-Location -LiteralPath $sourceRoot + +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' | Remove-Item -Force + +New-MarkdownHelp -Module PSNotes -OutputFolder .\Documentation + + +$readme = Get-Content .\README.md +$docs = Get-ChildItem .\Documentation -Filter '*.md' | ForEach-Object{ + $content = Get-Content -LiteralPath $_.FullName + "| [$($_.BaseName)](Documentation/$($_.Name)) | $($content[$content.IndexOf('## SYNOPSIS')+2]) |" +} + +$commands = $false +$readmeupdate = foreach($line in $readme){ + if($line -eq '# Commands'){ + $commands = $true + $line + '' + '| Cmdlet | Synopsis |' + '| ------ | -------- |' + $docs + '' + '[top](#psnotes)' + } + elseif($commands -and $line -match '^#'){ + $commands = $false + } + + if(-not $commands){ + $line + } +} + +$readmeupdate | Out-File .\README.md + +Set-Location -LiteralPath $currentPath diff --git a/tools/build.ps1 b/tools/build.ps1 new file mode 100644 index 0000000..4e99d40 --- /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`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..2526198 --- /dev/null +++ b/tools/load-development.ps1 @@ -0,0 +1,63 @@ +param( + [switch]$Refresh +) +$Path = Join-Path (Split-Path $PSScriptRoot) 'src' + +$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" +Get-ChildItem -Path $env:PSNOTES_HOME -Filter '*.json' | Where-Object { $_.BaseName -notin 'Default' } | ForEach-Object { + $script:_noteStore.LoadCatalog($_.BaseName) +} \ 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..570b823 --- /dev/null +++ b/tools/tests.ps1 @@ -0,0 +1,37 @@ +$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.Path = (Join-Path $TestPath 'UnitTests') +$config.Run.Throw = $false +$config.TestResult.Enabled = $true +$config.TestResult.OutputFormat = 'JUnitXml' +$config.TestResult.OutputPath = (Join-Path $binPath 'Pester.TestResults.xml') +Invoke-Pester -Configuration $config + +# 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 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