From cf290775d602ab942bea0f6b743ae1d53992ca4d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 16:02:25 -0700 Subject: [PATCH 001/724] feat: OP Build workflow ( Fixes #8 ) --- .github/workflows/BuildOP.yml | 501 +++++++++++++++++++++ Build/GitHub/Jobs/BuildOP.psd1 | 64 +++ Build/GitHub/Steps/PublishTestResults.psd1 | 10 + Build/OP.GitHubWorkflow.PSDevOps.ps1 | 15 + Build/OP.ezout.ps1 | 39 ++ 5 files changed, 629 insertions(+) create mode 100644 .github/workflows/BuildOP.yml create mode 100644 Build/GitHub/Jobs/BuildOP.psd1 create mode 100644 Build/GitHub/Steps/PublishTestResults.psd1 create mode 100644 Build/OP.GitHubWorkflow.PSDevOps.ps1 create mode 100644 Build/OP.ezout.ps1 diff --git a/.github/workflows/BuildOP.yml b/.github/workflows/BuildOP.yml new file mode 100644 index 0000000..c2ed709 --- /dev/null +++ b/.github/workflows/BuildOP.yml @@ -0,0 +1,501 @@ + +name: Build OP Module +on: + push: + pull_request: + workflow_dispatch: +jobs: + TestPowerShellOnLinux: + runs-on: ubuntu-latest + steps: + - name: InstallPester + id: InstallPester + shell: pwsh + run: | + $Parameters = @{} + $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + Installs Pester + .Description + Installs Pester + #> + param( + # The maximum pester version. Defaults to 4.99.99. + [string] + $PesterMaxVersion = '4.99.99' + ) + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber + Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters + - name: Check out repository + uses: actions/checkout@v2 + - name: RunPester + id: RunPester + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} + $Parameters.NoCoverage = ${env:NoCoverage} + $Parameters.NoCoverage = $parameters.NoCoverage -match 'true'; + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + Runs Pester + .Description + Runs Pester tests after importing a PowerShell module + #> + param( + # The module path. If not provided, will default to the second half of the repository ID. + [string] + $ModulePath, + # The Pester max version. By default, this is pinned to 4.99.99. + [string] + $PesterMaxVersion = '4.99.99', + + # If set, will not collect code coverage. + [switch] + $NoCoverage + ) + + $global:ErrorActionPreference = 'continue' + $global:ProgressPreference = 'silentlycontinue' + + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + if (-not $ModulePath) { $ModulePath = ".\$moduleName.psd1" } + $importedPester = Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion + $importedModule = Import-Module $ModulePath -Force -PassThru + $importedPester, $importedModule | Out-Host + + $codeCoverageParameters = @{ + CodeCoverage = "$($importedModule | Split-Path)\*-*.ps1" + CodeCoverageOutputFile = ".\$moduleName.Coverage.xml" + } + + if ($NoCoverage) { + $codeCoverageParameters = @{} + } + + + $result = + Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml @codeCoverageParameters + + "::set-output name=TotalCount::$($result.TotalCount)", + "::set-output name=PassedCount::$($result.PassedCount)", + "::set-output name=FailedCount::$($result.FailedCount)" | Out-Host + if ($result.FailedCount -gt 0) { + "::debug:: $($result.FailedCount) tests failed" + foreach ($r in $result.TestResult) { + if (-not $r.Passed) { + "::error::$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)" + } + } + throw "::error:: $($result.FailedCount) tests failed" + } + } @Parameters + - name: PublishTestResults + uses: actions/upload-artifact@main + with: + name: PesterResults + path: '**.TestResults.xml' + if: ${{always()}} + TagReleaseAndPublish: + runs-on: ubuntu-latest + if: ${{ success() }} + steps: + - name: Check out repository + uses: actions/checkout@v2 + - name: TagModuleVersion + id: TagModuleVersion + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + $Parameters.TagVersionFormat = ${env:TagVersionFormat} + $Parameters.TagAnnotationFormat = ${env:TagAnnotationFormat} + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: TagModuleVersion $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + # The user email associated with a git commit. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName, + + # The tag version format (default value: 'v$(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagVersionFormat = 'v$($imported.Version)', + + # The tag version format (default value: '$($imported.Name) $(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagAnnotationFormat = '$($imported.Name) $($imported.Version)' + ) + + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping Tagging" | Out-Host + return + } + + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) + $existingTags = git tag --list + + @" + Target Version: $targetVersion + + Existing Tags: + $($existingTags -join [Environment]::NewLine) + "@ | Out-Host + + $versionTagExists = $existingTags | Where-Object { $_ -match $targetVersion } + + if ($versionTagExists) { + "::warning::Version $($versionTagExists)" + return + } + + if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } + if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } + git config --global user.email $UserEmail + git config --global user.name $UserName + + git tag -a $targetVersion -m $ExecutionContext.InvokeCommand.ExpandString($TagAnnotationFormat) + git push origin --tags + + if ($env:GITHUB_ACTOR) { + exit 0 + }} @Parameters + - name: ReleaseModule + id: ReleaseModule + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + $Parameters.TagVersionFormat = ${env:TagVersionFormat} + $Parameters.ReleaseNameFormat = ${env:ReleaseNameFormat} + $Parameters.ReleaseAsset = ${env:ReleaseAsset} + $Parameters.ReleaseAsset = $parameters.ReleaseAsset -split ';' -replace '^[''"]' -replace '[''"]$' + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: ReleaseModule $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + # The user email associated with a git commit. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName, + + # The tag version format (default value: 'v$(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagVersionFormat = 'v$($imported.Version)', + + # The release name format (default value: '$($imported.Name) $($imported.Version)') + [string] + $ReleaseNameFormat = '$($imported.Name) $($imported.Version)', + + # Any assets to attach to the release. Can be a wildcard or file name. + [string[]] + $ReleaseAsset + ) + + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping GitHub release" | Out-Host + return + } + + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) + $targetReleaseName = $targetVersion + $releasesURL = 'https://api.github.com/repos/${{github.repository}}/releases' + "Release URL: $releasesURL" | Out-Host + $listOfReleases = Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{ + "Accept" = "application/vnd.github.v3+json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } + + $releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion + + if ($releaseExists) { + "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host + $releasedIt = $releaseExists + } else { + $releasedIt = Invoke-RestMethod -Uri $releasesURL -Method Post -Body ( + [Ordered]@{ + owner = '${{github.owner}}' + repo = '${{github.repository}}' + tag_name = $targetVersion + name = $ExecutionContext.InvokeCommand.ExpandString($ReleaseNameFormat) + body = + if ($env:RELEASENOTES) { + $env:RELEASENOTES + } elseif ($imported.PrivateData.PSData.ReleaseNotes) { + $imported.PrivateData.PSData.ReleaseNotes + } else { + "$($imported.Name) $targetVersion" + } + draft = if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false } + prerelease = if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false } + } | ConvertTo-Json + ) -Headers @{ + "Accept" = "application/vnd.github.v3+json" + "Content-type" = "application/json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } + } + + + + + + if (-not $releasedIt) { + throw "Release failed" + } else { + $releasedIt | Out-Host + } + + $releaseUploadUrl = $releasedIt.upload_url -replace '\{.+$' + + if ($ReleaseAsset) { + $fileList = Get-ChildItem -Recurse + $filesToRelease = + @(:nextFile foreach ($file in $fileList) { + foreach ($relAsset in $ReleaseAsset) { + if ($relAsset -match '[\*\?]') { + if ($file.Name -like $relAsset) { + $file; continue nextFile + } + } elseif ($file.Name -eq $relAsset -or $file.FullName -eq $relAsset) { + $file; continue nextFile + } + } + }) + + $releasedFiles = @{} + foreach ($file in $filesToRelease) { + if ($releasedFiles[$file.Name]) { + Write-Warning "Already attached file $($file.Name)" + continue + } else { + $fileBytes = [IO.File]::ReadAllBytes($file.FullName) + $releasedFiles[$file.Name] = + Invoke-RestMethod -Uri "${releaseUploadUrl}?name=$($file.Name)" -Headers @{ + "Accept" = "application/vnd.github+json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } -Body $fileBytes -ContentType Application/octet-stream + $releasedFiles[$file.Name] + } + } + + "Attached $($releasedFiles.Count) file(s) to release" | Out-Host + } + + + + } @Parameters + - name: PublishPowerShellGallery + id: PublishPowerShellGallery + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.Exclude = ${env:Exclude} + $Parameters.Exclude = $parameters.Exclude -split ';' -replace '^[''"]' -replace '[''"]$' + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: PublishPowerShellGallery $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + [string[]] + $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif', 'docs[/\]*') + ) + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + if (-not $Exclude) { + $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif','docs[/\]*') + } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + @" + ::group::PSBoundParameters + $($PSBoundParameters | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping Gallery Publish" | Out-Host + return + } + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $foundModule = try { Find-Module -Name $imported.Name -ErrorAction SilentlyContinue} catch {} + + if ($foundModule -and (([Version]$foundModule.Version) -ge ([Version]$imported.Version))) { + "::warning::Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" | Out-Host + } else { + + $gk = '${{secrets.GALLERYKEY}}' + + $rn = Get-Random + $moduleTempFolder = Join-Path $pwd "$rn" + $moduleTempPath = Join-Path $moduleTempFolder $moduleName + New-Item -ItemType Directory -Path $moduleTempPath -Force | Out-Host + + Write-Host "Staging Directory: $ModuleTempPath" + + $imported | Split-Path | + Get-ChildItem -Force | + Where-Object Name -NE $rn | + Copy-Item -Destination $moduleTempPath -Recurse + + $moduleGitPath = Join-Path $moduleTempPath '.git' + Write-Host "Removing .git directory" + if (Test-Path $moduleGitPath) { + Remove-Item -Recurse -Force $moduleGitPath + } + + if ($Exclude) { + "::notice::Attempting to Exlcude $exclude" | Out-Host + Get-ChildItem $moduleTempPath -Recurse | + Where-Object { + foreach ($ex in $exclude) { + if ($_.FullName -like $ex) { + "::notice::Excluding $($_.FullName)" | Out-Host + return $true + } + } + } | + Remove-Item + } + + Write-Host "Module Files:" + Get-ChildItem $moduleTempPath -Recurse + Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery" + Publish-Module -Path $moduleTempPath -NuGetApiKey $gk + if ($?) { + Write-Host "Published to Gallery" + } else { + Write-Host "Gallery Publish Failed" + exit 1 + } + } + } @Parameters + BuildOP: + runs-on: ubuntu-latest + if: ${{ success() }} + steps: + - name: Check out repository + uses: actions/checkout@main + - name: UseEZOut + uses: StartAutomating/EZOut@master +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} diff --git a/Build/GitHub/Jobs/BuildOP.psd1 b/Build/GitHub/Jobs/BuildOP.psd1 new file mode 100644 index 0000000..2679de4 --- /dev/null +++ b/Build/GitHub/Jobs/BuildOP.psd1 @@ -0,0 +1,64 @@ +@{ + "runs-on" = "ubuntu-latest" + if = '${{ success() }}' + steps = @( + @{ + name = 'Check out repository' + uses = 'actions/checkout@main' + }, + 'RunEZOut' # , + + # 'BuildAndPublishContainer' + <#@{ + 'name'='Log in to ghcr.io' + 'uses'='docker/login-action@master' + 'with'=@{ + 'registry'='${{ env.REGISTRY }}' + 'username'='${{ github.actor }}' + 'password'='${{ secrets.GITHUB_TOKEN }}' + } + }, + @{ + name = 'Extract Docker Metadata (for branch)' + if = '${{github.ref_name != ''main'' && github.ref_name != ''master'' && github.ref_name != ''latest''}}' + id = 'meta' + uses = 'docker/metadata-action@master' + with = @{ + 'images'='${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}' + } + }, + @{ + name = 'Extract Docker Metadata (for main)' + if = '${{github.ref_name == ''main'' || github.ref_name == ''master'' || github.ref_name == ''latest''}}' + id = 'metaMain' + uses = 'docker/metadata-action@master' + with = @{ + 'images'='${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}' + 'flavor'='latest=true' + } + }, + @{ + name = 'Build and push Docker image (from main)' + if = '${{github.ref_name == ''main'' || github.ref_name == ''master'' || github.ref_name == ''latest''}}' + uses = 'docker/build-push-action@master' + with = @{ + 'context'='.' + 'push'='true' + 'tags'='${{ steps.metaMain.outputs.tags }}' + 'labels'='${{ steps.metaMain.outputs.labels }}' + } + }, + @{ + name = 'Build and push Docker image (from branch)' + if = '${{github.ref_name != ''main'' && github.ref_name != ''master'' && github.ref_name != ''latest''}}' + uses = 'docker/build-push-action@master' + with = @{ + 'context'='.' + 'push'='true' + 'tags'='${{ steps.meta.outputs.tags }}' + 'labels'='${{ steps.meta.outputs.labels }}' + } + } + #> + ) +} \ No newline at end of file diff --git a/Build/GitHub/Steps/PublishTestResults.psd1 b/Build/GitHub/Steps/PublishTestResults.psd1 new file mode 100644 index 0000000..e8111e8 --- /dev/null +++ b/Build/GitHub/Steps/PublishTestResults.psd1 @@ -0,0 +1,10 @@ +@{ + name = 'PublishTestResults' + uses = 'actions/upload-artifact@main' + with = @{ + name = 'PesterResults' + path = '**.TestResults.xml' + } + if = '${{always()}}' +} + diff --git a/Build/OP.GitHubWorkflow.PSDevOps.ps1 b/Build/OP.GitHubWorkflow.PSDevOps.ps1 new file mode 100644 index 0000000..688995b --- /dev/null +++ b/Build/OP.GitHubWorkflow.PSDevOps.ps1 @@ -0,0 +1,15 @@ +#requires -Module PSDevOps +Import-BuildStep -SourcePath ( + Join-Path $PSScriptRoot 'GitHub' +) -BuildSystem GitHubWorkflow + +Push-Location ($PSScriptRoot | Split-Path) +New-GitHubWorkflow -Name "Build OP Module" -On Push, + PullRequest, + Demand -Job TestPowerShellOnLinux, + TagReleaseAndPublish, BuildOP -Environment ([Ordered]@{ + REGISTRY = 'ghcr.io' + IMAGE_NAME = '${{ github.repository }}' + }) -OutputPath .\.github\workflows\BuildOP.yml + +Pop-Location \ No newline at end of file diff --git a/Build/OP.ezout.ps1 b/Build/OP.ezout.ps1 new file mode 100644 index 0000000..480ccb0 --- /dev/null +++ b/Build/OP.ezout.ps1 @@ -0,0 +1,39 @@ +#requires -Module EZOut +# Install-Module EZOut or https://github.com/StartAutomating/EZOut +$myFile = $MyInvocation.MyCommand.ScriptBlock.File +$myRoot = $myFile | Split-Path | Split-Path +$myModuleName = $myFile | Split-Path | Split-Path | Split-Path -Leaf +Push-Location $myRoot +$formatting = @( + # Add your own Write-FormatView here, + # or put them in a Formatting or Views directory + foreach ($potentialDirectory in 'Formatting','Views','Types') { + Join-Path $myRoot $potentialDirectory | + Get-ChildItem -ea ignore | + Import-FormatView -FilePath {$_.Fullname} + } +) + +$destinationRoot = $myRoot + +if ($formatting) { + $myFormatFilePath = Join-Path $destinationRoot "$myModuleName.format.ps1xml" + # You can also output to multiple paths by passing a hashtable to -OutputPath. + $formatting | Out-FormatData -Module $MyModuleName -OutputPath $myFormatFilePath +} + +$types = @( + # Add your own Write-TypeView statements here + # or declare them in the 'Types' directory + Join-Path $myRoot Types | + Get-Item -ea ignore | + Import-TypeView + +) + +if ($types) { + $myTypesFilePath = Join-Path $destinationRoot "$myModuleName.types.ps1xml" + # You can also output to multiple paths by passing a hashtable to -OutputPath. + $types | Out-TypeData -OutputPath $myTypesFilePath +} +Pop-Location From 0b7c705aa363c94350bf05bd9749a7abb8a70335 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 16:03:03 -0700 Subject: [PATCH 002/724] feat: Module scaffolding ( Fixes #1 ) --- OP.psm1 | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 OP.psm1 diff --git a/OP.psm1 b/OP.psm1 new file mode 100644 index 0000000..a12dd6d --- /dev/null +++ b/OP.psm1 @@ -0,0 +1,17 @@ +$CommandsPath = Join-Path $PSScriptRoot 'Commands' +foreach ($file in Get-ChildItem -Path $CommandsPath -Filter '*-*.ps1') { + if ($file.Name -like '*.*.ps1') { + continue + } + . $file.FullName +} + +if (-not ('IO.Packaging.Package' -as [type])) { + $addedTypes = Add-type -AssemblyName System.IO.Packaging -PassThru + $packageTypeFound = $addedTypes | Where-Object FullName -eq 'System.IO.Packaging.Package' + if (-not $packageTypeFound) { + Write-Warning "Could not find [IO.Packaging.Package]" + } +} + + From 9ccf0bf7635eb57ecab301bf9a4b353c07176162 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 16:35:06 -0700 Subject: [PATCH 003/724] feat: `OpenPackage.get/set_Identifier` ( Fixes #10 ) --- Types/OpenPackage/get_Identifier.ps1 | 1 + Types/OpenPackage/set_Identifier.ps1 | 1 + 2 files changed, 2 insertions(+) create mode 100644 Types/OpenPackage/get_Identifier.ps1 create mode 100644 Types/OpenPackage/set_Identifier.ps1 diff --git a/Types/OpenPackage/get_Identifier.ps1 b/Types/OpenPackage/get_Identifier.ps1 new file mode 100644 index 0000000..58bdc17 --- /dev/null +++ b/Types/OpenPackage/get_Identifier.ps1 @@ -0,0 +1 @@ +$this.PackageProperties.Identifier \ No newline at end of file diff --git a/Types/OpenPackage/set_Identifier.ps1 b/Types/OpenPackage/set_Identifier.ps1 new file mode 100644 index 0000000..2b8a7af --- /dev/null +++ b/Types/OpenPackage/set_Identifier.ps1 @@ -0,0 +1 @@ +$this.PackageProperties.Identifier = $args -join ' ' \ No newline at end of file From 6321c56282070dfbe85daeeb5a4cce2b88ad075e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 23:35:28 +0000 Subject: [PATCH 004/724] feat: `OpenPackage.get/set_Identifier` ( Fixes #10 ) --- OP.types.ps1xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 OP.types.ps1xml diff --git a/OP.types.ps1xml b/OP.types.ps1xml new file mode 100644 index 0000000..bfcc06f --- /dev/null +++ b/OP.types.ps1xml @@ -0,0 +1,17 @@ + + + + OpenPackage + + + Identifier + + $this.PackageProperties.Identifier + + + $this.PackageProperties.Identifier = $args -join ' ' + + + + + \ No newline at end of file From 88ff7f5bf896f71b7c937ebf96063fbddd087e55 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 16:36:52 -0700 Subject: [PATCH 005/724] feat: `OpenPackage.get/set_Description` ( Fixes #11 ) --- Types/OpenPackage/get_Description.ps1 | 1 + Types/OpenPackage/set_Description.ps1 | 1 + 2 files changed, 2 insertions(+) create mode 100644 Types/OpenPackage/get_Description.ps1 create mode 100644 Types/OpenPackage/set_Description.ps1 diff --git a/Types/OpenPackage/get_Description.ps1 b/Types/OpenPackage/get_Description.ps1 new file mode 100644 index 0000000..931a508 --- /dev/null +++ b/Types/OpenPackage/get_Description.ps1 @@ -0,0 +1 @@ +return $this.PackageProperties.Description \ No newline at end of file diff --git a/Types/OpenPackage/set_Description.ps1 b/Types/OpenPackage/set_Description.ps1 new file mode 100644 index 0000000..292e701 --- /dev/null +++ b/Types/OpenPackage/set_Description.ps1 @@ -0,0 +1 @@ +$this.PackageProperties.Description = $args -join [Environment]::NewLine \ No newline at end of file From d090620fd7f161fbe6e2167e103a9b7187424f12 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 23:37:09 +0000 Subject: [PATCH 006/724] feat: `OpenPackage.get/set_Description` ( Fixes #11 ) --- OP.types.ps1xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index bfcc06f..33a1fee 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3,6 +3,15 @@ OpenPackage + + Description + + return $this.PackageProperties.Description + + + $this.PackageProperties.Description = $args -join [Environment]::NewLine + + Identifier From 89788ae85aa27d16f74a056fffc029c0429dee28 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 16:43:22 -0700 Subject: [PATCH 007/724] feat: `OpenPackage.get/set_Created` ( Fixes #12 ) --- Types/OpenPackage/get_Created.ps1 | 11 +++++++++++ Types/OpenPackage/set_Created.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Created.ps1 create mode 100644 Types/OpenPackage/set_Created.ps1 diff --git a/Types/OpenPackage/get_Created.ps1 b/Types/OpenPackage/get_Created.ps1 new file mode 100644 index 0000000..d91cb1e --- /dev/null +++ b/Types/OpenPackage/get_Created.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage creation time +.DESCRIPTION + Gets the OpenPackage `Created` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Created \ No newline at end of file diff --git a/Types/OpenPackage/set_Created.ps1 b/Types/OpenPackage/set_Created.ps1 new file mode 100644 index 0000000..4f6aaf4 --- /dev/null +++ b/Types/OpenPackage/set_Created.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Created` +.DESCRIPTION + Sets the OpenPackage `Created` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 +#> +param([DateTime]$Created) + +$this.PackageProperties.Created = $Created \ No newline at end of file From 7e7db5c8c71288ffbbe32789642f5d604e9807c0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 23:44:44 +0000 Subject: [PATCH 008/724] feat: `OpenPackage.get/set_Created` ( Fixes #12 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 33a1fee..de68709 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3,6 +3,35 @@ OpenPackage + + Created + + <# +.SYNOPSIS + Gets OpenPackage creation time +.DESCRIPTION + Gets the OpenPackage `Created` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Created + + + <# +.SYNOPSIS + Sets OpenPackage `Created` +.DESCRIPTION + Sets the OpenPackage `Created` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 +#> +param([DateTime]$Created) + +$this.PackageProperties.Created = $Created + + Description From 635322ba046b5ffcd2cb7d55718872d12274a832 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 16:45:03 -0700 Subject: [PATCH 009/724] feat: `OpenPackage.get/set_Modified` ( Fixes #13 ) --- Types/OpenPackage/get_Modified.ps1 | 11 +++++++++++ Types/OpenPackage/set_Modified.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Modified.ps1 create mode 100644 Types/OpenPackage/set_Modified.ps1 diff --git a/Types/OpenPackage/get_Modified.ps1 b/Types/OpenPackage/get_Modified.ps1 new file mode 100644 index 0000000..82d4157 --- /dev/null +++ b/Types/OpenPackage/get_Modified.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage modified time +.DESCRIPTION + Gets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Modified \ No newline at end of file diff --git a/Types/OpenPackage/set_Modified.ps1 b/Types/OpenPackage/set_Modified.ps1 new file mode 100644 index 0000000..92908d8 --- /dev/null +++ b/Types/OpenPackage/set_Modified.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Modified` +.DESCRIPTION + Sets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 +#> +param([DateTime]$Modified) + +$this.PackageProperties.Modifiedd = $Modified \ No newline at end of file From 255a978a7d0ff9d6bf5077f66397fe96d4b954a3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 23:45:21 +0000 Subject: [PATCH 010/724] feat: `OpenPackage.get/set_Modified` ( Fixes #13 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index de68709..7eb6641 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -50,6 +50,35 @@ $this.PackageProperties.Created = $Created $this.PackageProperties.Identifier = $args -join ' ' + + Modified + + <# +.SYNOPSIS + Gets OpenPackage modified time +.DESCRIPTION + Gets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Modified + + + <# +.SYNOPSIS + Sets OpenPackage `Modified` +.DESCRIPTION + Sets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 +#> +param([DateTime]$Modified) + +$this.PackageProperties.Modifiedd = $Modified + + \ No newline at end of file From a992d9b7a74a7e9a139c6cc7ae369e70b91a434b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 17:05:44 -0700 Subject: [PATCH 011/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) --- .../DefaultTypeMap.psd1 | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 diff --git a/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 new file mode 100644 index 0000000..14c6733 --- /dev/null +++ b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 @@ -0,0 +1,87 @@ +@{ + '.aac' = 'audio/aac' + '.abw' = 'application/x-abiword' + '.apng' = 'image/apng' + '.arc' = 'application/x-freearc' + '.avif' = 'image/avif' + '.avi' = 'video/x-msvideo' + '.azw' = 'application/vnd.amazon.ebook' + '.bin' = 'application/octet-stream' + '.bmp' = 'image/bmp' + '.bz' = 'application/x-bzip' + '.bz2' = 'application/x-bzip2' + '.cda' = 'application/x-cdf' + '.csh' = 'application/x-csh' + '.css' = 'text/css' + '.csv' = 'text/csv' + '.doc' = 'application/msword' + '.docx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + '.eot' = 'application/vnd.ms-fontobject' + '.epub' = 'application/epub+zip' + '.exe' = 'application/executeable' + '.gz' = 'application/gzip' + '.gif' = 'image/gif' + '.htm' = 'text/html' + '.html' = 'text/html' + '.ico' = 'image/vnd.microsoft.icon' + '.ics' = 'text/calendar' + '.jar' = 'application/java-archive' + '.jpeg' = 'image/jpeg' + '.jpg' = 'image/jpeg' + '.js' = 'text/javascript' + '.jsm' = 'text/javascript' + '.json' = 'application/json' + '.jsonld' = 'application/ld+json' + '.md' = 'text/markdown' + '.mid' = 'audio/midi' + '.midi' = 'audio/midi' + '.mjs' = 'text/javascript' + '.mp3' = 'audio/mpeg' + '.mp4' = 'video/mp4' + '.mpeg' = 'video/mpeg' + '.mpkg' = 'application/vnd.apple.installer+xml' + '.odp' = 'application/vnd.oasis.opendocument.presentation' + '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' + '.odt' = 'application/vnd.oasis.opendocument.text' + '.oga' = 'audio/ogg' + '.ogv' = 'video/ogg' + '.ogx' = 'application/ogg' + '.opus' = 'audio/ogg' + '.otf' = 'font/otf' + '.png' = 'image/png' + '.pdf' = 'application/pdf' + '.php' = 'application/x-httpd-php' + '.ps1' = 'text/x-powershell' + '.ps1xml' = 'text/x-powershell+xml' + '.psm1' = 'text/x-powershell' + '.psd1' = 'text/x-powershell-data' + '.ppt' = 'application/vnd.ms-powerpoint' + '.pptx' = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + '.rar' = 'application/vnd.rar' + '.rtf' = 'application/rtf' + '.sh' = 'application/x-sh' + '.svg' = 'image/svg+xml' + '.tar' = 'application/x-tar' + '.tif' = 'image/tiff' + '.tiff' = 'image/tiff' + '.ts' = 'application/x-typescript' + '.ttf' = 'font/ttf' + '.txt' = 'text/plain' + '.vsd' = 'application/vnd.visio' + '.wav' = 'audio/wav' + '.weba' = 'audio/webm' + '.webm' = 'video/webm' + '.webmanifest' = 'application/manifest+json' + '.webp' = 'image/webp' + '.woff' = 'font/woff' + '.woff2' = 'font/woff2' + '.xhtml' = 'application/xhtml+xml' + '.xls' = 'application/vnd.ms-excel' + '.xlsx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + '.xml' = 'application/xml' + '.xul' = 'application/vnd.mozilla.xul+xml' + '.zip' = 'application/zip' + '.3gp' = 'video/3gpp' + '.3g2' = 'video/3gpp2' + '.7z' = 'application/x-7z-compressed' +} \ No newline at end of file From 1c38a3249333ab059b426874edc2b1866d9a9c1c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 00:06:04 +0000 Subject: [PATCH 012/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) --- OP.types.ps1xml | 97 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 7eb6641..8210ea7 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -81,4 +81,101 @@ $this.PackageProperties.Modifiedd = $Modified + + OpenPackage.ContentTypeMap + + + DefaultTypeMap + + data { @{ + '.aac' = 'audio/aac' + '.abw' = 'application/x-abiword' + '.apng' = 'image/apng' + '.arc' = 'application/x-freearc' + '.avif' = 'image/avif' + '.avi' = 'video/x-msvideo' + '.azw' = 'application/vnd.amazon.ebook' + '.bin' = 'application/octet-stream' + '.bmp' = 'image/bmp' + '.bz' = 'application/x-bzip' + '.bz2' = 'application/x-bzip2' + '.cda' = 'application/x-cdf' + '.csh' = 'application/x-csh' + '.css' = 'text/css' + '.csv' = 'text/csv' + '.doc' = 'application/msword' + '.docx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + '.eot' = 'application/vnd.ms-fontobject' + '.epub' = 'application/epub+zip' + '.exe' = 'application/executeable' + '.gz' = 'application/gzip' + '.gif' = 'image/gif' + '.htm' = 'text/html' + '.html' = 'text/html' + '.ico' = 'image/vnd.microsoft.icon' + '.ics' = 'text/calendar' + '.jar' = 'application/java-archive' + '.jpeg' = 'image/jpeg' + '.jpg' = 'image/jpeg' + '.js' = 'text/javascript' + '.jsm' = 'text/javascript' + '.json' = 'application/json' + '.jsonld' = 'application/ld+json' + '.md' = 'text/markdown' + '.mid' = 'audio/midi' + '.midi' = 'audio/midi' + '.mjs' = 'text/javascript' + '.mp3' = 'audio/mpeg' + '.mp4' = 'video/mp4' + '.mpeg' = 'video/mpeg' + '.mpkg' = 'application/vnd.apple.installer+xml' + '.odp' = 'application/vnd.oasis.opendocument.presentation' + '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' + '.odt' = 'application/vnd.oasis.opendocument.text' + '.oga' = 'audio/ogg' + '.ogv' = 'video/ogg' + '.ogx' = 'application/ogg' + '.opus' = 'audio/ogg' + '.otf' = 'font/otf' + '.png' = 'image/png' + '.pdf' = 'application/pdf' + '.php' = 'application/x-httpd-php' + '.ps1' = 'text/x-powershell' + '.ps1xml' = 'text/x-powershell+xml' + '.psm1' = 'text/x-powershell' + '.psd1' = 'text/x-powershell-data' + '.ppt' = 'application/vnd.ms-powerpoint' + '.pptx' = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + '.rar' = 'application/vnd.rar' + '.rtf' = 'application/rtf' + '.sh' = 'application/x-sh' + '.svg' = 'image/svg+xml' + '.tar' = 'application/x-tar' + '.tif' = 'image/tiff' + '.tiff' = 'image/tiff' + '.ts' = 'application/x-typescript' + '.ttf' = 'font/ttf' + '.txt' = 'text/plain' + '.vsd' = 'application/vnd.visio' + '.wav' = 'audio/wav' + '.weba' = 'audio/webm' + '.webm' = 'video/webm' + '.webmanifest' = 'application/manifest+json' + '.webp' = 'image/webp' + '.woff' = 'font/woff' + '.woff2' = 'font/woff2' + '.xhtml' = 'application/xhtml+xml' + '.xls' = 'application/vnd.ms-excel' + '.xlsx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + '.xml' = 'application/xml' + '.xul' = 'application/vnd.mozilla.xul+xml' + '.zip' = 'application/zip' + '.3gp' = 'video/3gpp' + '.3g2' = 'video/3gpp2' + '.7z' = 'application/x-7z-compressed' +} } + + + + \ No newline at end of file From d012787461692338311de5fd200000592501eeda Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 17:11:30 -0700 Subject: [PATCH 013/724] feat: `OpenPackage.ContentTypeMap.get/set_TypeMap` ( Fixes #15 ) --- .../get_TypeMap.ps1 | 18 ++++++++++++++ .../set_TypeMap.ps1 | 24 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 Types/OpenPackage.ContentTypeMap/get_TypeMap.ps1 create mode 100644 Types/OpenPackage.ContentTypeMap/set_TypeMap.ps1 diff --git a/Types/OpenPackage.ContentTypeMap/get_TypeMap.ps1 b/Types/OpenPackage.ContentTypeMap/get_TypeMap.ps1 new file mode 100644 index 0000000..d303cc2 --- /dev/null +++ b/Types/OpenPackage.ContentTypeMap/get_TypeMap.ps1 @@ -0,0 +1,18 @@ +<# +.SYNOPSIS + Gets the TypeMap +.DESCRIPTION + Gets the TypeMap. + + Will default to the values in `DefaultTypeMap` +#> +if ($this.'.TypeMap') { + return $this.'.TypeMap' +} +$typeMap = [Ordered]@{} +$defaultTypeMap = $this.DefaultTypeMap +foreach ($extension in ($defaultTypeMap.Keys | Sort-Object)) { + $typeMap[$extension] = $defaultTypeMap[$extension] +} +$this | Add-Member NoteProperty $typeMap '.TypeMap' -Force +return $typeMap \ No newline at end of file diff --git a/Types/OpenPackage.ContentTypeMap/set_TypeMap.ps1 b/Types/OpenPackage.ContentTypeMap/set_TypeMap.ps1 new file mode 100644 index 0000000..1ef969b --- /dev/null +++ b/Types/OpenPackage.ContentTypeMap/set_TypeMap.ps1 @@ -0,0 +1,24 @@ +<# +.SYNOPSIS + Sets the TypeMap +.DESCRIPTION + Sets the TypeMap. + + If an empty dictionary is provided, will clear the typemap. + + Any other values provided will override the existing content type map. +#> +param( +[Collections.IDictionary] +$TypeMap +) + +$thisTypeMap = $this.TypeMap + +if ($TypeMap.Count -eq 0) { + $thisTypeMap.Clear() +} else { + foreach ($key in $TypeMap.Keys) { + $thisTypeMap[$key] = $TypeMap[$key] + } +} \ No newline at end of file From 6ebe9731dd31ed69bcd053f48ab64ee4a876c924 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 00:11:47 +0000 Subject: [PATCH 014/724] feat: `OpenPackage.ContentTypeMap.get/set_TypeMap` ( Fixes #15 ) --- OP.types.ps1xml | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 8210ea7..8c1b240 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -176,6 +176,55 @@ $this.PackageProperties.Modifiedd = $Modified } } + + TypeMap + + <# +.SYNOPSIS + Gets the TypeMap +.DESCRIPTION + Gets the TypeMap. + + Will default to the values in `DefaultTypeMap` +#> +if ($this.'.TypeMap') { + return $this.'.TypeMap' +} +$typeMap = [Ordered]@{} +$defaultTypeMap = $this.DefaultTypeMap +foreach ($extension in ($defaultTypeMap.Keys | Sort-Object)) { + $typeMap[$extension] = $defaultTypeMap[$extension] +} +$this | Add-Member NoteProperty $typeMap '.TypeMap' -Force +return $typeMap + + + <# +.SYNOPSIS + Sets the TypeMap +.DESCRIPTION + Sets the TypeMap. + + If an empty dictionary is provided, will clear the typemap. + + Any other values provided will override the existing content type map. +#> +param( +[Collections.IDictionary] +$TypeMap +) + +$thisTypeMap = $this.TypeMap + +if ($TypeMap.Count -eq 0) { + $thisTypeMap.Clear() +} else { + foreach ($key in $TypeMap.Keys) { + $thisTypeMap[$key] = $TypeMap[$key] + } +} + + \ No newline at end of file From 30eb7ce8be3e0e422470e6a2555d7899953e3883 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 17:25:42 -0700 Subject: [PATCH 015/724] feat: `OpenPackage.get/set_Version` ( Fixes #17 ) --- Types/OpenPackage/get_Version.ps1 | 11 +++++++++++ Types/OpenPackage/set_Version.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Version.ps1 create mode 100644 Types/OpenPackage/set_Version.ps1 diff --git a/Types/OpenPackage/get_Version.ps1 b/Types/OpenPackage/get_Version.ps1 new file mode 100644 index 0000000..0a1d22b --- /dev/null +++ b/Types/OpenPackage/get_Version.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage `Version` +.DESCRIPTION + Gets the OpenPackage `Version` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Version \ No newline at end of file diff --git a/Types/OpenPackage/set_Version.ps1 b/Types/OpenPackage/set_Version.ps1 new file mode 100644 index 0000000..e431594 --- /dev/null +++ b/Types/OpenPackage/set_Version.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Version` +.DESCRIPTION + Sets the OpenPackage `Version` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 +#> +param([string]$Version) + +$this.PackageProperties.Modifiedd = $Version \ No newline at end of file From 504958d438889b5a929e2053e07a19abf40fd7d5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 00:26:02 +0000 Subject: [PATCH 016/724] feat: `OpenPackage.get/set_Version` ( Fixes #17 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 8c1b240..833630f 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -79,6 +79,35 @@ param([DateTime]$Modified) $this.PackageProperties.Modifiedd = $Modified + + Version + + <# +.SYNOPSIS + Gets OpenPackage `Version` +.DESCRIPTION + Gets the OpenPackage `Version` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Version + + + <# +.SYNOPSIS + Sets OpenPackage `Version` +.DESCRIPTION + Sets the OpenPackage `Version` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 +#> +param([string]$Version) + +$this.PackageProperties.Modifiedd = $Version + + From 8a73ed0aba99b1de6226895b7be86881c3f6c4ed Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 17:51:37 -0700 Subject: [PATCH 017/724] feat: `OpenPackage.get_FileList` ( Fixes #16 ) --- Types/OpenPackage/DefaultDisplay.txt | 2 ++ Types/OpenPackage/get_FileList.ps1 | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 Types/OpenPackage/DefaultDisplay.txt create mode 100644 Types/OpenPackage/get_FileList.ps1 diff --git a/Types/OpenPackage/DefaultDisplay.txt b/Types/OpenPackage/DefaultDisplay.txt new file mode 100644 index 0000000..6044d11 --- /dev/null +++ b/Types/OpenPackage/DefaultDisplay.txt @@ -0,0 +1,2 @@ +Identifier +FileList \ No newline at end of file diff --git a/Types/OpenPackage/get_FileList.ps1 b/Types/OpenPackage/get_FileList.ps1 new file mode 100644 index 0000000..3f56c05 --- /dev/null +++ b/Types/OpenPackage/get_FileList.ps1 @@ -0,0 +1,12 @@ +<# +.SYNOPSIS + Gets OpenPackage file list +.DESCRIPTION + Gets the list of files in an OpenPackage. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.package.getparts?wt.mc_id=MVP_321542 +#> +[OutputType([string[]])] +param() + +@($this.GetParts()).Uri -as [string[]] \ No newline at end of file From aeb47e5312245850d3cc22d73a45b336553fdb0c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 00:52:00 +0000 Subject: [PATCH 018/724] feat: `OpenPackage.get_FileList` ( Fixes #16 ) --- OP.types.ps1xml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 833630f..e2d4d39 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3,6 +3,18 @@ OpenPackage + + PSStandardMembers + + + DefaultDisplayPropertySet + + Identifier + FileList + + + + Created @@ -41,6 +53,23 @@ $this.PackageProperties.Created = $Created $this.PackageProperties.Description = $args -join [Environment]::NewLine + + FileList + + <# +.SYNOPSIS + Gets OpenPackage file list +.DESCRIPTION + Gets the list of files in an OpenPackage. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.package.getparts?wt.mc_id=MVP_321542 +#> +[OutputType([string[]])] +param() + +@($this.GetParts()).Uri -as [string[]] + + Identifier @@ -108,6 +137,11 @@ param([string]$Version) $this.PackageProperties.Modifiedd = $Version + + DefaultDisplay + Identifier +FileList + From 4d078f3613e3dab0c3f23a545c18e648d195d9b0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 17:54:44 -0700 Subject: [PATCH 019/724] feat: `OpenPackage.get/set_Version` ( Fixes #17 ) Updating implementation --- Types/OpenPackage/set_Version.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Types/OpenPackage/set_Version.ps1 b/Types/OpenPackage/set_Version.ps1 index e431594..2e642df 100644 --- a/Types/OpenPackage/set_Version.ps1 +++ b/Types/OpenPackage/set_Version.ps1 @@ -8,4 +8,4 @@ #> param([string]$Version) -$this.PackageProperties.Modifiedd = $Version \ No newline at end of file +$this.PackageProperties.Version = $Version \ No newline at end of file From 644bf24688328878e3cee55f8d9bc1d33b397b22 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 00:55:04 +0000 Subject: [PATCH 020/724] feat: `OpenPackage.get/set_Version` ( Fixes #17 ) Updating implementation --- OP.types.ps1xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index e2d4d39..22a2434 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -134,7 +134,7 @@ $this.PackageProperties.Version #> param([string]$Version) -$this.PackageProperties.Modifiedd = $Version +$this.PackageProperties.Version = $Version From fc3adb15a94d1e16046e27e6972f25512932ac14 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 17:56:05 -0700 Subject: [PATCH 021/724] feat: `OpenPackage.get/set_Revision` ( Fixes #18 ) --- Types/OpenPackage/get_Revision.ps1 | 11 +++++++++++ Types/OpenPackage/set_Revision.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Revision.ps1 create mode 100644 Types/OpenPackage/set_Revision.ps1 diff --git a/Types/OpenPackage/get_Revision.ps1 b/Types/OpenPackage/get_Revision.ps1 new file mode 100644 index 0000000..7bd6a54 --- /dev/null +++ b/Types/OpenPackage/get_Revision.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage `Revision` +.DESCRIPTION + Gets the OpenPackage `Revision` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Revision \ No newline at end of file diff --git a/Types/OpenPackage/set_Revision.ps1 b/Types/OpenPackage/set_Revision.ps1 new file mode 100644 index 0000000..1aede6c --- /dev/null +++ b/Types/OpenPackage/set_Revision.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Revision` +.DESCRIPTION + Sets the OpenPackage `Revision` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 +#> +param([string]$Revision) + +$this.PackageProperties.Revision = $Revision \ No newline at end of file From 1cd48fb35b179ba8a4ce5e268d25e5869fbea508 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 00:56:26 +0000 Subject: [PATCH 022/724] feat: `OpenPackage.get/set_Revision` ( Fixes #18 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 22a2434..5e8d45d 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -108,6 +108,35 @@ param([DateTime]$Modified) $this.PackageProperties.Modifiedd = $Modified + + Revision + + <# +.SYNOPSIS + Gets OpenPackage `Revision` +.DESCRIPTION + Gets the OpenPackage `Revision` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Revision + + + <# +.SYNOPSIS + Sets OpenPackage `Revision` +.DESCRIPTION + Sets the OpenPackage `Revision` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 +#> +param([string]$Revision) + +$this.PackageProperties.Revision = $Revision + + Version From da680f2f2edb94d054822be41bb7d4b106602541 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:00:18 -0700 Subject: [PATCH 023/724] feat: `OpenPackage.get/set_Creator` ( Fixes #19 ) --- Types/OpenPackage/get_Creator.ps1 | 11 +++++++++++ Types/OpenPackage/set_Creator.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Creator.ps1 create mode 100644 Types/OpenPackage/set_Creator.ps1 diff --git a/Types/OpenPackage/get_Creator.ps1 b/Types/OpenPackage/get_Creator.ps1 new file mode 100644 index 0000000..abfc739 --- /dev/null +++ b/Types/OpenPackage/get_Creator.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage `Creator` +.DESCRIPTION + Gets the OpenPackage `Creator` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Creator \ No newline at end of file diff --git a/Types/OpenPackage/set_Creator.ps1 b/Types/OpenPackage/set_Creator.ps1 new file mode 100644 index 0000000..cb73414 --- /dev/null +++ b/Types/OpenPackage/set_Creator.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Creator` +.DESCRIPTION + Sets the OpenPackage `Creator` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 +#> +param([string]$Creator) + +$this.PackageProperties.Creator = $Creator \ No newline at end of file From 31746ec4125cb89c78df73e47c0d360e65ae1fd3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:00:40 +0000 Subject: [PATCH 024/724] feat: `OpenPackage.get/set_Creator` ( Fixes #19 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 5e8d45d..454fd7b 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -44,6 +44,35 @@ param([DateTime]$Created) $this.PackageProperties.Created = $Created + + Creator + + <# +.SYNOPSIS + Gets OpenPackage `Creator` +.DESCRIPTION + Gets the OpenPackage `Creator` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Creator + + + <# +.SYNOPSIS + Sets OpenPackage `Creator` +.DESCRIPTION + Sets the OpenPackage `Creator` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 +#> +param([string]$Creator) + +$this.PackageProperties.Creator = $Creator + + Description From a3cdd3f0a6d47d1558ff4ca3730ede27bbc91541 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:04:02 -0700 Subject: [PATCH 025/724] feat: `OpenPackage.get/set_Keywords` ( Fixes #20 ) --- Types/OpenPackage/get_Keywords.ps1 | 11 +++++++++++ Types/OpenPackage/set_Keywords.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Keywords.ps1 create mode 100644 Types/OpenPackage/set_Keywords.ps1 diff --git a/Types/OpenPackage/get_Keywords.ps1 b/Types/OpenPackage/get_Keywords.ps1 new file mode 100644 index 0000000..5a27f9c --- /dev/null +++ b/Types/OpenPackage/get_Keywords.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage `Keywords` +.DESCRIPTION + Gets the OpenPackage `Keywords` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Keywords -split '[\s\r\n]+' \ No newline at end of file diff --git a/Types/OpenPackage/set_Keywords.ps1 b/Types/OpenPackage/set_Keywords.ps1 new file mode 100644 index 0000000..c62fe2d --- /dev/null +++ b/Types/OpenPackage/set_Keywords.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Keywords` +.DESCRIPTION + Sets the OpenPackage `Keywords` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Keywords = $args -join ' ' \ No newline at end of file From ca1cbcf6b30fc44a1981db2de305012b3a03fe4c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:04:22 +0000 Subject: [PATCH 026/724] feat: `OpenPackage.get/set_Keywords` ( Fixes #20 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 454fd7b..0883321 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -108,6 +108,35 @@ param() $this.PackageProperties.Identifier = $args -join ' ' + + Keywords + + <# +.SYNOPSIS + Gets OpenPackage `Keywords` +.DESCRIPTION + Gets the OpenPackage `Keywords` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Keywords -split '[\s\r\n]+' + + + <# +.SYNOPSIS + Sets OpenPackage `Keywords` +.DESCRIPTION + Sets the OpenPackage `Keywords` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Keywords = $args -join ' ' + + Modified From e14bc3c7c449f4854b552a33a6551c6d980590d5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:15:58 -0700 Subject: [PATCH 027/724] feat: `OpenPackage.get/set_Subject` ( Fixes #21 ) --- Types/OpenPackage/get_Subject.ps1 | 11 +++++++++++ Types/OpenPackage/set_Subject.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Subject.ps1 create mode 100644 Types/OpenPackage/set_Subject.ps1 diff --git a/Types/OpenPackage/get_Subject.ps1 b/Types/OpenPackage/get_Subject.ps1 new file mode 100644 index 0000000..3885796 --- /dev/null +++ b/Types/OpenPackage/get_Subject.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage `Subject` +.DESCRIPTION + Gets the OpenPackage `Subject` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Subject \ No newline at end of file diff --git a/Types/OpenPackage/set_Subject.ps1 b/Types/OpenPackage/set_Subject.ps1 new file mode 100644 index 0000000..3f35bb1 --- /dev/null +++ b/Types/OpenPackage/set_Subject.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Subject` +.DESCRIPTION + Sets the OpenPackage `Subject` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 +#> +param([string]$Subject) + +$this.PackageProperties.Subject = $Subject \ No newline at end of file From 4952f54d86802be6f20f1ee8ae3854b9b0bf115a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:16:18 +0000 Subject: [PATCH 028/724] feat: `OpenPackage.get/set_Subject` ( Fixes #21 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0883321..68e4764 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -195,6 +195,35 @@ param([string]$Revision) $this.PackageProperties.Revision = $Revision + + Subject + + <# +.SYNOPSIS + Gets OpenPackage `Subject` +.DESCRIPTION + Gets the OpenPackage `Subject` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Subject + + + <# +.SYNOPSIS + Sets OpenPackage `Subject` +.DESCRIPTION + Sets the OpenPackage `Subject` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 +#> +param([string]$Subject) + +$this.PackageProperties.Subject = $Subject + + Version From e306315739e73658b2536f1feb801e3009ba6c51 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:20:46 -0700 Subject: [PATCH 029/724] feat: `OpenPackage.get/set_Title` ( Fixes #22 ) --- Types/OpenPackage/get_Title.ps1 | 11 +++++++++++ Types/OpenPackage/set_Title.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Title.ps1 create mode 100644 Types/OpenPackage/set_Title.ps1 diff --git a/Types/OpenPackage/get_Title.ps1 b/Types/OpenPackage/get_Title.ps1 new file mode 100644 index 0000000..eff3b41 --- /dev/null +++ b/Types/OpenPackage/get_Title.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage `Title` +.DESCRIPTION + Gets the OpenPackage `Title` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Title \ No newline at end of file diff --git a/Types/OpenPackage/set_Title.ps1 b/Types/OpenPackage/set_Title.ps1 new file mode 100644 index 0000000..a6ee923 --- /dev/null +++ b/Types/OpenPackage/set_Title.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Title` +.DESCRIPTION + Sets the OpenPackage `Title` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 +#> +param([string]$Title) + +$this.PackageProperties.Title = $Title \ No newline at end of file From 0bb0220e0867e0b1a1c798658019eaaa1c41d4aa Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:21:06 +0000 Subject: [PATCH 030/724] feat: `OpenPackage.get/set_Title` ( Fixes #22 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 68e4764..2ae981d 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -224,6 +224,35 @@ param([string]$Subject) $this.PackageProperties.Subject = $Subject + + Title + + <# +.SYNOPSIS + Gets OpenPackage `Title` +.DESCRIPTION + Gets the OpenPackage `Title` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Title + + + <# +.SYNOPSIS + Sets OpenPackage `Title` +.DESCRIPTION + Sets the OpenPackage `Title` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 +#> +param([string]$Title) + +$this.PackageProperties.Title = $Title + + Version From a04f7331323097a1dbd5769ce3832610f53c5798 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:23:22 -0700 Subject: [PATCH 031/724] feat: `OpenPackage.get/set_Category` ( Fixes #23 ) --- Types/OpenPackage/get_Category.ps1 | 11 +++++++++++ Types/OpenPackage/set_Category.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Category.ps1 create mode 100644 Types/OpenPackage/set_Category.ps1 diff --git a/Types/OpenPackage/get_Category.ps1 b/Types/OpenPackage/get_Category.ps1 new file mode 100644 index 0000000..558cf93 --- /dev/null +++ b/Types/OpenPackage/get_Category.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage `Category` +.DESCRIPTION + Gets the OpenPackage `Category` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Category \ No newline at end of file diff --git a/Types/OpenPackage/set_Category.ps1 b/Types/OpenPackage/set_Category.ps1 new file mode 100644 index 0000000..25a56c6 --- /dev/null +++ b/Types/OpenPackage/set_Category.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Category` +.DESCRIPTION + Sets the OpenPackage `Category` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 +#> +param([string]$Category) + +$this.PackageProperties.Category = $Category \ No newline at end of file From 1cdccc7cad7480c2150430ee9c0fcf9c7ce0446f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:24:09 +0000 Subject: [PATCH 032/724] feat: `OpenPackage.get/set_Category` ( Fixes #23 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 2ae981d..d1e3b4a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -15,6 +15,35 @@ + + Category + + <# +.SYNOPSIS + Gets OpenPackage `Category` +.DESCRIPTION + Gets the OpenPackage `Category` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Category + + + <# +.SYNOPSIS + Sets OpenPackage `Category` +.DESCRIPTION + Sets the OpenPackage `Category` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 +#> +param([string]$Category) + +$this.PackageProperties.Category = $Category + + Created From e74e07820e97edc2475773305df48739bf3e4e9f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:41:28 -0700 Subject: [PATCH 033/724] feat: `OpenPackage.get/set_ContentStatus` ( Fixes #24 ) --- Types/OpenPackage/get_ContentStatus.ps1 | 11 +++++++++++ Types/OpenPackage/set_ContentStatus.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_ContentStatus.ps1 create mode 100644 Types/OpenPackage/set_ContentStatus.ps1 diff --git a/Types/OpenPackage/get_ContentStatus.ps1 b/Types/OpenPackage/get_ContentStatus.ps1 new file mode 100644 index 0000000..687270f --- /dev/null +++ b/Types/OpenPackage/get_ContentStatus.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage `ContentStatus` +.DESCRIPTION + Gets the OpenPackage `ContentStatus` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.ContentStatus \ No newline at end of file diff --git a/Types/OpenPackage/set_ContentStatus.ps1 b/Types/OpenPackage/set_ContentStatus.ps1 new file mode 100644 index 0000000..01cce10 --- /dev/null +++ b/Types/OpenPackage/set_ContentStatus.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `ContentStatus` +.DESCRIPTION + Sets the OpenPackage `ContentStatus` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 +#> +param([string]$ContentStatus) + +$this.PackageProperties.ContentStatus = $ContentStatus \ No newline at end of file From 9cc57bc6f51ab142dc501238763b8e666322bc50 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:41:48 +0000 Subject: [PATCH 034/724] feat: `OpenPackage.get/set_ContentStatus` ( Fixes #24 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d1e3b4a..63f667f 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -44,6 +44,35 @@ param([string]$Category) $this.PackageProperties.Category = $Category + + ContentStatus + + <# +.SYNOPSIS + Gets OpenPackage `ContentStatus` +.DESCRIPTION + Gets the OpenPackage `ContentStatus` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.ContentStatus + + + <# +.SYNOPSIS + Sets OpenPackage `ContentStatus` +.DESCRIPTION + Sets the OpenPackage `ContentStatus` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 +#> +param([string]$ContentStatus) + +$this.PackageProperties.ContentStatus = $ContentStatus + + Created From 4383a1f4545faef5d2a330b0bc2c4c364a95838c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:44:19 -0700 Subject: [PATCH 035/724] feat: `OpenPackage.get/set_ContentType` ( Fixes #25 ) --- Types/OpenPackage/get_ContentType.ps1 | 11 +++++++++++ Types/OpenPackage/set_ContentType.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_ContentType.ps1 create mode 100644 Types/OpenPackage/set_ContentType.ps1 diff --git a/Types/OpenPackage/get_ContentType.ps1 b/Types/OpenPackage/get_ContentType.ps1 new file mode 100644 index 0000000..f0fe842 --- /dev/null +++ b/Types/OpenPackage/get_ContentType.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage `ContentType` +.DESCRIPTION + Gets the OpenPackage `ContentType` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.ContentType -split ';' \ No newline at end of file diff --git a/Types/OpenPackage/set_ContentType.ps1 b/Types/OpenPackage/set_ContentType.ps1 new file mode 100644 index 0000000..cdd6189 --- /dev/null +++ b/Types/OpenPackage/set_ContentType.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `ContentType` +.DESCRIPTION + Sets the OpenPackage `ContentType` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 +#> +param([string]$ContentType) + +$this.PackageProperties.ContentType = $ContentType \ No newline at end of file From 089c38a8753a6c90f4110f8db5b0b2cd01f453db Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:44:43 +0000 Subject: [PATCH 036/724] feat: `OpenPackage.get/set_ContentType` ( Fixes #25 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 63f667f..a4c0adf 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -73,6 +73,35 @@ param([string]$ContentStatus) $this.PackageProperties.ContentStatus = $ContentStatus + + ContentType + + <# +.SYNOPSIS + Gets OpenPackage `ContentType` +.DESCRIPTION + Gets the OpenPackage `ContentType` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.ContentType -split ';' + + + <# +.SYNOPSIS + Sets OpenPackage `ContentType` +.DESCRIPTION + Sets the OpenPackage `ContentType` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 +#> +param([string]$ContentType) + +$this.PackageProperties.ContentType = $ContentType + + Created From d471f13aa7a45ea7ce82613eaba689748c7f59d3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:48:34 -0700 Subject: [PATCH 037/724] feat: `OpenPackage.get/set_LastModifiedBy` ( Fixes #26 ) --- Types/OpenPackage/get_LastModifiedBy.ps1 | 11 +++++++++++ Types/OpenPackage/set_LastModifiedBy.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_LastModifiedBy.ps1 create mode 100644 Types/OpenPackage/set_LastModifiedBy.ps1 diff --git a/Types/OpenPackage/get_LastModifiedBy.ps1 b/Types/OpenPackage/get_LastModifiedBy.ps1 new file mode 100644 index 0000000..8ff1250 --- /dev/null +++ b/Types/OpenPackage/get_LastModifiedBy.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage LastModifiedBy time +.DESCRIPTION + Gets the OpenPackage `LastModifiedBy` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastmodifiedby?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.LastModifiedBy \ No newline at end of file diff --git a/Types/OpenPackage/set_LastModifiedBy.ps1 b/Types/OpenPackage/set_LastModifiedBy.ps1 new file mode 100644 index 0000000..d8b9339 --- /dev/null +++ b/Types/OpenPackage/set_LastModifiedBy.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `LastModifiedBy` +.DESCRIPTION + Sets the OpenPackage `LastModifiedBy` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.Lastmodifiedby?wt.mc_id=MVP_321542 +#> +param([string]$LastModifiedBy) + +$this.PackageProperties.LastModifiedBy = $LastModifiedBy \ No newline at end of file From 20ffcf370eb92f43c86cb1a8f9bc8f16b1afc9a3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:48:53 +0000 Subject: [PATCH 038/724] feat: `OpenPackage.get/set_LastModifiedBy` ( Fixes #26 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index a4c0adf..9b5a8c9 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -224,6 +224,35 @@ param() $this.PackageProperties.Keywords = $args -join ' ' + + LastModifiedBy + + <# +.SYNOPSIS + Gets OpenPackage LastModifiedBy time +.DESCRIPTION + Gets the OpenPackage `LastModifiedBy` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastmodifiedby?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.LastModifiedBy + + + <# +.SYNOPSIS + Sets OpenPackage `LastModifiedBy` +.DESCRIPTION + Sets the OpenPackage `LastModifiedBy` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.Lastmodifiedby?wt.mc_id=MVP_321542 +#> +param([string]$LastModifiedBy) + +$this.PackageProperties.LastModifiedBy = $LastModifiedBy + + Modified From e8e74dafcbf9ba5bf275859fb8af03044dbbebf2 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:51:08 -0700 Subject: [PATCH 039/724] feat: `OpenPackage.get/set_Language` ( Fixes #27 ) --- Types/OpenPackage/get_Language.ps1 | 11 +++++++++++ Types/OpenPackage/set_Language.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Language.ps1 create mode 100644 Types/OpenPackage/set_Language.ps1 diff --git a/Types/OpenPackage/get_Language.ps1 b/Types/OpenPackage/get_Language.ps1 new file mode 100644 index 0000000..9b70582 --- /dev/null +++ b/Types/OpenPackage/get_Language.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage Language time +.DESCRIPTION + Gets the OpenPackage `Language` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Language \ No newline at end of file diff --git a/Types/OpenPackage/set_Language.ps1 b/Types/OpenPackage/set_Language.ps1 new file mode 100644 index 0000000..e87a7e8 --- /dev/null +++ b/Types/OpenPackage/set_Language.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `Language` +.DESCRIPTION + Sets the OpenPackage `Language` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 +#> +param([string]$Language) + +$this.PackageProperties.Language = $Language \ No newline at end of file From a24e31cac9b787a323487a69eb965c00a8969971 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:51:27 +0000 Subject: [PATCH 040/724] feat: `OpenPackage.get/set_Language` ( Fixes #27 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9b5a8c9..7291eea 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -224,6 +224,35 @@ param() $this.PackageProperties.Keywords = $args -join ' ' + + Language + + <# +.SYNOPSIS + Gets OpenPackage Language time +.DESCRIPTION + Gets the OpenPackage `Language` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Language + + + <# +.SYNOPSIS + Sets OpenPackage `Language` +.DESCRIPTION + Sets the OpenPackage `Language` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 +#> +param([string]$Language) + +$this.PackageProperties.Language = $Language + + LastModifiedBy From e77e4b51b258f69f62e222f17a5e384f8deec4cc Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 18:54:01 -0700 Subject: [PATCH 041/724] feat: `OpenPackage.get/set_LastPrinted` ( Fixes #28 ) --- Types/OpenPackage/get_LastPrinted.ps1 | 11 +++++++++++ Types/OpenPackage/set_LastPrinted.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_LastPrinted.ps1 create mode 100644 Types/OpenPackage/set_LastPrinted.ps1 diff --git a/Types/OpenPackage/get_LastPrinted.ps1 b/Types/OpenPackage/get_LastPrinted.ps1 new file mode 100644 index 0000000..85c4573 --- /dev/null +++ b/Types/OpenPackage/get_LastPrinted.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets OpenPackage LastPrinted time +.DESCRIPTION + Gets the OpenPackage `LastPrinted` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.LastPrinted \ No newline at end of file diff --git a/Types/OpenPackage/set_LastPrinted.ps1 b/Types/OpenPackage/set_LastPrinted.ps1 new file mode 100644 index 0000000..e743920 --- /dev/null +++ b/Types/OpenPackage/set_LastPrinted.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Sets OpenPackage `LastPrinted` +.DESCRIPTION + Sets the OpenPackage `LastPrinted` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 +#> +param([DateTime]$LastPrinted) + +$this.PackageProperties.LastPrinted = $LastPrinted \ No newline at end of file From 77f93458c7ad7bff7b6047fbb106db9d281ab323 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 01:54:15 +0000 Subject: [PATCH 042/724] feat: `OpenPackage.get/set_LastPrinted` ( Fixes #28 ) --- OP.types.ps1xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 7291eea..bf0a7a1 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -282,6 +282,35 @@ param([string]$LastModifiedBy) $this.PackageProperties.LastModifiedBy = $LastModifiedBy + + LastPrinted + + <# +.SYNOPSIS + Gets OpenPackage LastPrinted time +.DESCRIPTION + Gets the OpenPackage `LastPrinted` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.LastPrinted + + + <# +.SYNOPSIS + Sets OpenPackage `LastPrinted` +.DESCRIPTION + Sets the OpenPackage `LastPrinted` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 +#> +param([DateTime]$LastPrinted) + +$this.PackageProperties.LastPrinted = $LastPrinted + + Modified From 7aaf9ad9fdd6eb5188eca6208d74b4fb0ded5c28 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 19:05:39 -0700 Subject: [PATCH 043/724] feat: `OpenPackage.get_CHANGELOG` ( Fixes #29 ) --- Types/OpenPackage/get_CHANGELOG.ps1 | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Types/OpenPackage/get_CHANGELOG.ps1 diff --git a/Types/OpenPackage/get_CHANGELOG.ps1 b/Types/OpenPackage/get_CHANGELOG.ps1 new file mode 100644 index 0000000..ae31465 --- /dev/null +++ b/Types/OpenPackage/get_CHANGELOG.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Gets a package's changelog +.DESCRIPTION + Gets the content of any parts in the package named CHANGELOG.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Make the markdown a PSObject + $markdownObject = [PSObject]::new($readStream) + # add the URI + $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force + # and decorate it as `text/markdown` + $markdownObject.pstypenames.insert(0, 'text/markdown') + + $markdownObject + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() +} + +# We are done. \ No newline at end of file From 8fdbca5bb739cb479c3be31362b6fb4f672dc0dc Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 02:06:01 +0000 Subject: [PATCH 044/724] feat: `OpenPackage.get_CHANGELOG` ( Fixes #29 ) --- OP.types.ps1xml | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index bf0a7a1..8afca3a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -44,6 +44,50 @@ param([string]$Category) $this.PackageProperties.Category = $Category + + CHANGELOG + + <# +.SYNOPSIS + Gets a package's changelog +.DESCRIPTION + Gets the content of any parts in the package named CHANGELOG.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Make the markdown a PSObject + $markdownObject = [PSObject]::new($readStream) + # add the URI + $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force + # and decorate it as `text/markdown` + $markdownObject.pstypenames.insert(0, 'text/markdown') + + $markdownObject + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() +} + +# We are done. + + ContentStatus From bda8f02116f5cad60f0cec3419a554677bbef6fc Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 19:19:36 -0700 Subject: [PATCH 045/724] feat: `OpenPackage.set_CHANGELOG` ( Fixes #30 ) Also updating markdown decoration --- Types/OpenPackage/get_CHANGELOG.ps1 | 4 ++-- Types/OpenPackage/set_CHANGELOG.ps1 | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 Types/OpenPackage/set_CHANGELOG.ps1 diff --git a/Types/OpenPackage/get_CHANGELOG.ps1 b/Types/OpenPackage/get_CHANGELOG.ps1 index ae31465..2a122da 100644 --- a/Types/OpenPackage/get_CHANGELOG.ps1 +++ b/Types/OpenPackage/get_CHANGELOG.ps1 @@ -23,8 +23,8 @@ foreach ($part in $this.GetParts()) { # add the URI $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force # and decorate it as `text/markdown` - $markdownObject.pstypenames.insert(0, 'text/markdown') - + $markdownObject.pstypenames.add('text/markdown') + $markdownObject # Close the reader diff --git a/Types/OpenPackage/set_CHANGELOG.ps1 b/Types/OpenPackage/set_CHANGELOG.ps1 new file mode 100644 index 0000000..b807d1a --- /dev/null +++ b/Types/OpenPackage/set_CHANGELOG.ps1 @@ -0,0 +1,25 @@ +param() + +$newChangelog = ($args -join [Environment]::NewLine) + [Environment]::NewLine + +$changelogParts = +@(foreach ($part in $($this.GetParts())) { + if ($part.Uri -match '/CHANGELOG\.md$') { $part} +}) + + +$stream = +if (-not $changelogParts) { + $newPart = $this.CreatePart('/CHANGELOG.md', 'text/markdown', 'Normal') + $newPart.GetStream() +} +else { + $firstChangelog = $changelogParts[0] + $changelogPart = $this.GetPart($firstChangelog.Uri) + $changelogPart.GetStream() +} + +$buffer = $OutputEncoding.GetBytes("$newChangelog") +$stream.Write($buffer,0, $buffer.Length) +$stream.Close() +$stream.Dispose() From baa5f9a787498350c9414679a5a775578485cd33 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 02:19:57 +0000 Subject: [PATCH 046/724] feat: `OpenPackage.set_CHANGELOG` ( Fixes #30 ) Also updating markdown decoration --- OP.types.ps1xml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 8afca3a..c77fac0 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -72,8 +72,8 @@ foreach ($part in $this.GetParts()) { # add the URI $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force # and decorate it as `text/markdown` - $markdownObject.pstypenames.insert(0, 'text/markdown') - + $markdownObject.pstypenames.add('text/markdown') + $markdownObject # Close the reader @@ -87,6 +87,34 @@ foreach ($part in $this.GetParts()) { # We are done. + + param() + +$newChangelog = ($args -join [Environment]::NewLine) + [Environment]::NewLine + +$changelogParts = +@(foreach ($part in $($this.GetParts())) { + if ($part.Uri -match '/CHANGELOG\.md$') { $part} +}) + + +$stream = +if (-not $changelogParts) { + $newPart = $this.CreatePart('/CHANGELOG.md', 'text/markdown', 'Normal') + $newPart.GetStream() +} +else { + $firstChangelog = $changelogParts[0] + $changelogPart = $this.GetPart($firstChangelog.Uri) + $changelogPart.GetStream() +} + +$buffer = $OutputEncoding.GetBytes("$newChangelog") +$stream.Write($buffer,0, $buffer.Length) +$stream.Close() +$stream.Dispose() + + ContentStatus From 35831bac90d2452cd218e6657699624e408b9093 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 19:34:19 -0700 Subject: [PATCH 047/724] feat: `OpenPackage.get_NuSpec` ( Fixes #31 ) --- Types/OpenPackage/get_Nuspec.ps1 | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Types/OpenPackage/get_Nuspec.ps1 diff --git a/Types/OpenPackage/get_Nuspec.ps1 b/Types/OpenPackage/get_Nuspec.ps1 new file mode 100644 index 0000000..e4d9b42 --- /dev/null +++ b/Types/OpenPackage/get_Nuspec.ps1 @@ -0,0 +1,36 @@ +<# +.SYNOPSIS + Gets a package's nuspec +.DESCRIPTION + Gets the content of any `*.nuspec` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '\.nuspec$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + $nuspecXml = $readStream -as [xml] + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + if (-not $nuspecXml) { continue } + + $nuspecXml +} + +# We are done. \ No newline at end of file From ca29b70ee922daf2d75688d6746d7c000c03788e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 02:34:44 +0000 Subject: [PATCH 048/724] feat: `OpenPackage.get_NuSpec` ( Fixes #31 ) --- OP.types.ps1xml | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index c77fac0..7445af7 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -412,6 +412,47 @@ param([DateTime]$Modified) $this.PackageProperties.Modifiedd = $Modified + + Nuspec + + <# +.SYNOPSIS + Gets a package's nuspec +.DESCRIPTION + Gets the content of any `*.nuspec` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '\.nuspec$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + $nuspecXml = $readStream -as [xml] + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + if (-not $nuspecXml) { continue } + + $nuspecXml +} + +# We are done. + + Revision From 6261f1d9afd0298dc1fa9d5fb398bb48a7725ca3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 19:40:00 -0700 Subject: [PATCH 049/724] feat: `OpenPackage.get_PackageJson` ( Fixes #32 ) --- Types/OpenPackage/get_PackageJson.ps1 | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Types/OpenPackage/get_PackageJson.ps1 diff --git a/Types/OpenPackage/get_PackageJson.ps1 b/Types/OpenPackage/get_PackageJson.ps1 new file mode 100644 index 0000000..1a86ce2 --- /dev/null +++ b/Types/OpenPackage/get_PackageJson.ps1 @@ -0,0 +1,33 @@ +<# +.SYNOPSIS + Gets a package's `package.json` +.DESCRIPTION + Gets the content of any `package.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/package\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json +} + +# We are done. \ No newline at end of file From 02ce1a7b5d097b51d951465cbe0a72d3be53acca Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 02:40:21 +0000 Subject: [PATCH 050/724] feat: `OpenPackage.get_PackageJson` ( Fixes #32 ) --- OP.types.ps1xml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 7445af7..d744cb9 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -450,6 +450,44 @@ foreach ($part in $this.GetParts()) { $nuspecXml } +# We are done. + + + + PackageJson + + <# +.SYNOPSIS + Gets a package's `package.json` +.DESCRIPTION + Gets the content of any `package.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/package\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json +} + # We are done. From ded01d925d2f2f7af8591e65bac39094668125b5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 21:38:16 -0700 Subject: [PATCH 051/724] feat: `OpenPackage.get_ProjectFile` ( Fixes #33 ) --- Types/OpenPackage/get_ProjectFile.ps1 | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Types/OpenPackage/get_ProjectFile.ps1 diff --git a/Types/OpenPackage/get_ProjectFile.ps1 b/Types/OpenPackage/get_ProjectFile.ps1 new file mode 100644 index 0000000..6e79270 --- /dev/null +++ b/Types/OpenPackage/get_ProjectFile.ps1 @@ -0,0 +1,40 @@ +<# +.SYNOPSIS + Gets a package's project files +.DESCRIPTION + Gets the content of any `*.*proj` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +:nextPart foreach ($part in $this.GetParts()) { + # and ignore any non-project parts + if ($part.Uri -notmatch '\..+?proj$') { continue nextPart } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Try to cast the text to XML + $projectXml = $readStream -as [xml] + + # If we could not, continue to the next part + if (-not $projectXml) { continue nextPart } + + # Output the project XML, with a note property containing the URI + $projectXml | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. \ No newline at end of file From 564626807f97fab75ecf73543bedf2493eac5a79 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 04:38:36 +0000 Subject: [PATCH 052/724] feat: `OpenPackage.get_ProjectFile` ( Fixes #33 ) --- OP.types.ps1xml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d744cb9..9d5754a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -488,6 +488,51 @@ foreach ($part in $this.GetParts()) { $readStream | ConvertFrom-Json } +# We are done. + + + + ProjectFile + + <# +.SYNOPSIS + Gets a package's project files +.DESCRIPTION + Gets the content of any `*.*proj` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +:nextPart foreach ($part in $this.GetParts()) { + # and ignore any non-project parts + if ($part.Uri -notmatch '\..+?proj$') { continue nextPart } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Try to cast the text to XML + $projectXml = $readStream -as [xml] + + # If we could not, continue to the next part + if (-not $projectXml) { continue nextPart } + + # Output the project XML, with a note property containing the URI + $projectXml | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + # We are done. From 22fd6355e50ee020128696d5fab1f547158e1b61 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 27 Oct 2025 21:46:14 -0700 Subject: [PATCH 053/724] feat: `OpenPackage.get_PowerShellManifest` ( Fixes #34 ) --- Types/OpenPackage/get_PowerShellManifest.ps1 | 55 ++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 Types/OpenPackage/get_PowerShellManifest.ps1 diff --git a/Types/OpenPackage/get_PowerShellManifest.ps1 b/Types/OpenPackage/get_PowerShellManifest.ps1 new file mode 100644 index 0000000..483309f --- /dev/null +++ b/Types/OpenPackage/get_PowerShellManifest.ps1 @@ -0,0 +1,55 @@ +<# +.SYNOPSIS + Gets a package's PowerShell manifest files +.DESCRIPTION + Gets a package's PowerShell manifest files. + + These are any `*.psd1` files in the package that: + + * Are valid PowerShell data blocks + * Contain a ModuleVersion +#> +[OutputType([PSObject])] +param() + +# Get every part +:nextPart foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '\.psd1$') { continue nextPart } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + if ($readStream -notmatch 'ModuleVersion') { + continue nextPart + } + + try { + $scriptBlock = [scriptblock]::Create($readStream) + $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") + if ($dataScriptBlock.Ast.EndBlock.Statements.Count -ne 1) { + continue nextPart + } + if ($dataScriptBlock.Ast.EndBlock.Statements[0] -isnot + [Management.Automation.Language.DataStatementAst]) { + continue nextPart + } + & $dataScriptBlock | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru + } catch { + continue nextPart + } +} + +# We are done. \ No newline at end of file From 188ca751bc0f2045a2777961438b19ef9dda65aa Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 28 Oct 2025 04:46:41 +0000 Subject: [PATCH 054/724] feat: `OpenPackage.get_PowerShellManifest` ( Fixes #34 ) --- OP.types.ps1xml | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9d5754a..cf37944 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -488,6 +488,66 @@ foreach ($part in $this.GetParts()) { $readStream | ConvertFrom-Json } +# We are done. + + + + PowerShellManifest + + <# +.SYNOPSIS + Gets a package's PowerShell manifest files +.DESCRIPTION + Gets a package's PowerShell manifest files. + + These are any `*.psd1` files in the package that: + + * Are valid PowerShell data blocks + * Contain a ModuleVersion +#> +[OutputType([PSObject])] +param() + +# Get every part +:nextPart foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '\.psd1$') { continue nextPart } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + if ($readStream -notmatch 'ModuleVersion') { + continue nextPart + } + + try { + $scriptBlock = [scriptblock]::Create($readStream) + $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") + if ($dataScriptBlock.Ast.EndBlock.Statements.Count -ne 1) { + continue nextPart + } + if ($dataScriptBlock.Ast.EndBlock.Statements[0] -isnot + [Management.Automation.Language.DataStatementAst]) { + continue nextPart + } + & $dataScriptBlock | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru + } catch { + continue nextPart + } +} + # We are done. From d6e22732c9e8375f0b117c19fc273aa68c679129 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 29 Oct 2025 13:27:36 -0700 Subject: [PATCH 055/724] feat: `OpenPackage.get_Count` ( Fixes #38 ) --- Types/OpenPackage/get_Count.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Types/OpenPackage/get_Count.ps1 diff --git a/Types/OpenPackage/get_Count.ps1 b/Types/OpenPackage/get_Count.ps1 new file mode 100644 index 0000000..a75a194 --- /dev/null +++ b/Types/OpenPackage/get_Count.ps1 @@ -0,0 +1,7 @@ +<# +.SYNOPSIS + Gets the package files count +.DESCRIPTION + Gets the number of files in a package. +#> +return @($this.GetParts()).Length \ No newline at end of file From e4d6aca41dd8e6b80d5e13edf778abe3220d849a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 29 Oct 2025 20:28:20 +0000 Subject: [PATCH 056/724] feat: `OpenPackage.get_Count` ( Fixes #38 ) --- OP.types.ps1xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index cf37944..8dc9e48 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -174,6 +174,18 @@ param([string]$ContentType) $this.PackageProperties.ContentType = $ContentType + + Count + + <# +.SYNOPSIS + Gets the package files count +.DESCRIPTION + Gets the number of files in a package. +#> +return @($this.GetParts()).Length + + Created From c82512dbe8278542f4486b5b8057f0ad193f4047 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 30 Oct 2025 19:17:27 -0700 Subject: [PATCH 057/724] feat: `Get-OpenPackage` ( Fixes #2 ) --- Commands/Get-OpenPackage.ps1 | 1357 ++++++++++++++++++++++++++++++++++ 1 file changed, 1357 insertions(+) create mode 100644 Commands/Get-OpenPackage.ps1 diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 new file mode 100644 index 0000000..1db7a10 --- /dev/null +++ b/Commands/Get-OpenPackage.ps1 @@ -0,0 +1,1357 @@ +function Get-OpenPackage +{ + <# + .SYNOPSIS + Gets Open Packages + .DESCRIPTION + Gets Open Packages from a path, uri, or repository. Runs related open package commands. + + Anything can be a package + .NOTES + + .EXAMPLE + # Make the current directory into a package + # (do not try this at `$home`) + Get-OpenPackage . + .EXAMPLE + # Make the module into a package + $opPackage = Get-Module OP | Get-OpenPackage + .EXAMPLE + $opPackage = Get-Module OP | + Get-OpenPackage -Include *.ps1 + .EXAMPLE + # Another way to make the current directory into a package + # (do not try this at `$home`) + Get-Item . | Get-OpenPackage + .EXAMPLE + # Get a package from nuget + $dependencyInjectionPackage = OP https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection + .EXAMPLE + # Get a package from the PowerShell gallery + $turtlePackage = op https://powershellgallery.com/packages/Turtle + .EXAMPLE + # Get a package from a single URL + $imagePackage = op https://MrPowerShell.com/MrPowerShell.png + .EXAMPLE + # Create a package from multiple URLs by piping back to ourself + $svgAndPng = op https://MrPowerShell.com/MrPowerShell.png | + op https://MrPowerShell.com/MrPowerShell.svg + .EXAMPLE + # Get a package from an at protocol URI + $atPost = op at://mrpowershell.com/app.bsky.feed.post/3k4hf5dy6nf2g + .EXAMPLE + # Get the most recent 50 posts + $atLast50 = op at://mrpowershell.com/app.bsky.feed.post/ -First 50 + .EXAMPLE + + #> + [CmdletBinding(PositionalBinding=$false,DefaultParameterSetName='Any',SupportsPaging)] + [Alias('Get-OP', 'OP', 'OpenPackage')] + param( + # Any unnamed arguments to the command. + # Each argument will be treated as a potential -FilePath or -Uri. + # Once the first related verb is detected, these will become arguments to that verb + # (For example, `op . start` will get an open package and then start a server for that package) + [Parameter(ValueFromRemainingArguments)] + [PSObject[]] + $ArgumentList, + + # The path of a file to import + [Parameter(Mandatory,ParameterSetName='FilePath',ValueFromPipelineByPropertyName)] + [Alias('Fullname')] + [string] + $FilePath, + + # A URI to package. + # If this URI is a git repository, will make a package out of the repository + # If this URI is a nuget package url or powershell gallery url, will download the package. + [Parameter(Mandatory,ParameterSetName='Uri',ValueFromPipelineByPropertyName)] + [Alias('Url', 'Link', 'Href')] + [uri] + $Uri, + + # A Repository to package. + # This can be the root of a repo or a link to a portion of the tree. + # If a portion of the tree is provided, will perform a sparse clone of the repository + [Parameter(Mandatory,ParameterSetName='Repository',ValueFromPipelineByPropertyName)] + [Alias('clone_url')] + [string] + $Repository, + + # The github branch name. + [Parameter(ParameterSetName='Repository',ValueFromPipelineByPropertyName)] + [string] + $Branch, + + # One or more optional sparse filters to a repository. + # If these are provided, only files matching these filters will be downloaded. + [Parameter(ParameterSetName='Repository',ValueFromPipelineByPropertyName)] + [string[]] + $SparseFilter, + + # An At Uri to package. + # This can be a single post or a collection of all posts of a type. + [Parameter(Mandatory,ParameterSetName='AtUri',ValueFromPipelineByPropertyName)] + [string] + $AtUri, + + # A Nuget Uri to package. + # The package at this location will be downloaded and opened directly. + [Parameter(Mandatory,ParameterSetName='NugetPackage',ValueFromPipelineByPropertyName)] + [Alias('NugetPackage','PowerShellGallery')] + [uri] + $NuGet, + + # A module to package + # A loaded module name or moduleinfo object to package. + # The loaded module must have a path property. + # The files in this path will be packaged. + [Parameter(Mandatory,ParameterSetName='Module',ValueFromPipelineByPropertyName)] + [PSObject] + $Module, + + # A list of file wildcards to include. + [Parameter(ValueFromPipelineByPropertyName)] + [string[]] + $Include, + + # A list of file wildcards to exclude. + [Parameter(ValueFromPipelineByPropertyName)] + [string[]] + $Exclude, + + # A content type map. + # This maps extensions and URIs to a content type. + [Collections.IDictionary] + $TypeMap = $( + ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap + ), + + # One or more input objects. + [Parameter(ValueFromPipeline)] + [PSObject] + $InputObject, + + # If set, will force the redownload of various resources and remove existing files or directories + [switch] + $Force + ) + + begin { + # Begin by declaring several variables we will keep coming back to + $myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage + + $myCommandName = $MyInvocation.MyCommand.Name + + # and initializing our default type map + $typeMap = ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap + + # The full default type map is much more robust. + # If this was being used without a module context, we still want to work for _most_ scenarios + # So this a secondary default, declared inline, that captures a fairly short list of common content types. + if (-not $typeMap) { + $typeMap = [Ordered]@{ + ".apng" = "image/apng" + ".css" = "text/css" + ".gif" = "image/gif" + ".jpg" = "image/jpeg" + ".jpeg" = "image/jpeg" + ".js" = "text/javascript" + ".jsm" = "text/javascript" + ".html" = "text/html" + ".md" = "text/markdown" + ".mp3" = "audio/mpeg" + ".mp4" = "video/mp4" + ".png" = "image/png" + ".ps1" = "text/x-powershell" + ".psm1" = "text/x-powershell" + ".psd1" = "text/x-powershell-data" + ".ps1xml" = "text/x-powershell+xml" + ".clixml" = "text/cli+xml" + ".svg" = "image/svg+xml" + ".xml" = "application/xml" + } + } + + # Now declare several internal filters to turn various things into packages. + + # These are in alphabetical order, not in order of likely use. + filter getCurrentPack { + # Gets the current package + + # If the input object was a package, get it + if ($inputObject -is [IO.Packaging.Package]) { + $currentPackage = $InputObject + } else { + # Otherwise, + $memoryStream = [IO.MemoryStream]::new() + $currentPackage = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate','ReadWrite') + Add-Member NoteProperty MemoryStream $memoryStream -Force -InputObject $currentPackage + } + + if ($currentPackage.pstypenames -notcontains 'OP') { + $currentPackage.pstypenames.insert(0, 'OP') + } + if ($currentPackage.pstypenames -notcontains 'OpenPackage') { + $currentPackage.pstypenames.insert(0, 'OpenPackage') + } + $currentPackage + } + + filter getPartContent { + $thisPart = $_ + + # If this part has no stream, return now + if (-not $thisPart.GetStream) { + return + } + + # Get the stream + $partStream = $thisPart.GetStream() + + # And copy it right into a memory stream + $memoryStream = [IO.MemoryStream]::new() + $partStream.CopyTo($memoryStream) + # and clean up and close out (this minimizes read time) + $partStream.Close() + $partStream.Dispose() + + # Then get that stream + [byte[]]$partBytes = $memoryStream.ToArray() + + # Seek back to the beginning + $null = $memoryStream.Seek(0, 'begin') + # and read it as text + $partStreamReader = [IO.StreamReader]::new($memoryStream) + $partString = $partStreamReader.ReadToEnd() + # then close and dispose of our memory. + $partStreamReader.Close() + $partStreamReader.Dispose() + $memoryStream.Close() + $memoryStream.Dispose() + + # We will base our parsing off of the content type and uri + + # Starting off with one of the easy content types: + # `x-www-form-urlencoded` (aka form data) + if ($thisPart.ContentType -eq 'application/x-www-form-urlencoded') { + # Let's parse this + $formData = [Web.HttpUtility]::ParseQueryString($partString) + # into a nice ordered dictionary + $formDataObject = [Ordered]@{} + # Remember that multiple values are passed with multiple keys. + foreach ($key in $formData.Keys) { + if ($null -eq $formDataObject[$key]) { + $formDataObject[$key] = $formData[$key] + } else { + $formDataObject[$key] = @($formDataObject[$key]) + $formData[$key] + } + } + # and output our form data dictionary. + return $formDataObject + } + + # If the content type was some `xml` type or the uri ends with `xml` + if (($thisPart.ContentType -match '[/\+]xml$' -or $thisPart.Uri -match 'xml$')) { + # try casting it to XML + $partAsXml = $partString -as [xml] + if ($partASXml.Objs) { + try { + [Management.Automation.PSSerializer]::Deserialize($partString) + return + } catch { + Write-Warning "$($thisPart.Uri) was not clixml" + } + } + if ($null -ne $partAsXml) { + return $partAsXml + } + } + + # If the content type is json or the uri ends with json + if ($thisPart.ContentType -match '[/\+].json$' -or $thisPart.Uri -match 'json$') { + # try to convert it from json + $partAsJson = try { + ConvertFrom-Json -InputObject $partString -ErrorAction Ignore + } catch { + Write-Warning "'$($thisPart.Uri)' was not json: $_" + } + + if ($null -ne $partAsJson) { + $partAsJson + return + } + } + + # If the content type was powershell data (or the part ended with .psd1) + if ($thisPart.ContentType -in 'text/x-powershell-data', + 'application/x-powershell-data' -or $thisPart.Uri -match '\.psd1$') { + + # Try to create a data block + $dataBlockOutput = try { + $scriptBlock = [ScriptBlock]::Create($partString) + $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") + & $dataScriptBlock + } catch { + Write-Warning "$($thisPart.Uri) is not a valid PowerShell data file: $_" + } + + if ($null -ne $dataBlockOutput) { + $dataBlockOutput + return + } + } + + + if ($thisPart.ContentType -match '^text/') { + $partString + } else { + ,$partBytes + } + } + + filter packAt { + param() + # Capture the current input + $whereAt = $_ + # and declare a pattern to pick apart an at uri + $atPattern = 'at://(?[^/]+)/(?[^/]+)(?:/(?.+?$))?' + + # If this does not match the pattern or we don't have a type, we are done. + if ($whereAt -notmatch $atPattern -or + -not $matches.type) { return } + + # Store our match information before anything else needs to `-match`. + $atMatch = [Ordered]@{} + $Matches + + # Create a package in memory + $currentPackage = getCurrentPack + if (-not $currentPackage.PackageProperties.Identifier) { + $currentPackage.PackageProperties.Identifier = $atMatch.did + } + + # If we have a type and rkey, we are after a single record. + if ($matches.type -and $matches.rkey) { + . packAtRecord + } + else { + . packAtType + } + } + + filter packAtRecord { + # Construct the XRPC url to that record + $xrpcUrl = "https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=$( + $matches.did + )&collection=$( + $matches.type + )&rkey=$( + $matches.rkey + )" + + # and go fetch. + $atRecord = Invoke-RestMethod -Uri $xrpcUrl + + # Declare a package uri for the segment. + # (make sure to switch did colons to something else, so that the files can unpack cleanly regardless of OS) + $currentPackageUri = "/$($matches.did -replace ':','_')/$($matches.type)/$($matches.rkey).json" + + # If the part exists, + if ($currentPackage.PartExists($currentPackageUri)) { + $currentPackage.DeletePart($currentPackageUri) # recreate it. + } + $atPart = $currentPackage.CreatePart($currentPackageUri, 'application/json', 'Normal') + + # Get the stream. + $atStream = $atPart.GetStream() + # Turn our message into json, and get the bytes. + $atJsonBytes = $outputEncoding.GetBytes( + ($atRecord | ConvertTo-Json -Depth 100) + ) + + # Then write them to the stream, + $atStream.Write($atJsonBytes, 0, $atJsonBytes.Count) + + # clean up, + $atStream.Close() + $atStream.Dispose() + + # and emit the result + $currentPackage + } + + filter packAtType { + # Otherwise, we are getting everything of a type + $total = [long]0 + $skipped = [long]0 + $cursor = '' + $progress = [Ordered]@{Id = Get-Random} + $BatchSize = 100 + $progress.Status = "Getting records" + :AtSync do { + $xrpcUrl = "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=$( + $atMatch.did + )&collection=$( + $atMatch.type + )&cursor=$Cursor&limit=$BatchSize" + $progress.Activity = "$total " + Write-Progress @progress + # Get the page of records + $results = Invoke-RestMethod $xrpcUrl + # If we got results and have a cursor to more + if ($results -and $results.cursor) { + # set it for the next round. + $Cursor = $results.cursor + } + + # Unroll and store each record. + :nextRecord foreach ($record in $results.records) { + + # Records are sent latest to earliest. + # We can use -Skip to skip N records + if ($PSCmdlet.PagingParameters.Skip -and + $skipped -lt $PSCmdlet.PagingParameters.Skip + ) { + $skipped++ + continue nextRecord + } + + # If the uri is not an at uri + if ($record.uri -notmatch $atPattern) { + # continue to the next record + continue nextRecord + } + + # Construct the URI within the package. + $currentPackageUri = "/$($atMatch.did -replace ':','-')/$($matches.type)/$($matches.rkey).json" + + # Create or recreate the part for the at content + $atPart = if ($currentPackage.PartExists($currentPackageUri)) { + $currentPackage.DeletePart($currentPackageUri) + $currentPackage.CreatePart($currentPackageUri, 'application/json', 'Normal') + } else { + $currentPackage.CreatePart($currentPackageUri, 'application/json', 'Normal') + } + + # Store the json + $atStream = $atPart.GetStream() + $atJsonBytes = $outputEncoding.GetBytes( + ($atRecord | ConvertTo-Json -Depth 100) + ) + $atStream.Write($atJsonBytes, 0, $atJsonBytes.Count) + # and clean up + $atStream.Close() + $atStream.Dispose() + + # Increment our total + $total++ + + # If we provided -First and our total exceeds our -First, break out + if ($PSCmdlet.PagingParameters.First -and + $total -ge $PSCmdlet.PagingParameters.First) { + break AtSync + } + } + } while ($results -and $results.cursor) + + $progress.Completed = $true + Write-Progress @progress + $currentPackage + } + + filter packDir { + # We want a package from a directory + $resolvedItem = $_ + # start off by pushing into that location, for it will make operations easier + Push-Location -LiteralPath $resolvedItem.FullName + # Get all files beneath this point + $filesToArchive = @(Get-ChildItem -LiteralPath $resolvedItem.FullName -Recurse -File) + + # and create a memory stream and package. + $currentPackage = getCurrentPack + + if (-not $currentPackage.PackageProperties.Identifier) { + # The identifier will be the directory name. + $currentPackage.PackageProperties.Identifier = $resolvedItem.Name + } + + # This make take a sec, so let's create a progress bar + $Progress = [Ordered]@{ + Status = " " + Activity = "Creating Package $($resolvedItem.Name)" + Id = Get-Random + } + $total = $filesToArchive.Length + $counter = 0 + + # We will use file types to provide package metadata + + # So declare an oldest created file and newest write time. + $oldestCreationTime = [DateTime]::Now + $lastWriteTime = [DateTime]::MinValue + + #region Filter Filters + $filteredFiles = @( + # If any exclusions are present, + # security dictates we process them first. + # (deny before approve) + :filterFiles foreach ($file in $filesToArchive) { + if ($exclude) { + foreach ($exclusion in $Exclude) { + if ($file.FullName -like $exclusion) { + continue filterFiles + } + } + } + + if ($include) { + $included = $false + foreach ($inclusion in $include) { + if ($file.FullName -like $inclusion) { + $included = $true + break + } + } + + if (-not $included) { continue filterFiles } + } + + $file + } + ) + #region Filter Filters + + # Go over each file we want to archive + :packingFiles foreach ($file in $filteredFiles) { + # get each file as a relative uri, and then get it's bytes + $relativeUri = $file.FullName.Substring($resolvedItem.FullName.Length) + $fileBytes = Get-Content -AsByteStream -Raw -LiteralPath $file.FullName + + # If the file was blank + if (-not $fileBytes) { + # write a message to verbose indicating we are skipping the file. + Write-Verbose "Skipping blank file $($file.FullName)" + continue + } + + # encode our URI, + $encodedUri = [Web.HttpUtility]::UrlEncode($relativeUri) -replace + # but don't forget to fix spaces and decode slashes + '\+', '%20' -replace '%2f', '/' + # (our relative URI may already contain them) + + $relativeUri = '/' + ($encodedUri -replace '^/') + + # Determine the right content type for the extension + $fileContentType = $typeMap[$file.Extension] + # and fall back to text/plain + if (-not $fileContentType) { $fileContentType = 'text/plain'} + + # Then update our creation times / last write times as needed. + if ($file.CreationTime -lt $oldestCreationTime) { + $oldestCreationTime = $file.CreationTime + } + if ($file.LastWriteTime -gt $lastWriteTime) { + $lastWriteTime = $file.LastWriteTime + } + + # Write our progress message + $progress.PercentComplete = (++$counter * 100 / $total) + $Progress.Status = "$relativeUri" + Write-Progress @Progress + + if (-not $fileBytes) { continue } + + # Try to create a new part + try { + $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, 'Normal') + } catch { + # If that didn't work, + $ex = $_ + # at least one exception has some well known answers + if ($ex.Exception.HResult -eq 0x80131501) { + # Open Packaging Conventions do not allow case-sensitive collisions + # (most likely because they would not extract well on all operating systems) + # If we find an exception indicating a conflict, and multiple copies + $multipleCopies = @($filesToArchive | + Where-Object Fullname -ieq $file.FullName | + Select-Object -ExpandProperty Fullname) + if ($multipleCopies.Count) { + # warn the user + Write-Warning "Skipping '$($File.Fullname)' - Case Sensitivity conflict between:$( + @( + [Environment]::NewLine + # and provide a helpful pair of paths so they can resolve the conflict + $multipleCopies + ) -join + [Environment]::NewLine + )" + } else { + # If there were not multiple files, error (but do not return) + Write-Error -ErrorRecord $ex + } + } else { + # and if the error was unknown, error (but do not return) + Write-Error -ErrorRecord $ex + } + } + + # If we could not create the part, continue + if (-not $newPart) { continue } + $newStream = $newPart.GetStream() + $newStream.Write($fileBytes, 0, $fileBytes.Length) + $newStream.Close() + } + + Pop-Location + + $currentPackage | Add-Member NoteProperty SourceDirectory $resolvedItem -Force + + $Progress.Remove('PercentComplete') + $Progress.Completed = $true + Write-Progress @Progress + $currentPackage.PackageProperties.Created = $oldestCreationTime + $currentPackage.PackageProperties.Modified = $lastWriteTime + $currentPackage + + } + + filter packFile { + # If we are creating a package from a file + $resolvedItem = $_ + + try { + # try to open it as an archive first. + # If this was nothing like a .zip file, this will throw an exception. + $resolvedItem.FullName | packZip + } catch { + # The file is not a zip. + # Before we make a package for a single file + # Check if it was a `.tar.gz` file, first. + $packageFromTar = $resolvedItem | packTar + + if ($packageFromTar) { + return $packageFromTar + } + + # Read the file content + $fileBytes = Get-Content -AsByteStream -Raw -LiteralPath $resolvedItem.FullName + + # Get our current package + $currentPackage = getCurrentPack + + # Make the file name an encoded URI + $relativeUri = [Web.HttpUtility]::UrlEncode($resolvedItem.Name) -replace '\+', '%20' + $relativeUri = '/' + ($relativeUri -replace '^/') + $relativeUri = $relativeUri -replace '\s', '%20' + + # If the package has no identifier, + if (-not $currentPackage.PackageProperties.Identifier) { + # set it to the file name + $currentPackage.PackageProperties.Identifier = $resolvedItem.Name + } + + $currentPackage.PackageProperties.Identifier = $resolvedItem.Name + + # And determine the right extension content type + + $fileContentType = $typeMap[$resolvedItem.Extension] + + # and create a part + $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, 'Normal') + if (-not $newPart) { + continue + } + # then write the file to the part + $newStream = $newPart.GetStream() + $newStream.Write($fileBytes, 0, $fileBytes.Length) + $newStream.Close() + + # and set the package properties based off of the resolved info. + $currentPackage.PackageProperties.Created = $resolvedItem.CreationTime + $currentPackage.PackageProperties.Modified = $resolvedItem.LastWriteTime + $currentPackage + } + } + + filter packRepo { + $Repository = $_ + $repositoryUrl = $Repository + + # See if we are matching a repo or a tree + $treePattern = '/tree/(?[^/]+)/' + if ($Repository -match $treePattern -and -not $SparseFilter) { + # If we are matching a tree, turn the file portion into a sparse filter + $branch = $matches.branch + $SparseFilter = $RepositoryUrl -replace "^.+?$treePattern" -replace '^/?', '/' -replace '/?$', '/' + $Repository = $RepositoryUrl -replace "$treePattern.+?$" + if ($SparseFilter -notmatch '/[^\.]+\.[^\.]+?$') { + $sparseFilter = $SparseFilter -replace '/$','/**' + } + } + + # Pick out the owner and repository + $owner, $repositoryName = ($Repository -as [uri]).Segments[1,2] -replace '/$' + if (-not $owner -or -not $repositoryName) { + Write-Error "Could not identifier owner and repository $reposityUrl" + return + } + + # Get the path to our repos + $reposPaths = Join-Path $myAppData 'repos' + if (-not (Test-Path $reposPaths)) { + # (create it if it did not exist) + $null = New-Item -ItemType Directory $reposPaths -Force + if (-not $?) { return } + } + + # Get the path to repos of that owner + $ownerDirectory = Join-Path $reposPaths $owner + if (-not (Test-Path $ownerDirectory)) { + $null = New-Item -ItemType Directory $ownerDirectory -Force + if (-not $?) { return } + } + + # Finally, get the path to the repo itself. + $repoDirectory = Join-Path $ownerDirectory $repositoryName + + # If we are using `-Force`, remove the directory + if ($Force -and (Test-path $repoDirectory)) { + Remove-Item -Path $repoDirectory -Recurse -Force + } + + $namedParameters.Remove('Repository') + + # and get read to turn our git activity into progress bars + $gitProgress = @{Id=Get-Random;Status="Cloning $repository"} + $writeGitProgress = { + process { + $gitProgress.Activity = "$_" + Write-Progress @gitProgress + } + } + + $checkoutBranch = @() + if ($branch) { $checkoutBranch += $branch} + + # If the directory did not exist + if (-not (Test-Path $repoDirectory)) { + # push to the owner directory and clone it + Push-Location $ownerDirectory + + # If sparse filters were provided + if ($SparseFilter) { + # Perform a sparse clone + & $gitApp clone --depth 1 --no-checkout --sparse --filter=tree:0 $Repository "$repositoryName" *>&1 | + . $writeGitProgress + # and push into the location + Push-Location -LiteralPath $repoDirectory + # then set our sparse filter + $SparseArgs = @($SparseFilter) + & $gitApp sparse-checkout set --no-cone @SparseArgs *>&1 | + . $writeGitProgress + # and checkout. + & $gitApp checkout @checkoutBranch *>&1 | + . $writeGitProgress + # we should now have the files, so prepare an -Include filter + if (-not $namedParameters.Include) { + $namedParameters.Include = foreach ($sparse in $SparseFilter) { + "*$sparse" + } + } + # clean up our progress + $gitProgress.Completed = $true + Write-Progress @gitProgress + # and call ourselves with the repoDirectory + & $myCommandName -FilePath $repoDirectory @namedParameters + Pop-Location + } else { + # If no sparse filters were provided, just clone + & $gitApp clone $Repository *>&1 | + . $writeGitProgress + + $gitProgress.Completed = $true + Write-Progress @gitProgress + + if ($?) { + Push-Location $repoDirectory + # and call ourself + & $myCommandName -FilePath $repoDirectory @namedParameters + Pop-Location + } + } + + Pop-Location + if (-not $?) { return } + } + else { + # If the directory already exists, go there + Push-Location $repoDirectory + # and checkout + & $gitApp checkout @checkoutBranch | . $writeGitProgress + # and if a sparse filter was provided + if ($SparseFilter) { + if (-not $namedParameters.Include) { + # try to only include those files + $namedParameters.Include = foreach ($sparse in $SparseFilter) { + "*$sparse" + } + } + } + $gitProgress.Completed = $true + Write-Progress @gitProgress + + # Call ourselves and make a package from this directory + & $myCommandName -FilePath $repoDirectory @namedParameters + Pop-Location + } + } + + filter packTar { + + $resolvedItem = $_ + # First lets peek at the magic bytes that might give us a clue + $peekMagicBytes = Get-Content -AsByteStream -LiteralPath $resolvedItem.FullName -First 5 + + # If it starts with 31, 139, and 8, it may be a gzipped tarball + if (-not ($peekMagicBytes -and + $peekMagicBytes[0] -eq 31 -and + $peekMagicBytes[1] -eq 139 -and + $peekMagicBytes[2] -eq 8)) { + return + } + + # Put tar files into their own subdirectory + $tarDestination = Join-Path $myAppData 'tar' + if (-not (Test-Path $tarDestination)) { + $newDir = New-Item -ItemType Directory -Path $tarDestination -Force + if (-not $newDir) { return } + } + + # and each destination in its own subdirectory + $thisTarDestination = Join-Path $tarDestination ($resolvedItem.Name -replace '.\tar\.gz$') + if (-not (Test-Path $thisTarDestination)) { + $newDir = New-Item -ItemType Directory -Path $thisTarDestination -Force + if (-not $newDir) { return } + } + + # If the engine supports tarfiles + if ('Formats.Tar.TarFile' -as [Type]) { + # read the file as a stream + $openFile = [IO.File]::OpenRead($resolvedItem.FullName) + # and read that as a decompressed gzip stream + $gzipStream = [IO.Compression.GZipStream]::new($openFile, [IO.Compression.CompressionMode]'Decompress') + if (-not $gzipStream) { + $openFile.Close() + } + + # and extract the tarfile to that directory + [Formats.Tar.TarFile]::ExtractToDirectory($gzipStream, $thisTarDestination, $true) + # Track it it worked + $worked = $? + # and close up + $gzipStream.Close() + $openFile.Close() + # If it worked + if ($worked) { + # pack that directory + return Get-Item -LiteralPath "$thisTarDestination" | packDir + } + # otherwise, keep going + } elseif ($( + $tarApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('tar', 'Application') + $tarApp + )) { + # Alternatively, if the tar application installed, we can use that + $null = & $tarApp -xvf $resolvedItem.FullName -C "$thisTarDestination" + if ($?) { + # and then pack the directory + return Get-Item -LiteralPath "$thisTarDestination" | packDir + } + } else { + # If neither of those paths work, write a warning. + Write-Warning "$($resolvedItem.FullName) is a tar gz, but Formats.Tar.TarFile is not loaded and `tar` app does not exist" + } + } + + filter packUri { + # If the uri is an at:// uri + if ($uri.Scheme -eq 'at') { + $namedParameters.Remove('Uri') + # call ourself + & $myCommandName -AtUri $uri @namedParameters + return + } + + # If the URI is a git uri, or is a well-known git domain, + if ( + $uri.Scheme -eq 'git' -or + $uri.DnsSafeHost -match '^git(?>hub|lab)\.com$' + ) { + # pack a repository + $namedParameters.Remove('Uri') + & $myCommandName -Repository $uri @namedParameters + return + } + + # If the URI is a nuget.org or powershellgallery.com link. + if ($uri.DnsSafeHost -in 'nuget.org','www.nuget.org' -or + $uri.DnsSafeHost -in 'powershellgallery.com','www.powershellgallery.com') { + $namedParameters.Remove('uri') + # If so, call ourselves with the Nuget uri + & $myCommandName -NuGet $uri @namedParameters + return + } + + # If the uri is relative + if ( + (-not $uri.IsAbsoluteUri) + ) { + $slashKey = "$uri" -replace "^\.?/?", '/' + # try to find it in the package if we have one + if ($InputObject -is [IO.Packaging.Package] -and + $InputObject.PartExists($slashKey)) { + $inputPart = $InputObject.GetPart($slashKey) + $inputPart | getPartContent + } + + return + } + + # At this point lets just poke at the uri and make a package. + $WebResponse = Invoke-WebRequest -Uri $Uri + + # If the web response is a byte array + if ($WebResponse.Content -is [byte[]] -and + # and starts with the magic pair of bytes indicate it might be a zip + ($WebResponse.Content[0] -eq 80 -and $WebResponse.Content[-1] -eq 75) + ) { + # Create a stream from the response + $memoryStream = [IO.MemoryStream]::new($downloadPackage.Content) + # and open the package + $currentPackage = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') + # If that did not work, it will error, + if (-not $currentPackage) { + return # and we should return. + } + # Attach the memory stream to the package + $currentPackage | Add-Member NoteProperty MemoryStream $memoryStream -Force + # and decorate its type names + $currentPackage.pstypenames.insert(0, 'OP') + $currentPackage.pstypenames.insert(0, 'OpenPackage') + # and emit the package. + $currentPackage + } else { + # Ok, the response was not a package. + + # Let's turn it into a new package, or put it in the current package. + $currentPackage = getCurrentPack + + # Let's use the domain as the identifier + if (-not $currentPackage.PackageProperties.Identifier) { + $currentPackage.PackageProperties.Identifier = $uri.DnsSafeHost + } + + # Pick out the content type from the response + $responseType, $responseTypeOptions = $WebResponse.Headers['content-type'] -split ';' + # And get the major and minor type + $majorType, $minorType = $responseType -split '/', 2 + $minorType = $minorType -replace '^.+?\+' # replace anything in the minor type before a plus + # (this is effectively the extension) + $localPath = + # If we ask for a root uri + if ($uri.LocalPath -match '/$') { + # make it an index of the replied content type + $uri.LocalPath + 'index.' + $minorType + } else { + # otherwise, use the path they provided. + $uri.LocalPath + } + # Now that we know where we're putting it, let's create a part + $newPart = $currentPackage.CreatePart($localPath, $WebResponse.Headers['content-type'], 'Normal') + # and get the content stream + $newStream = $newPart.GetStream() + + # If the response was a byte array + if ($WebResponse.Content -is [byte[]]) { + # write that to the stream + $newStream.Write($WebResponse.Content, 0, $WebResponse.Length) + } else { + # otherwise, turn the stringified content into bytes + $buffer = $OutputEncoding.GetBytes("$($WebResponse.Content)") + # and write that + $newStream.Write($buffer, 0, $buffer.Length) + } + + $newStream.Close() + $newStream.Dispose() + + $currentPackage + } + } + + filter packZip { + $filePath = $_ + # Get the file info and read the file as a byte stream. + $fileInfo = $FilePath -as [IO.FileInfo] + # By reading the file with Get-Content -AsByteStream, we avoid locking the file + # (or the file being locked by another process) + $packageBytes = Get-Content -LiteralPath $FilePath -AsByteStream -Raw + + # If there were no bytes, return + if (-not $packageBytes) { return } + + # Create a memory stream from the byte array + $memoryStream = [IO.MemoryStream]::new($packageBytes) + # and open the package from the memory stream + $currentPackage = [IO.Packaging.Package]::Open($memoryStream, "Open", "ReadWrite") + # If that did not work, return. + if (-not $currentPackage) { return } + + $packageParts = @($currentPackage.GetParts()) + + # If we could open the file but not see the parts, it's a normal zip + if (-not $packageParts) { + # Close the package and the stream and try Expand-Archive. + $currentPackage.Close() + $memoryStream.Close() + + # To make things work, we want to extract to a folder and reload + # it's important that the folder name matches the file name, without the extension + $folderName = $fileInfo.Name -replace '\.[^\.]+?$' + + $folderPath = Join-Path $myAppData $folderName + # If the folder path did not exist + if (-not (Test-Path $folderPath)) { + # create it + $newDirectory = New-Item -ItemType Directory -Path $folderPath + if (-not $newDirectory) { + return + } + } + + # If the folder already exists, and we are not using force + if ((Test-Path $folderPath)) { + if (-not $Force) { + Write-Error "$FolderPath exists, use -Force" + return + } + # remove the current folder, so that no errant files show up in the package. + Remove-Item -Path $folderPath -Recurse -Force + } + + # If that worked, expand the archive to this path (giving us a fresh copy) + Expand-Archive -LiteralPath $fileInfo.FullName -DestinationPath $folderPath + if ($?) { + # and call ourself + return & $myCommandName -FilePath $folderPath @namedParameters + } else { + return + } + } + + # + $currentPackage = $currentPackage | + Add-Member NoteProperty FilePath $filePath -Force -PassThru | + Add-Member NoteProperty MemoryStream $memoryStream -Force -PassThru + + $currentPackage.pstypenames.insert(0, 'OP') + $currentPackage.pstypenames.insert(0, 'OpenPackage') + + # If there is no identifier, set it to the file name + if (-not $currentPackage.PackageProperties.Identifier) { + $currentPackage.PackageProperties.Identifier = $fileInfo.Name + } + + $currentPackage + } + + $gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git', 'Application') + + $myNoun = $MyInvocation.MyCommand.Name -replace '^.+?-' + } + + process { + $namedParameters = [Ordered]@{} + $PSBoundParameters + $namedParameters.Remove('ArgumentList') + + if ($InputObject) { + $namedParameters.Remove('InputObject') + switch ($InputObject) { + {$_ -is [Management.Automation.PSModuleInfo] -and $_.Path} { + & $myCommandName -Module $InputObject @namedParameters + return + } + {$_ -is [IO.FileInfo] -or $_ -is [IO.DirectoryInfo]} { + & $myCommandName -FilePath $InputObject.FullName @namedParameters + return + } + {$_ -is [uri]} { + & $myCommandName -Uri $InputObject @namedParameters + } + } + $namedParameters.InputObject = $InputObject + } + + # If arguments were provided, we are trying to be as natural with syntax as we can. + # Each string can map to a parameter, or to a verb of a related command to run. + if ($ArgumentList) { + + $packages = @() + $loadedModules = @(Get-Module) + $outputPackages = $true + + # Walk over each argument + :nextArgument for ($argNumber = 0; $argNumber -lt $ArgumentList.Length; $argNumber++) { + $arg = $ArgumentList[$argNumber] + # If it is a string, path info, or uri + if ($arg -is [Management.Automation.PSModuleInfo]) { + & $myCommandName -Module $arg @namedParameters + # and continue to the next argument. + continue nextArgument + } + + if ($arg -is [string] -or + $arg -is [Management.Automation.PathInfo] -or + $arg -is [uri] + ) { + # see if the path exists + $slashKey = $arg -replace '^/?', '/' -replace '/$' + # If the input was a package, and it exists in the package + if ( + $InputObject -is [IO.Packaging.Package] -and + $InputObject.PartExists($slashKey) + ) { + # read the contents of the part and emit them to output + & $myCommandName -Uri $slashKey @namedParameters -InputObject $InputObject + # and continue to the next argument. + continue nextArgument + } + if (Test-Path $arg -ErrorAction Ignore) { + # and if it does, turn it into a package + $packages += & $myCommandName -FilePath $arg @namedParameters + continue nextArgument + } + # If the argument started with at:// + if ($arg -match '^at://') { + # treat it as an at uri and turn it into a package. + $packages += & $myCommandName -AtUri $arg @namedParameters + continue nextArgument + } + # If the argument could be a URI + $argUri = $arg -as [uri] + # and that URI is absolute + if ($argUri.IsAbsoluteUri) { + # get that uri as a package + $packages += & $myCommandName -Uri $argUri @namedParameters + continue nextArgument + } + + # The the argument is a string and the name of a loaded module + if ($arg -is [string] -and + $loadedModules.Name -contains $arg) { + $packages += & $myCommandName -Module $arg @namedParameters + continue nextArgument + } + + # If we have reached this point, we want to look for a related command. + # For the sake of sanity, this will be done by looking for any function with the same noun but a different verb + $verbExists = $ExecutionContext.SessionState.InvokeCommand.GetCommand("$arg-$myNoun", "Function") + + if (-not $verbExists) { + Write-Warning "Unable to map argument '$arg'" + continue nextArgument + } + + # Create a pair of collections for named and unnamed parameters + $namedParameters = [Ordered]@{} + $unnamedArguments = @() + + # We want to look for parameters to the verb. + # These can occur before the next verb, or go to the end of the arguments. + # So we want to walk thru a new loop, and update our argument index. + :findParamaeters for ($nextArgNumber = $argNumber + 1; $nextArgNumber -lt $ArgumentList.Length; $nextArgNumber++) { + # get the next argument + $nextArg = $ArgumentList[$nextArgNumber] + # If it is a string + if ($nextArg -is [string]) { + # check for the next verb + $nextVerb = $ExecutionContext.SessionState.InvokeCommand.GetCommand("$nextArg-$myNoun", "Function") + # If we found one + if ($nextVerb) { + # decrement our index (we will want to parse this in the next pass) + $nextArgNumber-- + # break out of this loop so we can run the command + break findParamaeters + } else { + # If the next argument was not a verb, + # add it to our unnamed arguments + $unnamedArguments += $nextArg + } + } + # If the next argument is a dictionary + elseif ($nextArg -is [Collections.IDictionary]) { + # try to treat it as a splat + $mappedAnything = $false + + # Find all of the potential parameter aliases + $aliases = $verbExists.Parameters.Aliases + # and go over each key + $mappedAnything = $false + foreach ($key in $nextArg.Keys) { + # If the key is a parameter or an aliased parameter in the next verb + if ($verbExists.Parameters[$key] -or $aliases -contains $key) { + # put it into the named parameters table + # (if it already existed, make it as list) + if ($null -ne $namedParameters[$key]) { + $namedParameters[$key] = @($namedParameters[$key]) + $nextArg[$key] + } else { + $namedParameters[$key] = $nextArg[$key] + } + # and make sure to mark that we've mapped anything + $mappedAnything = $true + continue + } + } + + # If we didn't map any part of the dictionary to a splat + if (-not $mappedAnything) { + # pass it positionally + $unnamedArguments += $nextArg + } + } + else { + # If the arg was not a string or a dictionary, pass it positionally + $unnamedArguments += $nextArg + } + } + + # Set our main loop's argument number so we skip all we have parsed + $argNumber = $nextArgNumber + # and do not output packages since were are calling some other command with the packages we have. + $outputPackages = $false + # Pipe those packages into the verb with any arguments we have mapped + $packages | & $verbExists @unnamedArguments @namedParameters + # and continue to the next argument to this command. + continue nextArgument + } + } + + # If we did not run any additional commands, + if ($outputPackages) { + $packages # we want to output our packages now + } + + return # and return. + } + + # If we are passed a uri + if ($Uri) { + # pack it up + return packUri + } + + #region Open Package from Repository + if ($Repository) { + if (-not $gitApp) { + throw "No git" + } + + $Repository | packRepo + + return + } + #endregion Open Package from Repository + + #region Open Package from At Uri + if ($AtUri) { + $AtUri | packAt + return + } + #endregion Open Package From At Uri + + if ($Module) { + if ($module -isnot [Management.Automation.PSModuleInfo]) { + $loadedModules = @(Get-Module) + foreach ($moduleInfo in $loadedModules) { + if ($moduleInfo.Name -eq $module) { + $module = $moduleInfo + break + } + } + } + if ($module -is [Management.Automation.PSModuleInfo] -and + $module.Path) { + $namedParameters.Remove('Module') + & $myCommandName -FilePath ($module.Path | Split-Path) @namedParameters + return + } + } + + #region Open Package from Nuget + if ($NuGet) { + $downloadsPath = Join-Path $myAppData 'Downloads' + if (-not (Test-Path $downloadsPath)) { + $null = New-Item -ItemType Directory $downloadsPath -Force + if (-not $?) { return } + } + if ($nuget.Segments.Count -lt 2) { + throw "Not enough information in $nuget" + } + if ($nuget.Segments[1] -notmatch 'api') { + $nuSegments = $NuGet.Segments -replace 'packages', 'api/v2/package' -join '' + $nuget = $nuget.Scheme + '://' + $NuGet.DnsSafeHost + $( + if ($NuGet.Port -notin 80, 443) { + ":$($NuGet.Port)" + } + ) + $nuSegments + } + + if ($nuget.Segments[1] -match 'api') { + $downloadPackage = Invoke-WebRequest -Uri $nuget + + if ($downloadPackage.Content -is [byte[]]) { + $memoryStream = [IO.MemoryStream]::new($downloadPackage.Content) + $currentPackage = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') + $currentPackage | Add-Member NoteProperty MemoryStream $memoryStream -Force + $currentPackage.pstypenames.insert(0, 'OP') + $currentPackage.pstypenames.insert(0, 'OpenPackage') + $currentPackage + } + } else { + Write-Error "Could not convert $($PSBoundParameters['nuget']) to a package URL" + return + } + return + } + #endregion Open Package from Nuget + + if ($filePath) { + # Try to resolve the file path + $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath) + # If we could not resolve the path, exit + if (-not $resolvedPath ) { return } + + # Get each file beneath the path + foreach ($resolved in $resolvedPath) { + # (watch our for escaped characters) + $resolvedItem = Get-Item -LiteralPath ($resolved -replace '`') + + # If the item is a file + if ($resolvedItem -is [IO.FileInfo]) { + # make packages from the file + $resolvedItem | packFile + } + # If the item is a directory + elseif ($resolvedItem -is [IO.DirectoryInfo]) { + # make packages from the directory + $resolvedItem | packDir + } + } + + return + } + + getCurrentPack + } +} \ No newline at end of file From 286cd89c15ea1079215df024dda033a5ac1d596d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 4 Nov 2025 11:52:12 -0800 Subject: [PATCH 058/724] feat: `Start-OpenPackage` ( Fixes #5 ) --- Commands/Start-OpenPackage.ps1 | 450 +++++++++++++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 Commands/Start-OpenPackage.ps1 diff --git a/Commands/Start-OpenPackage.ps1 b/Commands/Start-OpenPackage.ps1 new file mode 100644 index 0000000..1a1af42 --- /dev/null +++ b/Commands/Start-OpenPackage.ps1 @@ -0,0 +1,450 @@ +function Start-OpenPackage { + <# + .SYNOPSIS + Starts a OpenPackage Server + .DESCRIPTION + Starts a server, using one or more archive packages as the storage. + .NOTES + If a URI in the package is requested, that URI will be returned. + + If a path does not have an extension, it will search for an .index.html. + + If the file was not found, a 404 code will be returned. + + If the package contains a `/404.html`, the content in this file will be returned with the 404 + + If another method than GET or HEAD is used, a 405 code will be returned. + + If the package contains a `/405.html`, then content in this file will be returned with the 405. + .EXAMPLE + + #> + [Alias('Start-OP')] + [CmdletBinding(PositionalBinding=$false)] + param( + # The path to an OpenXML file, or a glob that matches multiple OpenXML files. + [Parameter()] + [string] + $FilePath, + + # The Root + [string] + $RootUrl = "http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/", + + # The input object. This can be provided to avoid loading a file from disk. + [Parameter(ValueFromPipeline)] + [PSObject] + $InputObject, + + [Parameter(ValueFromPipelineByPropertyName)] + [PSObject] + $Layout, + + [Parameter(ValueFromPipelineByPropertyName)] + [Alias('Configuration')] + [PSObject] + $Config, + + [Parameter(ValueFromPipelineByPropertyName)] + [string[]] + $Allow = @('get', 'head'), + + [Parameter(ValueFromPipelineByPropertyName)] + [Collections.IDictionary] + $TypeMap = $( + ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap + ) + ) + + begin { + if ($PSVersionTable.PSVersion -lt '7.0') { + Write-Error "This feature requires thread jobs, which are part of PowerShell Core" + return + } + $JobDefinition = { + param([Collections.IDictionary]$IO) + + # unpack our IO into local variables + foreach ($variableName in $IO.Keys) { + $ExecutionContext.SessionState.PSVariable.Set($variableName, $IO[$variableName]) + } + + # declare some inner functions to help serve + filter serverError { + $errorCode = $_ + $response.ErrorCode = $errorCode + foreach ($pack in $package) { + # and serve any /405.html we find. + if ($pack.PartExists("/$errorCode.html")) { + "/$errorCode.html" | servePart + continue nextRequest + } + if ($pack.PartExists("/$errorCode.md")) { + "/$errorCode.md" | servePart + continue nextRequest + } + } + $response.Close() + } + + # declare a little filter to serve a part + filter servePart { + $uriPart = $_ + $packagePart = $package.GetPart($uriPart) + if ($uriPart -match '\.[^.]+?$' -and + $TypeMap[$matches.0] + ) { + $response.ContentType = $TypeMap[$matches.0] + } else { + $response.ContentType = $packagePart.ContentType + } + + Write-Host "Now Serving $uriPart as $($response.ContentType)" -ForegroundColor Cyan + if ($response.ContentType -match 'markdown' -and + $request.Url.LocalPath -notmatch '\.$(md|markdown)$') { + + $packageStream = $packagePart.GetStream() + $packageReader = [IO.StreamReader]::new($packageStream) + + $markdown = $packageReader.ReadToEnd() + + $packageReader.Close() + $packageReader.Dispose() + + $packageStream.Close() + $packageStream.Dispose() + $yamlHeaderPattern = "^---[\s\S]+?---" + if ($markdown -match $yamlHeaderPattern) { + $yaml = $matches.0 -replace '^---' -replace '---[\s\r\n]{0,}$' + $markdown = $markdown -replace $yamlHeaderPattern + } + $response.ContentType = "text/html" + $markdownHtml = (ConvertFrom-Markdown -InputObject $markdown).Html + + $markdownHtml = + if ($layout) { + if ( + $layout -is [Management.Automation.CommandInfo] -or + $layout -is [ScriptBlock] + ) { + $markdownHtml | & $layout + } elseif ($layout -is [string]) { + $layout -replace '{{content}}', $markdownHtml + } + } else { + $markdownHtml.Html + } + + $response.Close($outputEncoding.GetBytes("$markdownHtml"), $false) + return + } + + $partStream = $packagePart.GetStream() + + # Requests for large files + if ($partStream.Length -gt 256kb) { + # should be run in a background job (of a background job) + Start-ThreadJob -Name ( + $Request.Url -replace '^https?', 'file' + ) -ScriptBlock { + param($response, $partStream) + $partStream.CopyTo($response.OutputStream) + $partStream.Close() + $response.Close() + } -ArgumentList $response, $partStream -ThrottleLimit 256 + } else { + $partStream.CopyTo($response.OutputStream) + $partStream.Close() + $response.Close() + } + } + + # and start listening + :nextRequest while ($httpListener.IsListening) { + $getContextAsync = $httpListener.GetContextAsync() + # wait in short increments to minimize CPU impact and stay snappy + while (-not $getContextAsync.Wait(7)) { + + } + # Get our listener context + $context = $getContextAsync.Result + # and break that into a result and response + $request, $response = $context.Request, $context.Response + + + if ($Handler) { + $context | . $handler + $response.Close() + continue nextRequest + } + + $requestTime = [DateTime]::Now + Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] $($request.HttpMethod) $($request.Url)" + # If they asked for an inappropriate method + if ($request.HttpMethod -notin $Allow) { + # use the appropriate status code + + Write-Host -ForegroundColor Red "[$($requestTime.ToString('o'))] 405 $($request.HttpMethod) $($request.Url)" + 405 | serverError + # and continue to the next request + continue nextRequest + } + + + if ($request.HttpMethod -in 'put', 'post' -and + $request.InputStream.CanRead + ) { + $requestDataPath = + $request.Url.LocalPath,"/$($request.HttpMethod)/$($request.RequestTraceIdentifier)" -join + '' -replace '//', '/' + + + $memoryStream = [IO.MemoryStream]::new() + $request.InputStream.CopyTo($memoryStream) + + foreach ($pack in $package) { + if (-not $pack.PartExists($requestDataPath)) { + $newPart = $pack.CreatePart($requestDataPath, $request.ContentType, 'Normal') + $newStream = $newPart.GetStream() + $memoryStream.CopyTo($newStream) + $newStream.Close() + $newStream.Dispose() + break + } + } + $response.Close() + $memoryStream.Close() + $memoryStream.Dispose() + + continue nextRequest + } + + # Get the local path + $localPath = $request.Url.LocalPath + # if it lacks an extension, look for an index. + $potentialUris = @( + if ($localPath -notmatch '\..+?$') { + $noTrailingSlash = ($localPath -replace '/$') + $noTrailingSlash + '/index.html' + $noTrailingSlash + '/README.md' + $noTrailingSlash + '.html' + } else { + $localPath + } + ) + + # If we find the part + foreach ($uriPart in $potentialUris) { + $uriPart = $uriPart -replace '\s', '%20' + foreach ($pack in $package) { + if ($pack.PartExists($uriPart)) { + # serve it + $uriPart | servePart + + continue nextRequest + } + } + } + + $uriPart = ($localPath -replace '/$') + '/' + + if ($uriPart -match '/$') { + $uriPart = $localPath + $beneathPart = "^$([Regex]::Escape($uriPart))" + Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] $($request.HttpMethod) $($request.Url) Missing index, generating" + $nestedParts = @( + foreach ($pack in $package) { + foreach ($part in $pack.GetParts()) { + if ($part.Uri -match $beneathPart) { + $part + } + } + } + ) + + if ($nestedParts) { + $listing = @( + "
    " + foreach ($part in $nestedParts) { + "
  • " + "$($part.uri)" + "
  • " + } + "
" + ) -join [Environment]::NewLine + if ($Layout) { + Write-Host "Using Layout for $($request.Url)" + $listing = if ( + $Layout -is [Management.Automation.CommandInfo] -or + $layout -is [scriptblock] + ) { + $listing | & $Layout + } elseif ($Layout -is [string]) { + $Layout -replace '{{content}}', $listing + } + } + if ($listing) { + $response.ContentType = 'text/html' + $response.Close($OutputEncoding.GetBytes($listing), $false) + } else { + $response.Close() + } + + continue nextRequest + } + } + else { + Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] Marco $($request.HttpMethod) $($request.Url)" + } + + # If we did not find a part, set the appropriate status code + 404 | serverError + } + } + } + process { + + # Get our package + $package = + if ($inputObject -is [IO.Packaging.Package]) { + $inputObject + } else { + Get-OpenPackage -FilePath $FilePath + } + + # and return if we could not + if (-not $package) { return } + + # Create a listener + $httpListener = [Net.HttpListener]::new() + $httpListener.Prefixes.Add($RootUrl) + + # Create an IO object to populate the background runspace + $IO = [Ordered]@{ + HttpListener = $httpListener + Package = $package + ParentRunspace = [Runspace]::DefaultRunspace + } + $PSBoundParameters + + if (-not $io.TypeMap) { + $io.TypeMap = $TypeMap + } + + if (-not $io.Allow) { + $io.Allow = $Allow + } + + # Because this function exposes a server, we want to fire some events. + # First is an approve event: `Approve-Start` + # By using Register-EngineEvent, this can be handled + $beforeEvent = + New-Event -SourceIdentifier "Approve-Start" -MessageData $IO -Sender $MyInvocation.MyCommand -EventArguments $IO + + # We will just wait almost no time, so that the handler can run. + Start-Sleep -Milliseconds 0 + + # To reject the event, the handler can put one of three values in the `$event.MessageData` + if ($beforeEvent.MessageData.Rejected -or + $beforeEvent.MessageData.Reject -or + $beforeEvent.MessageData.No) { + return + } + + $httpListener.Start() + + if ($?) { + Write-Warning "Listening on $rootUrl" + } + + if (-not $package.PackageProperties.Identifier) { + Write-Warning "No Package Identifier" + } + + if (-not $site) { + $site = [Ordered]@{} + } + + if ($config -is [ScriptBlock] -or + $config -is [Management.Automation.CommandInfo]) { + $IO.Config = $config = . $config + } + + $IO.Site = $site + + $partsToBuild = @( + foreach ($packagePart in @($package.GetParts())) { + if ($packagePart.Uri -match '\.(?>markdown|md)$') { + $htmlPartUri = + $packagePart.Uri -replace '\.(?>markdown|md)$', '.html' -replace 'README\.html$', 'index.html' + if (-not $package.PartExists($htmlPartUri)) { + $htmlPartUri + } + } + if ($packagePart.Uri -match '\.[^\.]+\.ps1$') { + $packagePart.Uri + } + } + ) + foreach ($packagePart in @($package.GetParts())) { + if ($packagePart.Uri -match '\.(?>markdown|md)$') { + $htmlPartUri = + $packagePart.Uri -replace '\.(?>markdown|md)$', '.html' -replace 'README\.html$', 'index.html' + if (-not $package.PartExists($htmlPartUri)) { + + $markdownStream = $packagePart.GetStream() + $markdownStreamReader = [IO.StreamReader]::new($markdownStream) + + $markdown = $markdownStreamReader.ReadToEnd() + + $markdownStreamReader.Close() + $markdownStreamReader.Dispose() + + $markdownHtml = ConvertFrom-Markdown -InputObject $markdown + + $markdownHtml = + if ($layout) { + if ( + $layout -is [Management.Automation.CommandInfo] -or + $layout -is [ScriptBlock] + ) { + $markdownHtml | & $layout + } elseif ($layout -is [string]) { + $layout -replace '{{content}}', $markdownHtml + } + } else { + $markdownHtml.Html + } + + if (-not $markdownHtml) { continue } + + $markdownHtml = $markdownHtml -join [Environment]::NewLine + + $markdownBytes = $OutputEncoding.GetBytes($markdownHtml) + + $htmlPart = $package.CreatePart($htmlPartUri, 'text/html', 'Normal') + + $htmlPartStream = $htmlPart.GetStream() + + $htmlPartStream.Write($markdownBytes, 0, $markdownBytes.Length) + $htmlPartStream.Close() + } + } + # *.*.ps1 files + if ($packagePart.Uri -match '\.[^\.]+\.ps1$' -and $Layout) { + + } + } + + # Start a thread job + $startedJob = Start-ThreadJob -ScriptBlock $JobDefinition -ArgumentList $IO -Name $RootUrl -ThrottleLimit 100 | + Add-Member NoteProperty IO $IO -Force -PassThru | + Add-Member NoteProperty HttpListener $httpListener -Force -PassThru | + Add-Member NoteProperty Package $package -Force -PassThru | + Add-Member NoteProperty Url $RootUrl -Force -PassThru + + $null = New-Event -SourceIdentifier Start-OpenPackage -Sender $MyInvocation.MyCommand -EventArguments $startedJob -MessageData $IO + + $startedJob + + } +} \ No newline at end of file From e1a4eede6dbb76cab7bfc9e8cb3f593354d2cd59 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 4 Nov 2025 11:54:41 -0800 Subject: [PATCH 059/724] feat: `Set-OpenPackage` ( Fixes #3 ) --- Commands/Set-OpenPackage.ps1 | 125 +++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 Commands/Set-OpenPackage.ps1 diff --git a/Commands/Set-OpenPackage.ps1 b/Commands/Set-OpenPackage.ps1 new file mode 100644 index 0000000..c4f282d --- /dev/null +++ b/Commands/Set-OpenPackage.ps1 @@ -0,0 +1,125 @@ +function Set-OpenPackage +{ + <# + .SYNOPSIS + Sets Open Package content + .DESCRIPTION + Sets content in an Open Packaging Conventions archive. + .EXAMPLE + $miniServer = Get-OpenPackage | + Set-OpenPackage -Uri '/index.html' -Content ([xml]"

Hello World

") -ContentType text/html | + Start-OpenPackage + Start-Process -FilePath $miniServer.Name + .LINK + Get-OpenPackage + #> + [Alias('Set-OP','sop')] + param( + # The uri to set + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [Alias('Url','Href','PartUri','PartUrl','LocalPath')] + [uri] + $Uri, + + # The content type. By default, `text/plain` + [Parameter(ValueFromPipelineByPropertyName)] + [string] + $ContentType = 'text/plain', + + # The content to set. + [Parameter(ValueFromPipelineByPropertyName)] + [PSObject] + $Content, + + # The input object. + # This must be a package, and it must be writeable. + [Parameter(ValueFromPipeline)] + [Alias('Package')] + [PSObject] + $InputObject, + + # Sets a part, even if it already exists. + [switch] + $Force + ) + + process { + # If there is no input, there is nothing to do + if (-not $InputObject) { return } + # If the input is not a package, pass it thru. + if ($InputObject -isnot [IO.Packaging.Package]) { + return $InputObject + } + + # If the uri is not prefixed, + if ($uri -notmatch '^/') { + $uri = "/$uri" # add it to avoid easy errors. + } + + # Create or recreate the part (otherwise writes might be partial) + $part = + if ($InputObject.PartExists($uri)) { + + if (-not $Force) { + Write-Error "'$uri' already exists, use -Force to overwrite" + return + } + + $currentPart = $InputObject.GetPart($uri) + + $currentPartUri, $currentContentType, $currentCompressionLevel = + $currentPart.Uri, $currentPart.ContentType, $currentPart.CompressionLevel + + $inputObject.DeletePart($currentPartUri) + + $InputObject.CreatePart($currentPartUri, $currentContentType, $currentCompressionLevel) + } else { + $InputObject.CreatePart($uri, $ContentType) + } + + if (-not $?) { return } + + # Get the stream + $partStream = $part.GetStream() + # First see if the content is a byte[] + if ($content -is [byte[]]) { + # if so, just write it + $partStream.Write($content, 0, $content.Length) + } + # If the content is a stream, + elseif ($content -is [IO.Stream]) { + # copy it in. + $content.CopyTo($partStream) + } + # If the content was xml or could be, + elseif ($content -is [xml] -or ($contentXml = $content -as [xml])) { + if ($contentXml) { $content = $contentXml } + $content.Save($partStream) + } elseif ($content -is [string]) { + # Put strings in as a byte array. + $buffer = $OutputEncoding.GetBytes($content) + $partStream.Write($buffer, 0, $buffer.Length) + } elseif ($contentBytes = $content -as [byte[]]) { + # Bytes are obviously a byte array + $partStream.Write($contentBytes, 0, $contentBytes.Length) + } + elseif ($ContentType -match '[/\+]json') { + # Explicitly typed json can be converted to json + $buffer = $OutputEncoding.GetBytes((ConvertTo-Json -InputObject $content -Depth 10)) + $partStream.Write($buffer, 0, $buffer.Length) + } + else { + # and everything else is stringified + $buffer = $OutputEncoding.GetBytes("$content") + $partStream.Write($buffer, 0, $buffer.Length) + } + + # Close the part stream + $partStream.Close() + + # and invalidate the parts cache on the object + $inputObject.PSObject.Properties.Remove('.Parts') + # then pass it thru so we can keep piping. + $inputObject + } +} \ No newline at end of file From 203d26c6c7b4be5f7edec6d25e370aa2919c3404 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 4 Nov 2025 13:24:12 -0800 Subject: [PATCH 060/724] feat: `Copy-OpenPackage` ( Fixes #7 ) --- Commands/Copy-OpenPackage.ps1 | 129 ++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 Commands/Copy-OpenPackage.ps1 diff --git a/Commands/Copy-OpenPackage.ps1 b/Commands/Copy-OpenPackage.ps1 new file mode 100644 index 0000000..06a54bb --- /dev/null +++ b/Commands/Copy-OpenPackage.ps1 @@ -0,0 +1,129 @@ +function Copy-OpenPackage +{ + <# + .SYNOPSIS + Copies Open Packages + .DESCRIPTION + Copies Contents from one packages to another. + .EXAMPLE + Copy-OpenPackage -DestinationPath ./Examples/Copy.docx -InputObject ./Examples/Sample.docx -Force + #> + [Alias('Copy-OP','cpop')] + param( + # The destination. + # If this is not a `[IO.Packaging.Package]`, it will be considered a file path. + [Parameter(ValueFromPipelineByPropertyName)] + [PSObject] + $Destination, + + # The input object + [Parameter(ValueFromPipeline)] + [PSObject] + $InputObject, + + # If set, will update existing packages. + [switch] + $Force + ) + + process { + # If the input was not a package + if ($inputObject -isnot [IO.Packaging.Package]) { + $loadedPackage = # see if it is a file we can load + if ($InputObject -is [IO.FileInfo]) { + Get-OpenPackage $InputObject.FullName + } elseif ($inputFile = Get-Item -ErrorAction Ignore -Path "$InputObject") { + Get-OpenPackage $inputFile.FullName + } + + # If it was not, return. + if ($loadedPackage -isnot [IO.Packaging.Package]) { return } + $InputObject = $loadedPackage + } + + $destinationPackage = + if ($Destination -is [IO.Packaging.Package]) { + $Destination + } else { + # Get the absolute path of the destination, without creating the file, + $unresolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath) + + # then see if the file exists. + $fileExists = Test-Path $unresolvedDestination + # If it does and we are not using the -Force + if ($fileExists -and -not $force) { + # write an error + Write-Error "$unresolvedDestionation already exists, use -Force to update" -Category ResourceExists + return + } + # If it did not exist, create it with New-Item -Force + elseif (-not $fileExists) + { + # this will create intermediate paths. + $newFile = New-Item -ItemType File -Path $unresolvedDestination -Force + if (-not $newFile) { return } + } + # Try to open or create our package for read and write. + [IO.Packaging.Package]::Open($unresolvedDestination, 'OpenOrCreate', 'ReadWrite') + } + + # If we could not, we are done. + if (-not $destinationPackage) { return } + + # Start off with the package properties + foreach ($property in $InputObject.PackageProperties.psobject.properties) { + if ($property.IsSettable) { + $destinationPackage.PackageProperties.$($property.Name) = + $InputObject.PackageProperties.$($property.Name) + } + } + + # Get the input parts and relationships + $inputPackageParts = $InputObject.GetParts() + $inputPackageRelationships = $InputObject.GetRelationships() + + # For each part in the input + foreach ($inputPart in $inputPackageParts) { + # Create or open a part in the destination + $destinationPart = + if (-not $destinationPackage.PartExists($inputPart.Uri)) { + $destinationPackage.CreatePart($inputPart.Uri, $inputPart.ContentType, $inputPart.CompressionOption) + } else { + $destinationPackage.GetPart($inputPart.Uri) + } + + # and copy the streams. + $inputStream = $inputPart.GetStream() + $destinationStream = $destinationPart.GetStream() + $inputStream.CopyTo($destinationStream) + $inputStream.Close() + $destinationStream.Close() + } + + # Then, create any relationships that do not exist. + foreach ($inputRelationship in $inputPackageRelationships) { + if ($inputRelationship) { + if (-not $destinationPackage.RelationshipExists($inputRelationship.id)) { + $null = $destinationPackage.CreateRelationship( + $inputRelationship.targetUri, + $inputRelationship.targetMode, + $inputRelationship.relationshipType, + $inputRelationship.id + ) + } + } + } + + if ($Destination -isnot [IO.Packaging.Package]) { + # We can now close our package, writing the file. + $destinationPackage.Close() + + # We want to open it right back up again as we output the updated file. + Get-OpenPackage -FilePath $unresolvedDestination + } else { + $destinationPackage + } + + + } +} \ No newline at end of file From 2cb1c5107485f9ff6b7268371b30a37399dceabd Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 4 Nov 2025 13:25:25 -0800 Subject: [PATCH 061/724] feat: `Export-OpenPackage` ( Fixes #39 ) --- Commands/Export-OpenPackage.ps1 | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Commands/Export-OpenPackage.ps1 diff --git a/Commands/Export-OpenPackage.ps1 b/Commands/Export-OpenPackage.ps1 new file mode 100644 index 0000000..bbcffd8 --- /dev/null +++ b/Commands/Export-OpenPackage.ps1 @@ -0,0 +1,38 @@ +function Export-OpenPackage { + <# + .SYNOPSIS + Exports OpenPackage packages + .DESCRIPTION + Exports loaded packages to a file. + #> + [Alias('Save-OpenPackage','epop','svop')] + param( + # The file path to save the turtle graphics pattern. + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [Alias('Path')] + [string] + $FilePath, + + # The input object. + # This must be a package loaded with this module. + [Parameter(ValueFromPipeline)] + [PSObject] + $InputObject, + + # If set, will force the export even if a file already exists. + [switch] + $Force + ) + + process { + # If there is no input return + if (-not $InputObject) { return } + # If the input is not a package, pass it thru + if ($InputObject -isnot [IO.Packaging.Package]) { + return $InputObject + } + + Copy-OpenPackage -DestinationPath $FilePath -InputObject $inputObject -force:$Force + } +} + From 01f69dfc60f63557f5da663f48ec665f06c820d3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 4 Nov 2025 13:31:53 -0800 Subject: [PATCH 062/724] feat: `Remove-OpenPackage` ( Fixes #40 ) --- Commands/Remove-OpenPackage.ps1 | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Commands/Remove-OpenPackage.ps1 diff --git a/Commands/Remove-OpenPackage.ps1 b/Commands/Remove-OpenPackage.ps1 new file mode 100644 index 0000000..050c2e6 --- /dev/null +++ b/Commands/Remove-OpenPackage.ps1 @@ -0,0 +1,37 @@ +function Remove-OpenPackage { + <# + .SYNOPSIS + Removes parts from an open package + .DESCRIPTION + Removes content parts from an open package. + #> + [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] + [Alias('Remove-OP','rop')] + param( + # One or more URIs to remove + [Parameter(ValueFromPipelineByPropertyName)] + [Uri[]] + $Uri, + + # The input object. + # If this is not a package, the input will be passed thru and nothing will be removed. + [Parameter(ValueFromPipeline)] + [Alias('Package')] + [PSObject] + $InputObject + ) + + process { + if ($InputObject -isnot [IO.Packaging.Package]) { + return $InputObject + } + + foreach ($part in @($InputObject.GetParts())) { + if ($part.Uri -in $Uri -and + $PSCmdlet.ShouldProcess("Remove $Uri") + ) { + $InputObject.DeletePart($part.Uri) + } + } + } +} \ No newline at end of file From 945e8f97c96d19b35e0d17c7b009029cf2afa201 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 4 Nov 2025 15:30:25 -0800 Subject: [PATCH 063/724] feat: OP Module Scaffolding ( Fixes #1 ) --- OP.psd1 | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 OP.psd1 diff --git a/OP.psd1 b/OP.psd1 new file mode 100644 index 0000000..81d2396 --- /dev/null +++ b/OP.psd1 @@ -0,0 +1,132 @@ +# +# Module manifest for module 'OP' +# +# Generated by: James Brundage +# +# Generated on: 10/19/2025 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'OP.psm1' + +# Version number of this module. +ModuleVersion = '0.1' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = 'a5d8b33f-074f-435f-8397-dda2d7f15ecf' + +# Author of this module +Author = 'James Brundage' + +# Company or vendor of this module +CompanyName = 'Start-Automating' + +# Copyright statement for this module +Copyright = '2025 Start-Automating' + +# Description of the functionality provided by this module +Description = 'An Overpowered little module for Open Packages' + +# Minimum version of the PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +TypesToProcess = @('OP.types.ps1xml') + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = '*' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('OpenPackages','Zip','Nuget','PowerShellGallery','Git','AtProto','WebServer','Overpowered') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/PowerShellWeb/OP/tree/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PowerShellWeb/OP' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + From cae660b868c68ad6e12e9d0a992093137dc4f36c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 5 Nov 2025 14:07:02 -0800 Subject: [PATCH 064/724] feat: Select-OpenPackage ( Fixes #44 ) --- Commands/Select-OpenPackage.ps1 | 273 ++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 Commands/Select-OpenPackage.ps1 diff --git a/Commands/Select-OpenPackage.ps1 b/Commands/Select-OpenPackage.ps1 new file mode 100644 index 0000000..d3218b5 --- /dev/null +++ b/Commands/Select-OpenPackage.ps1 @@ -0,0 +1,273 @@ +function Select-OpenPackage +{ + <# + .SYNOPSIS + Selects Open Package content + .DESCRIPTION + Selects content from an Open Package using Regular Expressions or XPath + .LINK + Select-String + .LINK + Select-Xml + #> + [Alias('Select-OP','scop')] + [CmdletBinding(DefaultParameterSetName='All')] + param( + [Parameter(ParameterSetName='Select-String',ValueFromPipelineByPropertyName)] + [PSObject[]] + $Pattern, + + # Indicates that the cmdlet uses a simple match rather than a regular expression match. + [Parameter(ParameterSetName='Select-String')] + [switch] + $SimpleMatch, + + # Indicates that the cmdlet matches are case-sensitive. By default, pattern matches aren't case-sensitive. + [Parameter(ParameterSetName='Select-String')] + [switch] + $CaseSensitive, + + <# + Indicates that the cmdlet returns a simple response instead of a `[MatchInfo]` object. + The returned value is `$true` if the pattern is found or `$null` if the pattern is not found. + #> + [Parameter(ParameterSetName='Select-String')] + [switch] + $Quiet, + + + + # Only the first instance of matching text is returned from each input file. + # This is the most efficient way to retrieve a list of files that have contents matching the regular expression. + [Parameter(ParameterSetName='Select-String')] + [switch] + $List, + + <# + By default, `Select-String` highlights the string that matches the pattern you searched for with the + `-Pattern` parameter. The `-NoEmphasis` parameter disables the highlighting. + #> + [Parameter(ParameterSetName='Select-String')] + [switch] + $NoEmphasis, + + <# + Includes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + + Wildcards are permitted. + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Include, + + <# + Excludes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + + Wildcards are permitted. + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Exclude, + + <# + Includes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $IncludeContentType, + + <# + Excludes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $ExcludeContentType, + + # The `-NotMatch` parameter finds text that doesn't match the specified pattern. + [Parameter(ParameterSetName='Select-String')] + [switch] + $NotMatch, + + <# + Indicates that the cmdlet searches for more than one match in each line of text. + Without this parameter, `Select-String` finds only the first match in each line of text. + + When `Select-String` finds more than one match in a line of text, it still emits only one + `[MatchInfo]` object for the line, but the `.Matches` property of the object contains all the + matches. + #> + [Parameter(ParameterSetName='Select-String')] + [switch] + $AllMatches, + + <# + + Captures the specified number of lines before and after the line that matches the pattern. + + If you enter one number as the value of this parameter, that number determines the number of lines + captured before and after the match. If you enter two numbers as the value, the first number + determines the number of lines before the match and the second number determines the number of lines + after the match. For example, `-Context 2,3`. + #> + [Parameter(ParameterSetName='Select-String')] + [ValidateNotNullOrEmpty()] + [ValidateCount(1, 2)] + [ValidateRange(0, 2147483647)] + [int[]] + $Context, + + <# + Causes the cmdlet to output only the matching strings, rather than **MatchInfo** objects. This is + the results in behavior that's the most similar to the Unix **grep** or Windows **findstr.exe** + commands. + #> + [Parameter(ParameterSetName='Select-String')] + [switch] + $Raw, + + # Specifies an XPath search query. The query language is case-sensitive. + [Parameter(Mandatory,ParameterSetName='Select-XML')] + [Parameter(ValueFromPipelineByPropertyName)] + [PSObject] + $XPath, + + <# + Specifies a hash table of the namespaces used in the XML. + + Use the format`@{ = }`. + + When the XML uses the default namespace, which begins with xmlns, use an arbitrary key for the + namespace name. You cannot use xmlns. In the XPath statement, prefix each node name with the + namespace name and a colon, such as `//namespaceName:Node`. + #> + [Parameter(ParameterSetName='Select-XML')] + [Parameter(ValueFromPipelineByPropertyName)] + [Collections.IDictionary] + $Namespace, + + [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)] + [Alias('Package')] + [PSObject] + $InputObject + ) + + begin { + $selectString = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-String','Cmdlet') + $selectXml = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-Xml','Cmdlet') + } + + process { + if ($InputObject -isnot [IO.Packaging.Package]) { + return $InputObject + } + + $inputParts = @($InputObject.GetParts()) + + $inputParts = @( + :nextPart foreach ($inputPart in $inputParts) { + if ($ExcludeContentType) { + foreach ($wildcard in $Exclude) { + if ($inputPart.ContentType -like $wildcard) { + continue nextPart + } + } + } + + if ($Exclude) { + foreach ($wildcard in $Exclude) { + if ($inputPart.Uri -like $wildcard) { + continue nextPart + } + } + } + + if ($include) { + $included = $false + :included foreach ($wildcard in $include) { + if ($inputPart.Uri -like $wildcard) { + $included = $true + break included + } + } + if (-not $included) { + continue nextPart + } + } + + if ($IncludeContentType) { + $included = $false + :included foreach ($wildcard in $include) { + if ($inputPart.ContentType -like $wildcard) { + $included = $true + break included + } + } + if (-not $included) { + continue nextPart + } + } + + $inputPart + } + ) + + :nextPart foreach ($inputPart in $inputParts) { + if (-not $Pattern -and -not $XPath) { + $inputPart + continue + } + $inputStream = $inputPart.GetStream() + $inputStreamReader = [IO.StreamReader]::new($inputStream) + + $inputText = $inputStreamReader.ReadToEnd() + + $inputStreamReader.Close() + $inputStreamReader.Dispose() + $inputStream.Close() + $inputStream.Dispose() + + $selectParameters = [Ordered]@{} + + $inputAsXml = $inputText -as [xml] + + if ($XPath -and $inputAsXml) { + $selectParameters.XPath = $XPath + if ($namespace) { + $selectParameters.Namespace = @{} + foreach ($key in $Namespace.Keys) { + $selectParameters.Namespace[$key] = $Namespace[$key] + } + } + $inputAsXml | + & $selectXml @selectParameters | + Add-Member NoteProperty Uri $inputPart.Uri -Force -PassThru | + Select-Object Node, Pattern, Uri + } + + if ($Pattern) { + foreach ($key in $PSBoundParameters.Keys) { + if ($selectString.Parameters[$key]) { + $selectParameters[$key] = $PSBoundParameters[$key] + } + } + $selectParameters.Remove('Include') + $selectParameters.Remove('Exclude') + + Select-String @selectParameters -InputObject $inputText | + Add-Member NoteProperty Uri $inputPart.Uri -Force -PassThru + } + } + } +} \ No newline at end of file From a6b14a8fa309e4fd254c52ccd58b68ec6d8b886d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 6 Nov 2025 14:06:46 -0800 Subject: [PATCH 065/724] feat: `OpenPackage.get_ManifestJson` ( Fixes #46 ) --- Types/OpenPackage/get_ManifestJson.ps1 | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Types/OpenPackage/get_ManifestJson.ps1 diff --git a/Types/OpenPackage/get_ManifestJson.ps1 b/Types/OpenPackage/get_ManifestJson.ps1 new file mode 100644 index 0000000..74c45fd --- /dev/null +++ b/Types/OpenPackage/get_ManifestJson.ps1 @@ -0,0 +1,34 @@ +<# +.SYNOPSIS + Gets a package's `manifest.json` +.DESCRIPTION + Gets the content of any `manifest.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/manifest\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. \ No newline at end of file From b9550c6782c688c8cdf4fbbacf4704a9c4d27179 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 6 Nov 2025 22:07:12 +0000 Subject: [PATCH 066/724] feat: `OpenPackage.get_ManifestJson` ( Fixes #46 ) --- OP.types.ps1xml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 8dc9e48..d37c6cc 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -395,6 +395,45 @@ param([DateTime]$LastPrinted) $this.PackageProperties.LastPrinted = $LastPrinted
+ + ManifestJson + + <# +.SYNOPSIS + Gets a package's `manifest.json` +.DESCRIPTION + Gets the content of any `manifest.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/manifest\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. + + Modified From a7d70dd1ba3814a5e6b84c66d93d7aff02826b62 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 6 Nov 2025 14:21:33 -0800 Subject: [PATCH 067/724] feat: `OpenPackage.get_TemplateJson` ( Fixes #45 ) --- Types/OpenPackage/get_TemplateJson.ps1 | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Types/OpenPackage/get_TemplateJson.ps1 diff --git a/Types/OpenPackage/get_TemplateJson.ps1 b/Types/OpenPackage/get_TemplateJson.ps1 new file mode 100644 index 0000000..2efe884 --- /dev/null +++ b/Types/OpenPackage/get_TemplateJson.ps1 @@ -0,0 +1,34 @@ +<# +.SYNOPSIS + Gets a package's `template.json` +.DESCRIPTION + Gets the content of any `template.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/template\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. \ No newline at end of file From 5e1c496e1d5d7cc9b377434a21cfd1299d06e02b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 6 Nov 2025 22:21:54 +0000 Subject: [PATCH 068/724] feat: `OpenPackage.get_TemplateJson` ( Fixes #45 ) --- OP.types.ps1xml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d37c6cc..f95540e 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -705,6 +705,45 @@ param([string]$Subject) $this.PackageProperties.Subject = $Subject + + TemplateJson + + <# +.SYNOPSIS + Gets a package's `template.json` +.DESCRIPTION + Gets the content of any `template.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/template\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. + + Title From 7799021b2f3c22c2df4e4cf6d083d9f9345f9d9e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 6 Nov 2025 18:02:13 -0800 Subject: [PATCH 069/724] feat: `Copy-OpenPackage` ( Fixes #7 ) Adding filtering --- Commands/Copy-OpenPackage.ps1 | 83 +++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/Commands/Copy-OpenPackage.ps1 b/Commands/Copy-OpenPackage.ps1 index 06a54bb..0407d10 100644 --- a/Commands/Copy-OpenPackage.ps1 +++ b/Commands/Copy-OpenPackage.ps1 @@ -14,7 +14,51 @@ function Copy-OpenPackage # If this is not a `[IO.Packaging.Package]`, it will be considered a file path. [Parameter(ValueFromPipelineByPropertyName)] [PSObject] - $Destination, + $Destination, + + <# + Includes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + + Wildcards are permitted. + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Include, + + <# + Excludes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + + Wildcards are permitted. + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Exclude, + + <# + Includes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $IncludeContentType, + + <# + Excludes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $ExcludeContentType, # The input object [Parameter(ValueFromPipeline)] @@ -26,6 +70,10 @@ function Copy-OpenPackage $Force ) + begin { + $selectOpenPackage = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-OpenPackage','Function') + } + process { # If the input was not a package if ($inputObject -isnot [IO.Packaging.Package]) { @@ -44,9 +92,9 @@ function Copy-OpenPackage $destinationPackage = if ($Destination -is [IO.Packaging.Package]) { $Destination - } else { + } elseif ($Destination) { # Get the absolute path of the destination, without creating the file, - $unresolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath) + $unresolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination) # then see if the file exists. $fileExists = Test-Path $unresolvedDestination @@ -65,10 +113,20 @@ function Copy-OpenPackage } # Try to open or create our package for read and write. [IO.Packaging.Package]::Open($unresolvedDestination, 'OpenOrCreate', 'ReadWrite') + } else { + $memoryStream = [IO.MemoryStream]::new() + [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') | + Add-Member NoteProperty MemoryStream $memoryStream -Force -PassThru } # If we could not, we are done. if (-not $destinationPackage) { return } + if ($destinationPackage.pstypenames -notcontains 'OP') { + $destinationPackage.pstypenames.insert(0, 'OP') + } + if ($destinationPackage.pstypenames -notcontains 'OpenPackage') { + $destinationPackage.pstypenames.insert(0, 'OpenPackage') + } # Start off with the package properties foreach ($property in $InputObject.PackageProperties.psobject.properties) { @@ -80,6 +138,15 @@ function Copy-OpenPackage # Get the input parts and relationships $inputPackageParts = $InputObject.GetParts() + + $selectSplat = [Ordered]@{InputObject=$InputObject} + foreach ($key in $PSBoundParameters.Keys) { + if ($selectOpenPackage.Parameters[$key]) { + $selectSplat[$key] = $PSBoundParameters[$key] + } + } + $inputPackageParts = $selectedParts = Select-OpenPackage @selectSplat + $inputPackageRelationships = $InputObject.GetRelationships() # For each part in the input @@ -112,18 +179,16 @@ function Copy-OpenPackage ) } } - } + } - if ($Destination -isnot [IO.Packaging.Package]) { + if ($destination -and $Destination -isnot [IO.Packaging.Package]) { # We can now close our package, writing the file. $destinationPackage.Close() # We want to open it right back up again as we output the updated file. - Get-OpenPackage -FilePath $unresolvedDestination + Get-OpenPackage -FilePath $unresolvedDestination } else { $destinationPackage - } - - + } } } \ No newline at end of file From 77f055db21a43e1640fe8772d22076ef15ec8e70 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 6 Nov 2025 18:04:48 -0800 Subject: [PATCH 070/724] feat: `Split-OpenPackage ( Fixes #47 ) --- Commands/Split-OpenPackage.ps1 | 95 ++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Commands/Split-OpenPackage.ps1 diff --git a/Commands/Split-OpenPackage.ps1 b/Commands/Split-OpenPackage.ps1 new file mode 100644 index 0000000..7a5d057 --- /dev/null +++ b/Commands/Split-OpenPackage.ps1 @@ -0,0 +1,95 @@ +function Split-OpenPackage { + <# + .SYNOPSIS + + #> + [CmdletBinding(PositionalBinding=$false)] + param( + [Parameter(ValueFromRemainingArguments)] + [PSObject[]] + $Property, + <# + Includes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + + Wildcards are permitted. + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Include, + + <# + Excludes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + + Wildcards are permitted. + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Exclude, + + <# + Includes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $IncludeContentType, + + <# + Excludes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $ExcludeContentType, + + [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)] + [Alias('Package')] + [PSObject] + $InputObject + ) + + begin { + $selectOpenPackage = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-OpenPackage','Function') + } + + process { + if ($InputObject -isnot [IO.Packaging.Package]) { + return $InputObject + } + $selectSplat = [Ordered]@{InputObject=$InputObject} + foreach ($key in $PSBoundParameters.Keys) { + if ($selectOpenPackage.Parameters[$key]) { + $selectSplat[$key] = $PSBoundParameters[$key] + } + } + $selectedParts = Select-OpenPackage @selectSplat + + if (-not $property) { + $Property = { @(@($_.Uri -split '/' -ne '')[-1] -split '\.')[-1] } + } + foreach ($group in $selectedParts | Group-Object -Property $Property) { + + $splitPackage = $inputObject | + Copy-OpenPackage -Include $group.Group.Uri + + $splitPackage.Identifier = + $splitPackage.Identifier -replace + "(?:\p{P}$([Regex]::Escape($group.Name)))?$", ('.' + $group.Name) + + $splitPackage + } + } + end { + + } +} \ No newline at end of file From 3630ce8f2281adc87cdbecf28a915c8eaf9dcfd5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 6 Nov 2025 18:26:34 -0800 Subject: [PATCH 071/724] feat: `Join-OpenPackage ( Fixes #48 ) --- Commands/Join-OpenPackage.ps1 | 94 +++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 Commands/Join-OpenPackage.ps1 diff --git a/Commands/Join-OpenPackage.ps1 b/Commands/Join-OpenPackage.ps1 new file mode 100644 index 0000000..cc63dc0 --- /dev/null +++ b/Commands/Join-OpenPackage.ps1 @@ -0,0 +1,94 @@ +function Join-OpenPackage +{ + <# + .SYNOPSIS + Joins Open Packages + .DESCRIPTION + Joins multiple open packages into a single open package + + #> + [CmdletBinding(PositionalBinding=$false)] + param( + [Parameter(ValueFromPipeline)] + [PSObject] + $InputObject, + + <# + Includes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + + Wildcards are permitted. + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Include, + + <# + Excludes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + + Wildcards are permitted. + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Exclude, + + <# + Includes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $IncludeContentType, + + <# + Excludes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $ExcludeContentType, + + [switch] + $Force + ) + + begin { + $memoryStream = [IO.MemoryStream]::new() + $combinedPackage = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') + $copySplat = [Ordered]@{Destination=$combinedPackage} + $copyOpenPackage = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Copy-OpenPackage','Function') + foreach ($key in $psBoundParameters.Keys) { + if ($copyOpenPackage.Parameters[$key]) { + $copySplat[$key] = $PSBoundParameters[$key] + } + } + + $allIdentifiers = @() + } + + process { + if ($InputObject -isnot [IO.Packaging.Package]) { + return $InputObject + } + $allIdentifiers += $InputObject.Identifier + + $combinedPackage = $InputObject | Copy-OpenPackage @copySplat + } + + end { + $allUniqueIdentifiers = @($allIdentifiers | Select-Object -Unique) + + $combinedPackage.Identifier = $combinedPackage.Identifier -replace '\.[^\.]+?$' + + $combinedPackage + } +} \ No newline at end of file From 5d6de1b30382fd2502dde067e25655b6ee1dc32d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 6 Nov 2025 18:27:43 -0800 Subject: [PATCH 072/724] feat: `Join-OpenPackage ( Fixes #48 ) --- Commands/Join-OpenPackage.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Commands/Join-OpenPackage.ps1 b/Commands/Join-OpenPackage.ps1 index cc63dc0..6f61397 100644 --- a/Commands/Join-OpenPackage.ps1 +++ b/Commands/Join-OpenPackage.ps1 @@ -4,9 +4,9 @@ function Join-OpenPackage .SYNOPSIS Joins Open Packages .DESCRIPTION - Joins multiple open packages into a single open package - + Joins multiple open packages into a single open package #> + [Alias('Join-OP','jop')] [CmdletBinding(PositionalBinding=$false)] param( [Parameter(ValueFromPipeline)] @@ -88,7 +88,7 @@ function Join-OpenPackage $allUniqueIdentifiers = @($allIdentifiers | Select-Object -Unique) $combinedPackage.Identifier = $combinedPackage.Identifier -replace '\.[^\.]+?$' - + $combinedPackage } } \ No newline at end of file From 422ffe75c70333172b756f84a38b526aebf824ae Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 6 Nov 2025 18:33:53 -0800 Subject: [PATCH 073/724] feat: `Get-OpenPackage` dictionary support ( Fixes #41 ) --- Commands/Get-OpenPackage.ps1 | 117 +++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 6 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 1db7a10..e12b625 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -95,6 +95,10 @@ function Get-OpenPackage [string] $AtUri, + [Parameter(Mandatory,ParameterSetName='Dictionary',ValueFromPipelineByPropertyName)] + [Collections.IDictionary] + $Dictionary, + # A Nuget Uri to package. # The package at this location will be downloaded and opened directly. [Parameter(Mandatory,ParameterSetName='NugetPackage',ValueFromPipelineByPropertyName)] @@ -137,13 +141,18 @@ function Get-OpenPackage $Force ) - begin { - # Begin by declaring several variables we will keep coming back to + begin { + # First, set output encoding to UTF8 + # This should ensure things work consistently regardless of operating system and user preference. + $OutputEncoding = [Text.Encoding]::UTF8 + # Next we want to determine a local app data path $myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage - + + # And we want to keep track of our command name, so we can semi-anonymously recurse. $myCommandName = $MyInvocation.MyCommand.Name - # and initializing our default type map + # Next up we initialize a type map. + # This maps extensions and uris to a content type, and is used when creating parts. $typeMap = ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap # The full default type map is much more robust. @@ -459,6 +468,89 @@ function Get-OpenPackage $currentPackage } + filter packDictionary { + $dictionary = $_ + # If the input was not a dictionary, return + if ($dictionary -isnot [Collections.IDictionary]) { return } + $baseKey = $args -join '/' -replace '^/?', '/' -replace '/?$', '/' + if (-not $currentPackage) { + $currentPackage = getCurrentPack + } + :nextPart foreach ($key in $dictionary.Keys) { + $value = $dictionary[$key] + $partUri = ($baseKey + $key) -replace '//', '/' + if ($value -is [Collections.IDictionary]) { + $currentPackage = $value | + & $MyInvocation.MyCommand.ScriptBlock ( + $partUri + ) + } else { + $newPart = if ($currentPackage.PartExists -and + $currentPackage.PartExists($partUri) + ) { + if (-not $Force) { + Write-Warning "$PartUri already exists, use -Force to overwrite" + continue nextPart + } + $existingPart = $currentPackage.GetPart($partUri) + $partContentType = $existingPart.ContentType + $currentPackage.DeletePart($partUri) + $currentPackage.CreatePart($partUri, $partContentType, 'Normal') + } else { + $partContentType = + if ($partUri -match '\.[^\.]+?$' -and $TypeMap -and + $TypeMap[$matches.0]) { + $TypeMap[$matches.0] + } else { + if ($value -as [byte[]]) { + "application/octet-stream" + } else { + "text/plain" + } + } + $currentPackage.CreatePart($partUri, $partContentType, 'Normal') + } + + if (-not $newPart) { + continue nextPart + } + + $newStream = $newPart.GetStream() + + if ($value -is [xml]) { + $value.Save($newStream) + } + elseif ($partUri -match '\.json$') { + if ($value -isnot [string] -and $value -notmatch '[^\[\{]') { + $json = $value | ConvertTo-Json -Depth $FormatEnumerationLimit + $buffer = $OutputEncoding.GetBytes("$json") + $newStream.write($buffer, 0, $buffer.Length) + } else { + $value.Save($newStream) + } + } + elseif ($partUri -match '\.clixml$') { + $clixml = [Management.Automation.PSSerializer]::Serialize($value) + $buffer = $OutputEncoding.GetBytes($clixml) + $newStream.write($buffer, 0, $buffer.Length) + } + else { + if ($value -is [byte[]]) { + $newStream.write($value, 0, $value.Length) + } + else { + $buffer = $OutputEncoding.GetBytes("$value") + $newStream.write($buffer, 0, $buffer.Length) + } + } + + $newStream.Close() + $newStream.Dispose() + } + } + $currentPackage + } + filter packDir { # We want a package from a directory $resolvedItem = $_ @@ -524,7 +616,7 @@ function Get-OpenPackage # Go over each file we want to archive :packingFiles foreach ($file in $filteredFiles) { # get each file as a relative uri, and then get it's bytes - $relativeUri = $file.FullName.Substring($resolvedItem.FullName.Length) + $relativeUri = $file.FullName.Substring($resolvedItem.FullName.Length) -replace '[\\/]', '/' $fileBytes = Get-Content -AsByteStream -Raw -LiteralPath $file.FullName # If the file was blank @@ -1086,6 +1178,9 @@ function Get-OpenPackage & $myCommandName -FilePath $InputObject.FullName @namedParameters return } + {$_ -is [Collections.IDictionary]} { + & $myCommandName -Dictionary $InputObject @namedParameters + } {$_ -is [uri]} { & $myCommandName -Uri $InputObject @namedParameters } @@ -1106,10 +1201,15 @@ function Get-OpenPackage $arg = $ArgumentList[$argNumber] # If it is a string, path info, or uri if ($arg -is [Management.Automation.PSModuleInfo]) { - & $myCommandName -Module $arg @namedParameters + $packages += & $myCommandName -Module $arg @namedParameters # and continue to the next argument. continue nextArgument } + + if ($arg -is [Collections.IDictionary]) { + $packages += & $myCommandName -Dictionary $arg @namedParameters + continue nextArgument + } if ($arg -is [string] -or $arg -is [Management.Automation.PathInfo] -or @@ -1288,6 +1388,11 @@ function Get-OpenPackage } } + if ($Dictionary) { + $dictionary | packDictionary + return + } + #region Open Package from Nuget if ($NuGet) { $downloadsPath = Join-Path $myAppData 'Downloads' From fa4bf949615bdd96a05ec327cf09a6c7d919a3e3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 7 Nov 2025 11:00:52 -0800 Subject: [PATCH 074/724] feat: `OpenPackage.get_Dockerfile` ( Fixes #35 ) --- Types/OpenPackage/get_Dockerfile.ps1 | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Types/OpenPackage/get_Dockerfile.ps1 diff --git a/Types/OpenPackage/get_Dockerfile.ps1 b/Types/OpenPackage/get_Dockerfile.ps1 new file mode 100644 index 0000000..07d179b --- /dev/null +++ b/Types/OpenPackage/get_Dockerfile.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS + Gets a package's dockerfile +.DESCRIPTION + Gets the content of any `Dockerfile`s in the package. +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named Dockerfile + if ($part.Uri -notmatch '[/\.]DockerFile$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Clean up + $streamReader.Close() + $streamReader.Dispose() + $partStream.Close() + $partStream.Dispose() + + # output what we read. + $readStream +} \ No newline at end of file From 657d4083fc50cba5682bb8e54683e799d4ef6f9e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 7 Nov 2025 19:01:16 +0000 Subject: [PATCH 075/724] feat: `OpenPackage.get_Dockerfile` ( Fixes #35 ) --- OP.types.ps1xml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f95540e..082e1d8 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -253,6 +253,40 @@ $this.PackageProperties.Creator = $Creator $this.PackageProperties.Description = $args -join [Environment]::NewLine + + Dockerfile + + <# +.SYNOPSIS + Gets a package's dockerfile +.DESCRIPTION + Gets the content of any `Dockerfile`s in the package. +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named Dockerfile + if ($part.Uri -notmatch '[/\.]DockerFile$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Clean up + $streamReader.Close() + $streamReader.Dispose() + $partStream.Close() + $partStream.Dispose() + + # output what we read. + $readStream +} + + FileList From ebc5fa2f806b15609f8d90b58e6bd89f55afd243 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 7 Nov 2025 11:09:41 -0800 Subject: [PATCH 076/724] feat: `OpenPackage.get_TypeScriptConfig` ( Fixes #49 ) --- Types/OpenPackage/get_TypeScriptConfig.ps1 | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Types/OpenPackage/get_TypeScriptConfig.ps1 diff --git a/Types/OpenPackage/get_TypeScriptConfig.ps1 b/Types/OpenPackage/get_TypeScriptConfig.ps1 new file mode 100644 index 0000000..08f6dd9 --- /dev/null +++ b/Types/OpenPackage/get_TypeScriptConfig.ps1 @@ -0,0 +1,34 @@ +<# +.SYNOPSIS + Gets a package's `tsconfig.json` +.DESCRIPTION + Gets the content of any TypeScript configuration `tsconfig.json` files in the package +#> +[OutputType([psobject])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/tsconfig\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. \ No newline at end of file From f57ee7e79c7352f243b4ac135f5d47dc7207ee97 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 7 Nov 2025 19:09:59 +0000 Subject: [PATCH 077/724] feat: `OpenPackage.get_TypeScriptConfig` ( Fixes #49 ) --- OP.types.ps1xml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 082e1d8..d83bdb3 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -807,6 +807,45 @@ param([string]$Title) $this.PackageProperties.Title = $Title + + TypeScriptConfig + + <# +.SYNOPSIS + Gets a package's `tsconfig.json` +.DESCRIPTION + Gets the content of any TypeScript configuration `tsconfig.json` files in the package +#> +[OutputType([psobject])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/tsconfig\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. + + Version From 89d2297a493452928a1a25d3ba2670481933f7e8 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 7 Nov 2025 12:03:12 -0800 Subject: [PATCH 078/724] feat: `OpenPackage.get_Nix` ( Fixes #50 ) --- Types/OpenPackage/get_Nix.ps1 | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Types/OpenPackage/get_Nix.ps1 diff --git a/Types/OpenPackage/get_Nix.ps1 b/Types/OpenPackage/get_Nix.ps1 new file mode 100644 index 0000000..e4b0ed0 --- /dev/null +++ b/Types/OpenPackage/get_Nix.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS + Gets a package's `*.nix` files +.DESCRIPTION + Gets the content of any `*.nix` files in the package. +#> +[OutputType([string])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '\.nix$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru | + Add-Member NoteProperty Package $this -force -PassThru +} + +# We are done. \ No newline at end of file From 4769b6114305ebe02fe885f246339415153f9985 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 7 Nov 2025 20:03:36 +0000 Subject: [PATCH 079/724] feat: `OpenPackage.get_Nix` ( Fixes #50 ) --- OP.types.ps1xml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d83bdb3..9737915 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -497,6 +497,46 @@ param([DateTime]$Modified) $this.PackageProperties.Modifiedd = $Modified + + Nix + + <# +.SYNOPSIS + Gets a package's `*.nix` files +.DESCRIPTION + Gets the content of any `*.nix` files in the package. +#> +[OutputType([string])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '\.nix$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru | + Add-Member NoteProperty Package $this -force -PassThru +} + +# We are done. + + Nuspec From d41a6996988fa858e21f7954c9e52955bd1d5250 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 7 Nov 2025 12:12:02 -0800 Subject: [PATCH 080/724] feat: `OpenPackage.get_FileContentType` ( Fixes #51 ) --- Types/OpenPackage/get_FileContentType.ps1 | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Types/OpenPackage/get_FileContentType.ps1 diff --git a/Types/OpenPackage/get_FileContentType.ps1 b/Types/OpenPackage/get_FileContentType.ps1 new file mode 100644 index 0000000..2167528 --- /dev/null +++ b/Types/OpenPackage/get_FileContentType.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets file content types +.DESCRIPTION + Gets a table of all files in the package and their associated content types. +#> +$fileContentTypes = [Ordered]@{} +foreach ($part in @($this.GetParts())) { + $fileContentTypes[$part.Uri] = $part.ContentType +} +$fileContentTypes From a084510a54753760401686d1de5c627ea3c88003 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 7 Nov 2025 20:12:22 +0000 Subject: [PATCH 081/724] feat: `OpenPackage.get_FileContentType` ( Fixes #51 ) --- OP.types.ps1xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9737915..b978bfd 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -287,6 +287,23 @@ foreach ($part in $this.GetParts()) { } + + FileContentType + + <# +.SYNOPSIS + Gets file content types +.DESCRIPTION + Gets a table of all files in the package and their associated content types. +#> +$fileContentTypes = [Ordered]@{} +foreach ($part in @($this.GetParts())) { + $fileContentTypes[$part.Uri] = $part.ContentType +} +$fileContentTypes + + + FileList From 828f9309d3329f3b9174cc13a929397695671461 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 7 Nov 2025 12:16:37 -0800 Subject: [PATCH 082/724] feat: `Split-OpenPackage ( Fixes #47 ) Adding help --- Commands/Split-OpenPackage.ps1 | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Commands/Split-OpenPackage.ps1 b/Commands/Split-OpenPackage.ps1 index 7a5d057..b79a8f5 100644 --- a/Commands/Split-OpenPackage.ps1 +++ b/Commands/Split-OpenPackage.ps1 @@ -1,10 +1,21 @@ function Split-OpenPackage { <# .SYNOPSIS - + Splits Open Packages + .DESCRIPTION + Splits Open Packages into multiple parts + .EXAMPLE + Get-Module OP | + OP | + Split-OpenPackage + .LINK + Group-Object + .LINK + Select-OpenPackage #> [CmdletBinding(PositionalBinding=$false)] param( + # One or more properties to group. [Parameter(ValueFromRemainingArguments)] [PSObject[]] $Property, @@ -78,7 +89,8 @@ function Split-OpenPackage { $Property = { @(@($_.Uri -split '/' -ne '')[-1] -split '\.')[-1] } } foreach ($group in $selectedParts | Group-Object -Property $Property) { - + # If the group has no name, it will be excluded + if (-not $group.Name) { continue } $splitPackage = $inputObject | Copy-OpenPackage -Include $group.Group.Uri From 81d5c107c127e2d9f8b0d766a6c6960c8b661ef7 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 9 Nov 2025 12:51:57 -0800 Subject: [PATCH 083/724] feat: `Copy-OpenPackage` ( Fixes #7 ) Parameter aliasing --- Commands/Copy-OpenPackage.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Commands/Copy-OpenPackage.ps1 b/Commands/Copy-OpenPackage.ps1 index 0407d10..c350129 100644 --- a/Commands/Copy-OpenPackage.ps1 +++ b/Commands/Copy-OpenPackage.ps1 @@ -13,6 +13,7 @@ function Copy-OpenPackage # The destination. # If this is not a `[IO.Packaging.Package]`, it will be considered a file path. [Parameter(ValueFromPipelineByPropertyName)] + [Alias('DestinationPath')] [PSObject] $Destination, From fea366216c0aaba376c42fe71b1c8e5eda1c1ef9 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 12 Nov 2025 22:54:19 -0800 Subject: [PATCH 084/724] feat: `Get-OpenPackage -CompressionOption` ( Fixes #59 ) --- Commands/Get-OpenPackage.ps1 | 69 +++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index e12b625..096828c 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -4,11 +4,40 @@ function Get-OpenPackage .SYNOPSIS Gets Open Packages .DESCRIPTION - Gets Open Packages from a path, uri, or repository. Runs related open package commands. + Gets Open Packages from a path, uri, or repository and runs related commands. - Anything can be a package - .NOTES + Anything can be a package. + + This command helps you make anything into a package. + + The following types of packages are currently supported: + + * Any [Open Packaging Convention](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) files + * Any directory + * Any `*.zip` file + * Any *.tar.gz file + * Any url + * Any git repository + * Any public at protocol URI + * Any dictionary (including nested dictionaries) + * Any single file + + Anything can be a package. + Once we start to treat anything as a package, we can do amazing things with packages. + + Like: + + * Inspect any packages before we work with them. + * Modify the packages to customize their content. + * Split packages + * Filter our components. + * Join them back together. + * Search package content. + * Work with compressed trees of data. + * Have an in-memory containerized virtual filesystem. + * Serve a package from memory. + * Store data to N package layers. .EXAMPLE # Make the current directory into a package # (do not try this at `$home`) @@ -42,8 +71,6 @@ function Get-OpenPackage .EXAMPLE # Get the most recent 50 posts $atLast50 = op at://mrpowershell.com/app.bsky.feed.post/ -First 50 - .EXAMPLE - #> [CmdletBinding(PositionalBinding=$false,DefaultParameterSetName='Any',SupportsPaging)] [Alias('Get-OP', 'OP', 'OpenPackage')] @@ -92,8 +119,8 @@ function Get-OpenPackage # An At Uri to package. # This can be a single post or a collection of all posts of a type. [Parameter(Mandatory,ParameterSetName='AtUri',ValueFromPipelineByPropertyName)] - [string] - $AtUri, + [string[]] + $AtUri, [Parameter(Mandatory,ParameterSetName='Dictionary',ValueFromPipelineByPropertyName)] [Collections.IDictionary] @@ -116,11 +143,13 @@ function Get-OpenPackage # A list of file wildcards to include. [Parameter(ValueFromPipelineByPropertyName)] + [SupportsWildcards()] [string[]] $Include, # A list of file wildcards to exclude. [Parameter(ValueFromPipelineByPropertyName)] + [SupportsWildcards()] [string[]] $Exclude, @@ -130,6 +159,11 @@ function Get-OpenPackage $TypeMap = $( ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap ), + + # The compression option. + [IO.Packaging.CompressionOption] + [Alias('CompressionLevel')] + $CompressionOption = 'Superfast', # One or more input objects. [Parameter(ValueFromPipeline)] @@ -272,6 +306,7 @@ function Get-OpenPackage Write-Warning "$($thisPart.Uri) was not clixml" } } + if ($null -ne $partAsXml) { return $partAsXml } @@ -369,7 +404,7 @@ function Get-OpenPackage if ($currentPackage.PartExists($currentPackageUri)) { $currentPackage.DeletePart($currentPackageUri) # recreate it. } - $atPart = $currentPackage.CreatePart($currentPackageUri, 'application/json', 'Normal') + $atPart = $currentPackage.CreatePart($currentPackageUri, 'application/json', $CompressionOption) # Get the stream. $atStream = $atPart.GetStream() @@ -437,15 +472,15 @@ function Get-OpenPackage # Create or recreate the part for the at content $atPart = if ($currentPackage.PartExists($currentPackageUri)) { $currentPackage.DeletePart($currentPackageUri) - $currentPackage.CreatePart($currentPackageUri, 'application/json', 'Normal') + $currentPackage.CreatePart($currentPackageUri, 'application/json', $CompressionOption) } else { - $currentPackage.CreatePart($currentPackageUri, 'application/json', 'Normal') + $currentPackage.CreatePart($currentPackageUri, 'application/json', $CompressionOption) } # Store the json $atStream = $atPart.GetStream() $atJsonBytes = $outputEncoding.GetBytes( - ($atRecord | ConvertTo-Json -Depth 100) + ($record | ConvertTo-Json -Depth 100) ) $atStream.Write($atJsonBytes, 0, $atJsonBytes.Count) # and clean up @@ -495,7 +530,7 @@ function Get-OpenPackage $existingPart = $currentPackage.GetPart($partUri) $partContentType = $existingPart.ContentType $currentPackage.DeletePart($partUri) - $currentPackage.CreatePart($partUri, $partContentType, 'Normal') + $currentPackage.CreatePart($partUri, $partContentType, $CompressionOption) } else { $partContentType = if ($partUri -match '\.[^\.]+?$' -and $TypeMap -and @@ -508,7 +543,7 @@ function Get-OpenPackage "text/plain" } } - $currentPackage.CreatePart($partUri, $partContentType, 'Normal') + $currentPackage.CreatePart($partUri, $partContentType, $CompressionOption) } if (-not $newPart) { @@ -656,7 +691,7 @@ function Get-OpenPackage # Try to create a new part try { - $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, 'Normal') + $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, $CompressionOption) } catch { # If that didn't work, $ex = $_ @@ -750,7 +785,7 @@ function Get-OpenPackage $fileContentType = $typeMap[$resolvedItem.Extension] # and create a part - $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, 'Normal') + $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, $CompressionOption) if (-not $newPart) { continue } @@ -1060,7 +1095,7 @@ function Get-OpenPackage $uri.LocalPath } # Now that we know where we're putting it, let's create a part - $newPart = $currentPackage.CreatePart($localPath, $WebResponse.Headers['content-type'], 'Normal') + $newPart = $currentPackage.CreatePart($localPath, $WebResponse.Headers['content-type'], $CompressionOption) # and get the content stream $newStream = $newPart.GetStream() @@ -1429,7 +1464,7 @@ function Get-OpenPackage } return } - #endregion Open Package from Nuget + #endregion Open Package from Nuget if ($filePath) { # Try to resolve the file path From 13ba9dd3435730d015964544c5bd7eed6d32b9a8 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 13 Nov 2025 13:07:48 -0800 Subject: [PATCH 085/724] feat: `Expand-OpenPackage` ( Fixes #52 ) --- Commands/Expand-OpenPackage.ps1 | 162 ++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 Commands/Expand-OpenPackage.ps1 diff --git a/Commands/Expand-OpenPackage.ps1 b/Commands/Expand-OpenPackage.ps1 new file mode 100644 index 0000000..0c37a7f --- /dev/null +++ b/Commands/Expand-OpenPackage.ps1 @@ -0,0 +1,162 @@ +function Expand-OpenPackage +{ + <# + .SYNOPSIS + Expands an OpenPackage + .DESCRIPTION + Expands an OpenPackage into a destination on disk + .LINK + Expand-Archive + #> + [CmdletBinding(PositionalBinding=$false)] + param( + # The destination path. This should be a directory. + [Parameter(Position=0)] + [ValidateNotNullOrEmpty()] + [string] + $DestinationPath, + + <# + Includes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Include, + + <# + Excludes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Exclude, + + <# + Includes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $IncludeContentType, + + <# + Excludes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $ExcludeContentType, + + # The input object. If this is not a package, it will be passed thru. + [Parameter(ValueFromPipeline)] + [Alias('Package')] + [PSObject] + $InputObject, + + # If set, will overwrite existing files. + [switch] + $Force, + + # If set, will output the files that are expanded from the package. + [switch] + $PassThru + ) + + begin { + # We will be using Select-OpenPackage for filtering, so get a reference to it now. + $selectOpenPackage = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-OpenPackage','Function') + } + + process { + # Pass thru any input that is not a package. + if ($InputObject -isnot [IO.Packaging.Package]) { return $InputObject } + + + + # Copy our parameters to Select-OpenPackage + $selectSplat = [Ordered]@{InputObject=$InputObject} + foreach ($key in $PSBoundParameters.Keys) { + if ($selectOpenPackage.Parameters[$key]) { + $selectSplat[$key] = $PSBoundParameters[$key] + } + } + + # Get all of the package parts + $inputParts = @(Select-OpenPackage @selectSplat) + + # and prepare our progress. + $total = $inputParts.Length + $counter = 0 + $Progress = [Ordered]@{ + Activity = "Expanding $($InputObject.PackageProperties.Identifier)" + Id = Get-Random + } + + + # Go over each part + :nextPart foreach ($part in $inputParts) { + # Find their destination on disk + $partDestination = Join-Path $DestinationPath ([Web.HttpUtility]::UrlDecode($part.Uri)) + $counter++ + $Progress.Status = $part.Uri + $Progress.PercentComplete = $counter * 100 / $total + # and write progress. + Write-Progress @Progress + # Then check if it exists + $fileInfo = + if ((Test-Path -LiteralPath $partDestination)) { + if (-not $Force) { + # and warn if they are not -Forcing the point + Write-Warning "$PartDestination exists, use -Force to overwrite" + continue nextPart + } + Get-Item -LiteralPath $partDestination + } else { + # create a file if it did not exist. + New-Item -ItemType File -Path $partDestination -Force + } + + # If we do not have a file, + if ($fileInfo -isnot [IO.FileInfo]) { + # continue to the next part + continue nextPart + } + + # Open the file for write + $fileStream = $fileInfo.OpenWrite() + # and continue if that did not work for any reason (for example, the file being locked) + if (-not $?) { continue nextPart } + # Get the part stream + $partStream = $part.GetStream() + # copy it to the file + $partStream.CopyTo($fileStream) + # and close and dispose of them both + $fileStream.Close() + $fileStream.Dispose() + + $partStream.Close() + $partStream.Dispose() + + # If we are passing thru + if ($PassThru) { + # get the exported files. + Get-Item -LiteralPath $fileInfo.FullName + } + } + + # After we have expanded all of the parts + $Progress.Remove('PercentComplete') + $Progress.Completed = $true + # complete our progress + Write-Progress @Progress + } +} \ No newline at end of file From bab497b97427fcb1c96bca0c4747678acd9e6326 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 13 Nov 2025 16:29:35 -0800 Subject: [PATCH 086/724] feat: `Export/Expand-OpenPackage` ( Fixes #39, Fixes #52 ) Exporting by expanding if the target is a directory --- Commands/Expand-OpenPackage.ps1 | 5 ++- Commands/Export-OpenPackage.ps1 | 59 ++++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/Commands/Expand-OpenPackage.ps1 b/Commands/Expand-OpenPackage.ps1 index 0c37a7f..319891b 100644 --- a/Commands/Expand-OpenPackage.ps1 +++ b/Commands/Expand-OpenPackage.ps1 @@ -149,7 +149,10 @@ function Expand-OpenPackage # If we are passing thru if ($PassThru) { # get the exported files. - Get-Item -LiteralPath $fileInfo.FullName + Get-Item -LiteralPath $fileInfo.FullName | + Add-Member NoteProperty Package $InputObject -Force -PassThru | + Add-Member NoteProperty PartUri $part.Uri -Force -PassThru | + Add-Member NoteProperty PartContentType $part.ContentType -Force -PassThru } } diff --git a/Commands/Export-OpenPackage.ps1 b/Commands/Export-OpenPackage.ps1 index bbcffd8..09dcb4e 100644 --- a/Commands/Export-OpenPackage.ps1 +++ b/Commands/Export-OpenPackage.ps1 @@ -3,19 +3,51 @@ function Export-OpenPackage { .SYNOPSIS Exports OpenPackage packages .DESCRIPTION - Exports loaded packages to a file. + Exports loaded packages to a file or directory. #> [Alias('Save-OpenPackage','epop','svop')] param( - # The file path to save the turtle graphics pattern. + # The package file path. + # If this has no extension, it will be considered a directory name, and [Parameter(Mandatory,ValueFromPipelineByPropertyName)] - [Alias('Path')] + [Alias('Path','FilePath','Fullname')] [string] - $FilePath, + $DestinationPath, + + <# + Includes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Include, + + <# + Excludes the specified parts. + + Enter a wildcard pattern, such as `*.txt` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $Exclude, + + <# + Includes the specified content types. + + Enter a wildcard pattern, such as `text/*` + #> + [ValidateNotNullOrEmpty()] + [SupportsWildcards()] + [string[]] + $IncludeContentType, # The input object. # This must be a package loaded with this module. [Parameter(ValueFromPipeline)] + [Alias('Package')] [PSObject] $InputObject, @@ -28,11 +60,20 @@ function Export-OpenPackage { # If there is no input return if (-not $InputObject) { return } # If the input is not a package, pass it thru - if ($InputObject -isnot [IO.Packaging.Package]) { - return $InputObject - } - - Copy-OpenPackage -DestinationPath $FilePath -InputObject $inputObject -force:$Force + if ($InputObject -isnot [IO.Packaging.Package]) { return $InputObject } + + # Get the item if it already exists + $destinationItem = Get-Item $DestinationPath -ErrorAction Ignore + + if ($destinationItem -is [IO.DirectoryInfo] -or ($DestinationPath -notlike '*.*')) + { + Expand-OpenPackage @PSBoundParameters -PassThru + } else { + $copiedPackage = Copy-OpenPackage @PSBoundParameters + $outputFile = Get-Item $DestinationPath + $outputFile | + Add-Member NoteProperty Package $copiedPackage -Force -PassThru + } } } From 91d93d3a661552b0ba441cb060e46a8f1b2aabea Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 11:17:34 -0800 Subject: [PATCH 087/724] feat: 'OpenPackage.GetContent' ( Fixes #57 ) --- Types/OpenPackage/GetContent.ps1 | 216 +++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 Types/OpenPackage/GetContent.ps1 diff --git a/Types/OpenPackage/GetContent.ps1 b/Types/OpenPackage/GetContent.ps1 new file mode 100644 index 0000000..a8e0e9f --- /dev/null +++ b/Types/OpenPackage/GetContent.ps1 @@ -0,0 +1,216 @@ +<# +.SYNOPSIS + Gets part content +.DESCRIPTION + Gets the content of one or more parts. + + Will attempt to get the content in the best type available. + + * If the content is url-encoded, will get the content as an `[Ordered]` dictionary. + * If the content is XML, it will be returned as XML. + * If the content is CliXml, it will be deserialized into PSObjects + * If the content type or part ends with `.json`, it will converted from json. + * If the content type or part ends with `.yaml`, it will converted from yaml. + * If the content type or part ends with `.toml`, it will converted from toml. + * If the content type is PowerShell data, will run the block in a data guard. + * If the content type is PowerShell or ends with *.psm?1, it will be turned into a [ScriptBlock] + * If the + + Otherwise, if the content type is text, will get content as text +.NOTES + Yaml support requires the installation of an appropriate ConvertFrom-Yaml command + + [YaYaml](https://github.com/jborean93/PowerShell-Yayaml/) is recommended. + + Toml support requires the installation of an appropriate ConvertFrom-Toml command + + [PSToml](https://github.com/jborean93/PSToml) is recommended. + + +#> +param() + +$unrolledArgs = @(foreach ($arg in $args) { + $arg +}) + +filter addPackageAndPart { + $_ | + Add-Member NoteProperty Package $this -Force -PassThru | + Add-Member NoteProperty PartUri $thisPart.Uri -Force -PassThru +} + +$myParts = @($this.GetParts()) + +$matchingParts = + foreach ($arg in $unrolledArgs) { + if ($arg -is [string]) { + $SlashPart = '/' + ($arg -replace '^/') + if ($this.PartExists($SlashPart)) { + $this.GetPart($SlashPart) + } elseif ($arg -match '\*') { + @($myParts.Uri) -like $arg + } + } + } + +:nextPart foreach ($part in $matchingParts) { + $thisPart = $part + + $partStream = $thisPart.GetStream() + + $memoryStream = [IO.MemoryStream]::new() + $partStream.CopyTo($memoryStream) + $partStream.Close() + $partStream.Dispose() + + [byte[]]$partBytes = $memoryStream.ToArray() + + $memoryStream.Close() + $memoryStream.Dispose() + + $byteStream = [IO.MemoryStream]::new($partBytes) + $partStreamReader = [IO.StreamReader]::new($byteStream) + $partString = $partStreamReader.ReadToEnd() + $partStreamReader.Close() + $partStreamReader.Dispose() + $byteStream.Close() + $byteStream.Dispose() + + + if ($thisPart.ContentType -eq 'application/x-www-form-urlencoded') { + $formData = [Web.HttpUtility]::ParseQueryString($partString) + $formDataObject = [Ordered]@{} + foreach ($key in $formData.Keys) { + if ($null -eq $formDataObject[$key]) { + $formDataObject[$key] = $formData[$key] + } else { + $formDataObject[$key] = @($formDataObject[$key]) + $formData[$key] + } + } + $formDataObject + continue nextPart + } + + # We will only try to upcast text in ways that are relatively quick. + + # We will not care what the original content type was, only if the cast succeeeds. + + $partAsXml = $partString -as [xml] + if ($null -ne $partAsXml) { + if ($partASXml.Objs) { + try { + [Management.Automation.PSSerializer]::Deserialize($partString) | + addPackageAndPart + continue nextPart + } catch { + Write-Warning "$($thisPart.Uri) was not clixml" + } + } else { + $partAsXml | addPackageAndPart + } + + continue nextPart + } + + # If the content type is json or the uri ends with json + if ($thisPart.ContentType -match '[/\+]json$' -or $thisPart.Uri -match '\.jsonc?$') { + # try to convert it from json + $partAsJson = try { + ConvertFrom-Json -InputObject $partString -ErrorAction Ignore + } catch { + Write-Warning "'$($thisPart.Uri)' was not json: $_" + } + + if ($null -ne $partAsJson) { + $partAsJson | addPackageAndPart + continue nextPart + } + } + + if ($thisPart.ContentType -match '[/\+]ya?ml' -or $thisPart.Uri -match '\.ya?ml$') { + $convertFromYamlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') + if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.InputObject) { + Write-Warning "Convert-FromYaml not found, please install YaYaml" + } else { + try { + $partString | & $convertFromYamlCommand -ErrorAction Stop | addPackageAndPart + } catch { + Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" + } + continue nextPart + } + } + + if ($thisPart.ContentType -match '[/\+]toml' -or + $thisPart.Uri -match '\.toml$') { + $convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') + if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { + Write-Warning "Convert-FromToml not found, please install PSToml" + } else { + try { + $partString | & $convertFromTomlCommand -ErrorAction Stop | addPackageAndPart + } catch { + Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" + } + continue nextPart + } + } + + # If the content type is powershell data + if ( + $thisPart.ContentType -in 'text/x-powershell-data', 'application/x-powershell-data' -or + $thisPart.Uri -match '\.psd1$' + ) { + $dataBlockOutput = try { + $scriptBlock = [ScriptBlock]::Create($partString) + $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") + & $dataScriptBlock | addPackageAndPart + } catch { + $null + } + + if ($null -ne $dataBlockOutput) { + $dataBlockOutput + continue nextPart + } + } + + if ( + $thisPart.ContentType -in 'text/x-powershell', 'application/x-powershell' -or + $thisPart.Uri -match '\.ps[md]?1$' + ) { + try { + [ScriptBlock]::Create($partString) | addPackageAndPart + } catch { + Write-Warning "$($thisPart.Uri) is not a valid PowerShell script: $_" + } + continue nextPart + } + + if ($thisPart.Uri -match '\.cs$') { + $null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru + if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { + $partString | + Add-Member NoteProperty SyntaxTree ( + [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) + ) -Force -PassThru | + addPackageAndPart + + + + } else { + $partString | + addPackageAndPart + } + + continue nextPart + } + + if ($thisPart.ContentType -match '^text/') { + $partString | + addPackageAndPart + } else { + $partBytes + } +} \ No newline at end of file From fea604a3f8e5ddefdf386dadbe43199cbd45a515 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 19:17:58 +0000 Subject: [PATCH 088/724] feat: 'OpenPackage.GetContent' ( Fixes #57 ) --- OP.types.ps1xml | 221 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index b978bfd..63339e3 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -15,6 +15,227 @@ + + GetContent + + Category From 25f57cb450114e110f2af8d74f7c6102eadc5d4c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 11:33:30 -0800 Subject: [PATCH 089/724] feat: `Select-OpenPackage -AstCondition` ( Fixes #53 ) --- Commands/Select-OpenPackage.ps1 | 34 ++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Commands/Select-OpenPackage.ps1 b/Commands/Select-OpenPackage.ps1 index d3218b5..646371b 100644 --- a/Commands/Select-OpenPackage.ps1 +++ b/Commands/Select-OpenPackage.ps1 @@ -35,8 +35,6 @@ function Select-OpenPackage [switch] $Quiet, - - # Only the first instance of matching text is returned from each input file. # This is the most efficient way to retrieve a list of files that have contents matching the regular expression. [Parameter(ParameterSetName='Select-String')] @@ -157,6 +155,14 @@ function Select-OpenPackage [Collections.IDictionary] $Namespace, + # One or more Abstract Syntax Tree conditions. + # These will select elements in any PowerShell scripts that match the condition. + [Parameter(ParameterSetName='Ast', ValueFromPipelineByPropertyName)] + [Alias('AstSearch', 'SearchAst')] + [ScriptBlock[]] + $AstCondition, + + # The input object. This should be a package. [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)] [Alias('Package')] [PSObject] @@ -224,7 +230,7 @@ function Select-OpenPackage ) :nextPart foreach ($inputPart in $inputParts) { - if (-not $Pattern -and -not $XPath) { + if (-not $Pattern -and -not $XPath -and -not $AstCondition) { $inputPart continue } @@ -263,10 +269,28 @@ function Select-OpenPackage } } $selectParameters.Remove('Include') - $selectParameters.Remove('Exclude') + $selectParameters.Remove('Exclude') Select-String @selectParameters -InputObject $inputText | - Add-Member NoteProperty Uri $inputPart.Uri -Force -PassThru + Add-Member NoteProperty Uri $inputPart.Uri -Force -PassThru | + Add-Member NoteProperty Package $InputObject -Force -PassThru + } + + $inputAsScript = + if ($inputPart.Uri -match '\.ps[md]?1$') { + try { [scriptblock]::Create($inputText) } + catch { $null } + } else { + '' + } + + + if ($AstCondition -and $inputAsScript.Ast.FindAll) { + foreach ($searchScript in $AstCondition) { + $inputAsScript.Ast.FindAll($searchScript, $true) | + Add-Member NoteProperty Uri $inputPart.Uri -Force -PassThru | + Add-Member NoteProperty Package $InputObject -Force -PassThru + } } } } From f2f1ed7465c6f7a61673af897de20e729c672a6d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 13:21:08 -0800 Subject: [PATCH 090/724] feat: `OpenPackage.get_FileSize` ( Fixes #63 ) --- Types/OpenPackage/get_FileSize.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Types/OpenPackage/get_FileSize.ps1 diff --git a/Types/OpenPackage/get_FileSize.ps1 b/Types/OpenPackage/get_FileSize.ps1 new file mode 100644 index 0000000..078692e --- /dev/null +++ b/Types/OpenPackage/get_FileSize.ps1 @@ -0,0 +1,14 @@ +<# +.SYNOPSIS + Gets file content types +.DESCRIPTION + Gets a table of all files in the package and their associated content types. +#> +$fileLengths = [Ordered]@{} +foreach ($part in @($this.GetParts())) { + $partStream = $part.GetStream() + $fileLengths[$part.Uri] = $partStream.Length + $partStream.Close() + $partStream.Dispose() +} +$fileLengths From 3a7c7f206a583d22d8e3560e1c1b6fbaaba86ebc Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 21:21:29 +0000 Subject: [PATCH 091/724] feat: `OpenPackage.get_FileSize` ( Fixes #63 ) --- OP.types.ps1xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 63339e3..3e033dc 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -542,6 +542,26 @@ param() @($this.GetParts()).Uri -as [string[]] + + FileSize + + <# +.SYNOPSIS + Gets file content types +.DESCRIPTION + Gets a table of all files in the package and their associated content types. +#> +$fileLengths = [Ordered]@{} +foreach ($part in @($this.GetParts())) { + $partStream = $part.GetStream() + $fileLengths[$part.Uri] = $partStream.Length + $partStream.Close() + $partStream.Dispose() +} +$fileLengths + + + Identifier From 9a12f0f5bb08c0370f89aedc51accf61f0c06aff Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 13:40:05 -0800 Subject: [PATCH 092/724] feat: `OpenPackage.get_LanguagePercent` ( Fixes #61 ) --- Types/OpenPackage/get_LanguagePercent.ps1 | 72 +++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 Types/OpenPackage/get_LanguagePercent.ps1 diff --git a/Types/OpenPackage/get_LanguagePercent.ps1 b/Types/OpenPackage/get_LanguagePercent.ps1 new file mode 100644 index 0000000..5f84418 --- /dev/null +++ b/Types/OpenPackage/get_LanguagePercent.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS + Gets the language percentages of a package +.DESCRIPTION + Gets the language percentages present in the package. +#> +$LanguagesByLength = [Ordered]@{} + +$totalLength = 0 +$fileSizes = $this.FileSize +foreach ($part in $this.GetParts()) { + $partLength = $fileSizes[$part.Uri] + + $recognizedLanguage = + switch -regex ($part.Uri) { + '\.c$' { 'C' } + '\.cpp$' { 'C++' } + '\.clixml$' { 'Clixml'} + '\.cs$' { 'C# '} + '\.csv$' { 'CSV' } + '\.csh$' { 'CShell'} + '\.css$' { 'CSS' } + '\.dll$' { 'Binary' } + '\.exe$' { 'Binary' } + '\.h$' { 'C' } + '\.html?$' { 'HTML' } + '\.java$' {'Java' } + '\.json$' {'Json' } + '\.js[mx]$' { 'Javascript'} + '\.(?>md|markdown)$' { 'Markdown' } + '\.pl$' { 'Perl' } + '\.psm?1$' { 'PowerShell' } + '\.psd1$' {'PowerShellData' } + '\.ps1xml$' { 'PowerShellXml' } + '\.py$' { 'Python' } + '\.nix$' { 'Nix' } + '\.rs$' { 'Rust '} + '\.rss$' { 'RSS' } + '\.sh$' { 'BourneShell'} + '\.svg$' { 'SVG' } + '\.ts$' { 'TypeScript'} + '\.tsv$' { 'TSV' } + '\.toml$' { 'TOML' } + '\.xml$' { 'XML' } + '\.ya?ml$' { 'Yaml' } + default { 'Unknown' } + } + + if (-not $recognizedLanguage) { + continue + } + + + if (-not $LanguagesByLength[$recognizedLanguage]) { + $LanguagesByLength[$recognizedLanguage] = 0 + } + + $LanguagesByLength[$recognizedLanguage]+=$partLength + + $totalLength += $partLength +} + + +$SortedByLength = [Ordered]@{} + +foreach ($keyValue in $languagesByLength.GetEnumerator() | + Sort-Object Value -Descending +) { + $SortedByLength[$keyValue.Key] = $keyValue.Value / $totalLength +} + +return $SortedByLength \ No newline at end of file From 596bd94d894247a28f009a7da5ba0e4df39aa3d0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 21:40:25 +0000 Subject: [PATCH 093/724] feat: `OpenPackage.get_LanguagePercent` ( Fixes #61 ) --- OP.types.ps1xml | 77 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 3e033dc..4e416b8 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -629,6 +629,83 @@ param([string]$Language) $this.PackageProperties.Language = $Language + + LanguagePercent + + <# +.SYNOPSIS + Gets the language percentages of a package +.DESCRIPTION + Gets the language percentages present in the package. +#> +$LanguagesByLength = [Ordered]@{} + +$totalLength = 0 +$fileSizes = $this.FileSize +foreach ($part in $this.GetParts()) { + $partLength = $fileSizes[$part.Uri] + + $recognizedLanguage = + switch -regex ($part.Uri) { + '\.c$' { 'C' } + '\.cpp$' { 'C++' } + '\.clixml$' { 'Clixml'} + '\.cs$' { 'C# '} + '\.csv$' { 'CSV' } + '\.csh$' { 'CShell'} + '\.css$' { 'CSS' } + '\.dll$' { 'Binary' } + '\.exe$' { 'Binary' } + '\.h$' { 'C' } + '\.html?$' { 'HTML' } + '\.java$' {'Java' } + '\.json$' {'Json' } + '\.js[mx]$' { 'Javascript'} + '\.(?>md|markdown)$' { 'Markdown' } + '\.pl$' { 'Perl' } + '\.psm?1$' { 'PowerShell' } + '\.psd1$' {'PowerShellData' } + '\.ps1xml$' { 'PowerShellXml' } + '\.py$' { 'Python' } + '\.nix$' { 'Nix' } + '\.rs$' { 'Rust '} + '\.rss$' { 'RSS' } + '\.sh$' { 'BourneShell'} + '\.svg$' { 'SVG' } + '\.ts$' { 'TypeScript'} + '\.tsv$' { 'TSV' } + '\.toml$' { 'TOML' } + '\.xml$' { 'XML' } + '\.ya?ml$' { 'Yaml' } + default { 'Unknown' } + } + + if (-not $recognizedLanguage) { + continue + } + + + if (-not $LanguagesByLength[$recognizedLanguage]) { + $LanguagesByLength[$recognizedLanguage] = 0 + } + + $LanguagesByLength[$recognizedLanguage]+=$partLength + + $totalLength += $partLength +} + + +$SortedByLength = [Ordered]@{} + +foreach ($keyValue in $languagesByLength.GetEnumerator() | + Sort-Object Value -Descending +) { + $SortedByLength[$keyValue.Key] = $keyValue.Value / $totalLength +} + +return $SortedByLength + + LastModifiedBy From 6feed0b6f06ec85cc63a1cb495d25a54f354ab95 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 13:53:31 -0800 Subject: [PATCH 094/724] feat: `OpenPackage.get_LanguagePercent` ( Fixes #61 ) Adding Go --- Types/OpenPackage/get_LanguagePercent.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage/get_LanguagePercent.ps1 b/Types/OpenPackage/get_LanguagePercent.ps1 index 5f84418..062b1e0 100644 --- a/Types/OpenPackage/get_LanguagePercent.ps1 +++ b/Types/OpenPackage/get_LanguagePercent.ps1 @@ -21,7 +21,8 @@ foreach ($part in $this.GetParts()) { '\.csh$' { 'CShell'} '\.css$' { 'CSS' } '\.dll$' { 'Binary' } - '\.exe$' { 'Binary' } + '\.exe$' { 'Binary' } + '\.go$' { 'Go' } '\.h$' { 'C' } '\.html?$' { 'HTML' } '\.java$' {'Java' } From 3692b852e48e9a0e32c71e569fc2bc5244f397f5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 13:54:04 -0800 Subject: [PATCH 095/724] feat: `OpenPackage.get_Lexicon` ( Fixes #64 ) --- Types/OpenPackage/Alias.psd1 | 4 +++ Types/OpenPackage/get_Lexicon.ps1 | 50 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 Types/OpenPackage/Alias.psd1 create mode 100644 Types/OpenPackage/get_Lexicon.ps1 diff --git a/Types/OpenPackage/Alias.psd1 b/Types/OpenPackage/Alias.psd1 new file mode 100644 index 0000000..1d93d2c --- /dev/null +++ b/Types/OpenPackage/Alias.psd1 @@ -0,0 +1,4 @@ +@{ + RemovePart = "DeletePart" + Lexicons = "Lexicon" +} \ No newline at end of file diff --git a/Types/OpenPackage/get_Lexicon.ps1 b/Types/OpenPackage/get_Lexicon.ps1 new file mode 100644 index 0000000..c94bbf1 --- /dev/null +++ b/Types/OpenPackage/get_Lexicon.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Gets any lexicons +.DESCRIPTION + Gets all at protocol lexicons in the package + + A lexicon is defined by the presence of three keys: + + * `id` + * `lexicon` + * `defs + + The output will be a table mapping ids to defs. +#> +[OutputType([Ordered])] +param() + +$allLexicons = [Ordered]@{} + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $fromJson = $readStream | ConvertFrom-Json + if ($fromJson.lexicon -and + $fromJson.id -and + $fromJson.defs + ) { + $allLexicons[$fromJson.id] = $fromJson.defs + } + +} +return $allLexicons +# We are done. \ No newline at end of file From 39b37159ad67c9fd6d47a48d201bea1e69ef1c23 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 14 Nov 2025 21:54:23 +0000 Subject: [PATCH 096/724] feat: `OpenPackage.get_Lexicon` ( Fixes #64 ) --- OP.types.ps1xml | 66 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 4e416b8..995ecd5 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -15,6 +15,14 @@ + + Lexicons + Lexicon + + + RemovePart + DeletePart + GetContent + + SetContent + + Category From 192d1ece5493c2a6df70be6b400a63b2c7defa8e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Feb 2026 22:11:07 -0800 Subject: [PATCH 116/724] feat: OpenPackage.get_TemplateJson ( Fixes #45 ) Using .GetContent --- Types/OpenPackage/get_TemplateJson.ps1 | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Types/OpenPackage/get_TemplateJson.ps1 b/Types/OpenPackage/get_TemplateJson.ps1 index ad2f537..5f5a65a 100644 --- a/Types/OpenPackage/get_TemplateJson.ps1 +++ b/Types/OpenPackage/get_TemplateJson.ps1 @@ -7,7 +7,4 @@ [OutputType([PSObject])] param() -$this.GetContent( - $this.FileList -match '/template\.json$' -) - +$this.GetContent($this.FileList -match '/template\.json$') \ No newline at end of file From 7ad4f9522686c676081429d86d8f42339674b5a5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 06:11:24 +0000 Subject: [PATCH 117/724] feat: OpenPackage.get_TemplateJson ( Fixes #45 ) Using .GetContent --- OP.types.ps1xml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 7bdedcc..d8cafd5 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1213,11 +1213,7 @@ $this.PackageProperties.Subject = $Subject [OutputType([PSObject])] param() -$this.GetContent( - $this.FileList -match '/template\.json$' -) - - +$this.GetContent($this.FileList -match '/template\.json$') From a24e2a3a99993a4ad1988a270195d92e3a59131f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Feb 2026 22:17:12 -0800 Subject: [PATCH 118/724] feat: OpenPackage.get_Nix ( Fixes #50 ) Using .GetContent --- Types/OpenPackage/get_Nix.ps1 | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/Types/OpenPackage/get_Nix.ps1 b/Types/OpenPackage/get_Nix.ps1 index e4b0ed0..3162944 100644 --- a/Types/OpenPackage/get_Nix.ps1 +++ b/Types/OpenPackage/get_Nix.ps1 @@ -7,29 +7,4 @@ [OutputType([string])] param() -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG - if ($part.Uri -notmatch '\.nix$') { continue } - - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru | - Add-Member NoteProperty Package $this -force -PassThru -} - -# We are done. \ No newline at end of file +$this.GetContent(@($this.FileList) -match '\.nix$') From 4b6265cb06cc424c115e2995f72de95cb6ffa033 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 06:17:29 +0000 Subject: [PATCH 119/724] feat: OpenPackage.get_Nix ( Fixes #50 ) Using .GetContent --- OP.types.ps1xml | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d8cafd5..e06dbb0 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1017,32 +1017,8 @@ $this.PackageProperties.Modifiedd = $Modified [OutputType([string])] param() -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG - if ($part.Uri -notmatch '\.nix$') { continue } +$this.GetContent(@($this.FileList) -match '\.nix$') - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru | - Add-Member NoteProperty Package $this -force -PassThru -} - -# We are done. From 2faf7b0f24aaf0996ac6feb28b7587b550a853e9 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Feb 2026 22:21:10 -0800 Subject: [PATCH 120/724] feat: OpenPackage.get_README ( Fixes #81 ) --- Types/OpenPackage/get_README.ps1 | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_README.ps1 diff --git a/Types/OpenPackage/get_README.ps1 b/Types/OpenPackage/get_README.ps1 new file mode 100644 index 0000000..4a5fcdf --- /dev/null +++ b/Types/OpenPackage/get_README.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Gets a package's README +.DESCRIPTION + Gets the content of any parts in the package named README.md +#> +[OutputType("text/markdown")] +param() + +# Get all Readme files +foreach ($content in $this.GetContent( + $this.FileList -match '/README\.md$' +)) { + # decorate them as text/markdown + if ($content.pstypenames -notcontains 'text/markdown') { + $content.pstypenames.insert(0, 'text/markdown') + } + # and return them. + $content +} + +return \ No newline at end of file From 8c9b5fdc12e659f46651d65bcc5ccd8a8430fc05 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 06:21:28 +0000 Subject: [PATCH 121/724] feat: OpenPackage.get_README ( Fixes #81 ) --- OP.types.ps1xml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index e06dbb0..738876a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1119,6 +1119,33 @@ $this.GetContent( ) + + README + + <# +.SYNOPSIS + Gets a package's README +.DESCRIPTION + Gets the content of any parts in the package named README.md +#> +[OutputType("text/markdown")] +param() + +# Get all Readme files +foreach ($content in $this.GetContent( + $this.FileList -match '/README\.md$' +)) { + # decorate them as text/markdown + if ($content.pstypenames -notcontains 'text/markdown') { + $content.pstypenames.insert(0, 'text/markdown') + } + # and return them. + $content +} + +return + + Revision From 5bd23069a0db9c79719cc69b5383e7fd1c519023 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Feb 2026 22:27:58 -0800 Subject: [PATCH 122/724] feat: OpenPackage.get_ImportMap ( Fixes #82 ) --- Types/OpenPackage/get_ImportMap.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Types/OpenPackage/get_ImportMap.ps1 diff --git a/Types/OpenPackage/get_ImportMap.ps1 b/Types/OpenPackage/get_ImportMap.ps1 new file mode 100644 index 0000000..d8eec09 --- /dev/null +++ b/Types/OpenPackage/get_ImportMap.ps1 @@ -0,0 +1,12 @@ +<# +.SYNOPSIS + Gets a package's `importMap.json` +.DESCRIPTION + Gets the content of any `importMap.json` files in the package +#> +[OutputType([psobject])] +param() + + +$this.GetContent(@($this.FileList -match '\importMap\.json$')) + From 2d7ce898345d6d5e4c790d8d362aad1aa0bd7182 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 06:28:25 +0000 Subject: [PATCH 123/724] feat: OpenPackage.get_ImportMap ( Fixes #82 ) --- OP.types.ps1xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 738876a..3c0a44d 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -683,6 +683,24 @@ $fileLengths }) -as [string[]] + + ImportMap + + <# +.SYNOPSIS + Gets a package's `importMap.json` +.DESCRIPTION + Gets the content of any `importMap.json` files in the package +#> +[OutputType([psobject])] +param() + + +$this.GetContent(@($this.FileList -match '\importMap\.json$')) + + + + Keywords From 9ffcd469cf4030469dd882981232aaf168ed3957 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Feb 2026 23:03:57 -0800 Subject: [PATCH 124/724] feat: Read-OpenPackage ( Fixes #67 ) --- Commands/Read-OpenPackage.ps1 | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 Commands/Read-OpenPackage.ps1 diff --git a/Commands/Read-OpenPackage.ps1 b/Commands/Read-OpenPackage.ps1 new file mode 100644 index 0000000..862bc63 --- /dev/null +++ b/Commands/Read-OpenPackage.ps1 @@ -0,0 +1,90 @@ +function Read-OpenPackage +{ + <# + .SYNOPSIS + Reads Open Package Bytes + .DESCRIPTION + Reads Bytes within an Open Package + .EXAMPLE + $package = OP @{"hello.txt" = "Hello world"} + $package | + Read-OpenPackage -Uri /hello.txt + .EXAMPLE + $package = OP @{"hello.txt" = "Hello world"} + ($package | + Read-OpenPackage -Uri /hello.txt -RangeStart 0 -RangeEnd 5) -as 'char[]' + .EXAMPLE + $package = OP @{"hello.txt" = "Hello world"} + ($package | + Read-OpenPackage -Uri /hello.txt -RangeStart 0 -RangeEnd 5) -as 'char[]' -join '' + #> + [OutputType([byte[]])] + param( + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [Alias('PartUri')] + [Uri] + $Uri, + + [Parameter(ValueFromPipelineByPropertyName)] + [Alias('PartStart')] + [long] + $RangeStart, + + [Parameter(ValueFromPipelineByPropertyName)] + [Alias('PartEnd')] + [long] + $RangeEnd, + + [Parameter(ValueFromPipeline)] + [Alias('Package')] + [PSObject] + $InputObject + ) + + process { + if ($InputObject -isnot [IO.Packaging.Package]) { + return $InputObject + } + + $SlashPart = "$uri" -replace '^/?', '/' + + if (-not $InputObject.PartExists($SlashPart)) { + return + } + + $thisPart = $InputObject.GetPart($SlashPart) + if (-not $thisPart) { return } + $partStream = $thisPart.GetStream() + + $memoryStream = [IO.MemoryStream]::new() + + if (-not $RangeStart -and -not $RangeEnd) { + $partStream.CopyTo($memoryStream) + } else { + if ($RangeStart -lt $partStream.Length) { + $null = $partStream.Seek($RangeStart, 'Begin') + if ($rangeEnd -ge $partStream.Length) { + $rangeEnd = $partStream.Length + } + $buffer = [byte[]]::new($RangeEnd - $RangeStart) + $null = $partStream.Read($buffer, 0, $buffer.Length) + $null = $memoryStream.Write($buffer,0 , $buffer.Length) + } + } + + $partStream.Close() + $partStream.Dispose() + + + + + [byte[]]$partBytes = $memoryStream.ToArray() + + $memoryStream.Close() + $memoryStream.Dispose() + + + + return ,$partBytes + } +} \ No newline at end of file From e88a26485a790e9c668eb8473f5e52ab79e89423 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Feb 2026 23:05:29 -0800 Subject: [PATCH 125/724] feat: Write-OpenPackage ( Fixes #68 ) --- Commands/Write-OpenPackage.ps1 | 76 ++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 Commands/Write-OpenPackage.ps1 diff --git a/Commands/Write-OpenPackage.ps1 b/Commands/Write-OpenPackage.ps1 new file mode 100644 index 0000000..b2316bf --- /dev/null +++ b/Commands/Write-OpenPackage.ps1 @@ -0,0 +1,76 @@ +function Write-OpenPackage +{ + <# + .SYNOPSIS + Writes bytes to an Open Package + .DESCRIPTION + Writes bytes directly to an Open Package part. The part must already exist. + + This can be used to rapidly update small segments of a package. + + It can also corrupt package contents, and should be used with care. + + To set package parts, use Set-OpenPackage + .EXAMPLE + $package = OP @{"hello.txt" = "Hello world"} + $package | + Write-OpenPackage -Uri /hello.txt -Buffer ($outputEncoding.GetBytes("y")) | + Get-OpenPackage ./hello.txt + .LINK + Set-OpenPackage + #> + param( + # The Package Part Uri. This is the path to the content within a package. + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [Alias('PartUri')] + [Uri] + $Uri, + + # The byte buffer to write. + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [ValidateNotNullOrEmpty()] + [byte[]] + $Buffer, + + # The starting location for the write. + [Parameter(ValueFromPipelineByPropertyName)] + [Alias('PartStart')] + [long] + $RangeStart = 0, + + # The package. + [Parameter(ValueFromPipeline)] + [Alias('Package')] + [PSObject] + $InputObject + ) + + process { + if ($InputObject -isnot [IO.Packaging.Package]) { + return $InputObject + } + + $SlashPart = "$uri" -replace '^/?', '/' + + if (-not $InputObject.PartExists($SlashPart)) { + return + } + + $thisPart = $InputObject.GetPart($SlashPart) + if (-not $thisPart) { return } + $partStream = $thisPart.GetStream() + + + if ($RangeStart -lt $partStream.Length) { + $null = $partStream.Seek($RangeStart, 'Begin') + } + + $partStream.Write($Buffer, 0, $Buffer.Length) + + + $partStream.Close() + $partStream.Dispose() + + return $InputObject + } +} \ No newline at end of file From e1b0a0cb941321922cd93392ab9b3707747e28bc Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Feb 2026 23:14:36 -0800 Subject: [PATCH 126/724] feat: OpenPackage.GetContent ( Fixes #57 ) Not executing data blocks, adding .astro support --- Types/OpenPackage/GetContent.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage/GetContent.ps1 b/Types/OpenPackage/GetContent.ps1 index 6d21486..7af884c 100644 --- a/Types/OpenPackage/GetContent.ps1 +++ b/Types/OpenPackage/GetContent.ps1 @@ -164,7 +164,7 @@ $matchingParts = $dataBlockOutput = try { $scriptBlock = [ScriptBlock]::Create($partString) $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") - & $dataScriptBlock | addPackageAndPart + $dataScriptBlock | addPackageAndPart } catch { $null } @@ -206,6 +206,12 @@ $matchingParts = continue nextPart } + if ($thisPart.Uri -match './astro$') { + $partString | + addPackageAndPart + continue nextPart + } + if ($thisPart.ContentType -match '^text/') { $partString | addPackageAndPart From ca5cd856946eb3d0174f34d9d835664260fff10b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 07:14:56 +0000 Subject: [PATCH 127/724] feat: OpenPackage.GetContent ( Fixes #57 ) Not executing data blocks, adding .astro support --- OP.types.ps1xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 3c0a44d..65d855e 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -192,7 +192,7 @@ $matchingParts = $dataBlockOutput = try { $scriptBlock = [ScriptBlock]::Create($partString) $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") - & $dataScriptBlock | addPackageAndPart + $dataScriptBlock | addPackageAndPart } catch { $null } @@ -234,6 +234,12 @@ $matchingParts = continue nextPart } + if ($thisPart.Uri -match './astro$') { + $partString | + addPackageAndPart + continue nextPart + } + if ($thisPart.ContentType -match '^text/') { $partString | addPackageAndPart From f9a2dd806820b7aa93065ba199442a718a3fccdd Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Feb 2026 23:24:00 -0800 Subject: [PATCH 128/724] feat: OpenPackage.get_PowerShellManifest ( Fixes #34 ) Double-checking psd1 before extracting data --- Types/OpenPackage/get_PowerShellManifest.ps1 | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage/get_PowerShellManifest.ps1 b/Types/OpenPackage/get_PowerShellManifest.ps1 index 5b5a223..0d13f6c 100644 --- a/Types/OpenPackage/get_PowerShellManifest.ps1 +++ b/Types/OpenPackage/get_PowerShellManifest.ps1 @@ -14,6 +14,21 @@ param() # Get every part foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { - if (-not $psd1.ModuleVersion) { continue } - $psd1 + + # Skip any non-script blocks + if ($psd1 -isnot [ScriptBlock]) { continue } + # and any anything not matching ModuleVersion + if ("$psd1" -notmatch 'ModuleVersion[\s\S]{0,}=') { continue } + # and anything with more than a single statement + if ($psd1.Ast.EndBlock.Statements.Count -ne 1) { continue } + # and anything that is not a data statement + if ($psd1.Ast.EndBlock.Statements[0] -isnot + [Management.Automation.Language.DataStatementAst] -ne 1 + ) { continue } + # and anything with -supportedCommand + if ($psd1.Ast.EndBlock.Statements[0].CommandsAllowed) { + continue + } + # at long last, we can run the script and get the manifest data. + & $psd1 } From 1701eaf9a859d6bd51973b13a4c26ca44cb9a669 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 07:24:18 +0000 Subject: [PATCH 129/724] feat: OpenPackage.get_PowerShellManifest ( Fixes #34 ) Double-checking psd1 before extracting data --- OP.types.ps1xml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 65d855e..f14c925 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1120,8 +1120,23 @@ param() # Get every part foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { - if (-not $psd1.ModuleVersion) { continue } - $psd1 + + # Skip any non-script blocks + if ($psd1 -isnot [ScriptBlock]) { continue } + # and any anything not matching ModuleVersion + if ("$psd1" -notmatch 'ModuleVersion[\s\S]{0,}=') { continue } + # and anything with more than a single statement + if ($psd1.Ast.EndBlock.Statements.Count -ne 1) { continue } + # and anything that is not a data statement + if ($psd1.Ast.EndBlock.Statements[0] -isnot + [Management.Automation.Language.DataStatementAst] -ne 1 + ) { continue } + # and anything with -supportedCommand + if ($psd1.Ast.EndBlock.Statements[0].CommandsAllowed) { + continue + } + # at long last, we can run the script and get the manifest data. + & $psd1 } From f8ace383955c50a4bece0a6ff22357ea38b91d1d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Feb 2026 13:36:35 -0800 Subject: [PATCH 130/724] feat: OpenPackage.Match ( Fixes #83 ) --- Types/OpenPackage/Match.ps1 | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Types/OpenPackage/Match.ps1 diff --git a/Types/OpenPackage/Match.ps1 b/Types/OpenPackage/Match.ps1 new file mode 100644 index 0000000..3fabb63 --- /dev/null +++ b/Types/OpenPackage/Match.ps1 @@ -0,0 +1,46 @@ +<# +.SYNOPSIS + +.DESCRIPTION + +#> +param( +$FilePattern, +$ContentPattern +) + +if ($ContentPattern -and $ContentPattern -isnot [Regex]) { + $ContentPattern = [Regex]::new($ContentPattern,'IgnoreCase', '00:00:05') +} + +if (-not $this.GetParts) { return } + +:nextPart foreach ($part in $this.GetParts()) { + if ($filePattern -and ($part.Uri -notmatch $FilePattern)) { + continue nextPart + } + if (-not $ContentPattern) { + $part + continue nextPart + } + + $partStream = $part.GetStream() + + if (-not $partStream ) { continue } + + $partStreamReader = [IO.StreamReader]::new($partStream) + + $partText = $partStreamReader.ReadToEnd() + + + + $partStreamReader.Close() + $partStreamReader.Dispose() + + $partStream.Close() + $partStream.Dispose() + + $ContentPattern.Matches($partText) | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru | + Add-Member NoteProperty Package $this +} \ No newline at end of file From 1284783c3a535c0319ac5c3ebdccffd4b8442456 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Feb 2026 21:36:56 +0000 Subject: [PATCH 131/724] feat: OpenPackage.Match ( Fixes #83 ) --- OP.types.ps1xml | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f14c925..69d9b7d 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -246,6 +246,57 @@ $matchingParts = } else { $partBytes } +} + + + + Match + From dbd1f35a4c5e1fc56f436b26b7071f8d8af7e991 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Feb 2026 14:08:49 -0800 Subject: [PATCH 132/724] feat: OpenPackage.get_ChocolateyInstall ( Fixes #84 ) --- Types/OpenPackage/get_ChocolateyInstall.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Types/OpenPackage/get_ChocolateyInstall.ps1 diff --git a/Types/OpenPackage/get_ChocolateyInstall.ps1 b/Types/OpenPackage/get_ChocolateyInstall.ps1 new file mode 100644 index 0000000..5d5c232 --- /dev/null +++ b/Types/OpenPackage/get_ChocolateyInstall.ps1 @@ -0,0 +1,10 @@ +<# +.SYNOPSIS + Gets the Chocolatey Install Script +.DESCRIPTION + Gets the Chocolatey Install Script from an OpenPackage, if one is present. + + The Chocolatey install script must be located at /tools/chocolateyInstall.ps1 +#> +param() +$this.GetContent("/tools/chocolateyInstall.ps1") \ No newline at end of file From 2f5cbab6042711f4c4a8a281f216e43b81091e43 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Feb 2026 22:09:33 +0000 Subject: [PATCH 133/724] feat: OpenPackage.get_ChocolateyInstall ( Fixes #84 ) --- OP.types.ps1xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 69d9b7d..9af4532 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -481,6 +481,21 @@ $stream.Dispose() + + ChocolateyInstall + + <# +.SYNOPSIS + Gets the Chocolatey Install Script +.DESCRIPTION + Gets the Chocolatey Install Script from an OpenPackage, if one is present. + + The Chocolatey install script must be located at /tools/chocolateyInstall.ps1 +#> +param() +$this.GetContent("/tools/chocolateyInstall.ps1") + + Config From 7d7a47131953a8c7d8a6a3f71f3642078d71042a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Feb 2026 14:11:06 -0800 Subject: [PATCH 134/724] feat: OpenPackage.get_Astro ( Fixes #85 ) --- Types/OpenPackage/get_Astro.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Types/OpenPackage/get_Astro.ps1 diff --git a/Types/OpenPackage/get_Astro.ps1 b/Types/OpenPackage/get_Astro.ps1 new file mode 100644 index 0000000..4ecdd0d --- /dev/null +++ b/Types/OpenPackage/get_Astro.ps1 @@ -0,0 +1,10 @@ +<# +.SYNOPSIS + Gets Open Package Astro Files +.DESCRIPTION + Gets Astro File Content in an Open Package +#> + +param() + +$this.GetContent($this.FileList -match '\.astro$') \ No newline at end of file From cfb721d681b6d7644e396796bd42124bb821870b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Feb 2026 22:11:40 +0000 Subject: [PATCH 135/724] feat: OpenPackage.get_Astro ( Fixes #85 ) --- OP.types.ps1xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9af4532..68326f7 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -380,6 +380,21 @@ else { $partStream.Close() + + Astro + + <# +.SYNOPSIS + Gets Open Package Astro Files +.DESCRIPTION + Gets Astro File Content in an Open Package +#> + +param() + +$this.GetContent($this.FileList -match '\.astro$') + + Category From 7699df1f19854c04c7f3b2324bba5dbbed9a6aa3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Feb 2026 14:41:27 -0800 Subject: [PATCH 136/724] feat: OpenPackage.get_LanguagePercent ( Fixes #61 ) Adding languages --- Types/OpenPackage/get_LanguagePercent.ps1 | 39 ++++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/Types/OpenPackage/get_LanguagePercent.ps1 b/Types/OpenPackage/get_LanguagePercent.ps1 index a06bd66..3856a4e 100644 --- a/Types/OpenPackage/get_LanguagePercent.ps1 +++ b/Types/OpenPackage/get_LanguagePercent.ps1 @@ -3,6 +3,14 @@ Gets the language percentages of a package .DESCRIPTION Gets the language percentages present in the package. +.NOTES + Definitions of what constitutes a language have been quite contentious. + + For the purposes of accurately identifying what lies within a package, we want a very broad definition. + + If you believe a language should be included, file an issue. + + If you believe any given file format is or is not a language, do not file an issue. #> $LanguagesByLength = [Ordered]@{} @@ -13,7 +21,8 @@ foreach ($part in $this.GetParts()) { $recognizedLanguage = switch -regex ($part.Uri) { - '\.3mf' { '3MF'} + '\.3mf$' { '3MF'} + '\.astro' { 'Astro' } '\.c$' { 'C' } '\.cpp$' { 'C++' } '\.clixml$' { 'Clixml'} @@ -31,24 +40,38 @@ foreach ($part in $this.GetParts()) { '\.jpe?g$' { 'JPEG'} '\.json$' {'Json' } '\.js[mx]$' { 'Javascript'} - '\.(?>md|markdown)$' { 'Markdown' } + '\.(?>md|mdx|markdown)$' { 'Markdown' } + '\.midi?$' { 'MIDI' } + '\.mkv$' { 'Matroska Video'} + '\.mka$' { 'Matroska Audio'} + '\.mks$' { 'Matroska Subtitle'} + '\.mk3d$' { 'Matroska Stereoscopic Video'} + '\.mp3$' { 'MP3' } + '\.mp4$' { 'MP4' } + '\.nix$' { 'Nix' } + '\.oog$' { 'OOG' } '\.pl$' { 'Perl' } '\.psm?1$' { 'PowerShell' } '\.png$' { 'PNG' } '\.psd1$' {'PowerShellData' } '\.ps1xml$' { 'PowerShellXml' } - '\.py$' { 'Python' } - '\.nix$' { 'Nix' } + '\.py$' { 'Python' } '\.rs$' { 'Rust '} '\.rss$' { 'RSS' } '\.sh$' { 'BourneShell'} - '\.stl' { 'STL'} - '\.svg$' { 'SVG' } - '\.ts$' { 'TypeScript'} + '\.stl$' { 'STL'} + '\.svg$' { 'SVG' } + '\.tar$' { 'Tarfile' } + '\(?>.tar\.gz|tgz)$' { 'GZippedTarfile' } + '\.tsx?$' { 'TypeScript' } '\.tsv$' { 'TSV' } - '\.toml$' { 'TOML' } + '\.toml$' { 'TOML' } + '\.xhtml$' { 'XHTML' } + '\.xlsx$' { 'Excel '} + '\.xsl$' { 'XSL' } '\.xml$' { 'XML' } '\.ya?ml$' { 'Yaml' } + '\.zip$' { 'Zip' } default { 'Unknown' } } From bc0897aaaece9a1d7dd671f71a39aff5b97d66c3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Feb 2026 23:12:02 +0000 Subject: [PATCH 137/724] feat: OpenPackage.get_LanguagePercent ( Fixes #61 ) Adding languages --- OP.types.ps1xml | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 68326f7..f11ff85 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -854,6 +854,14 @@ $this.PackageProperties.Language = $Language Gets the language percentages of a package .DESCRIPTION Gets the language percentages present in the package. +.NOTES + Definitions of what constitutes a language have been quite contentious. + + For the purposes of accurately identifying what lies within a package, we want a very broad definition. + + If you believe a language should be included, file an issue. + + If you believe any given file format is or is not a language, do not file an issue. #> $LanguagesByLength = [Ordered]@{} @@ -864,7 +872,8 @@ foreach ($part in $this.GetParts()) { $recognizedLanguage = switch -regex ($part.Uri) { - '\.3mf' { '3MF'} + '\.3mf$' { '3MF'} + '\.astro' { 'Astro' } '\.c$' { 'C' } '\.cpp$' { 'C++' } '\.clixml$' { 'Clixml'} @@ -882,24 +891,38 @@ foreach ($part in $this.GetParts()) { '\.jpe?g$' { 'JPEG'} '\.json$' {'Json' } '\.js[mx]$' { 'Javascript'} - '\.(?>md|markdown)$' { 'Markdown' } + '\.(?>md|mdx|markdown)$' { 'Markdown' } + '\.midi?$' { 'MIDI' } + '\.mkv$' { 'Matroska Video'} + '\.mka$' { 'Matroska Audio'} + '\.mks$' { 'Matroska Subtitle'} + '\.mk3d$' { 'Matroska Stereoscopic Video'} + '\.mp3$' { 'MP3' } + '\.mp4$' { 'MP4' } + '\.nix$' { 'Nix' } + '\.oog$' { 'OOG' } '\.pl$' { 'Perl' } '\.psm?1$' { 'PowerShell' } '\.png$' { 'PNG' } '\.psd1$' {'PowerShellData' } '\.ps1xml$' { 'PowerShellXml' } - '\.py$' { 'Python' } - '\.nix$' { 'Nix' } + '\.py$' { 'Python' } '\.rs$' { 'Rust '} '\.rss$' { 'RSS' } '\.sh$' { 'BourneShell'} - '\.stl' { 'STL'} - '\.svg$' { 'SVG' } - '\.ts$' { 'TypeScript'} + '\.stl$' { 'STL'} + '\.svg$' { 'SVG' } + '\.tar$' { 'Tarfile' } + '\(?>.tar\.gz|tgz)$' { 'GZippedTarfile' } + '\.tsx?$' { 'TypeScript' } '\.tsv$' { 'TSV' } - '\.toml$' { 'TOML' } + '\.toml$' { 'TOML' } + '\.xhtml$' { 'XHTML' } + '\.xlsx$' { 'Excel '} + '\.xsl$' { 'XSL' } '\.xml$' { 'XML' } '\.ya?ml$' { 'Yaml' } + '\.zip$' { 'Zip' } default { 'Unknown' } } From e31e25060c15d0ed4ebfc609a4ee4b6efe5e5bd4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Feb 2026 15:19:06 -0800 Subject: [PATCH 138/724] docs: OpenPackage.Match ( Fixes #83 ) Adding pattern explanation --- Types/OpenPackage/Match.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage/Match.ps1 b/Types/OpenPackage/Match.ps1 index 3fabb63..b077bef 100644 --- a/Types/OpenPackage/Match.ps1 +++ b/Types/OpenPackage/Match.ps1 @@ -1,8 +1,19 @@ <# .SYNOPSIS - + Matches content in a package .DESCRIPTION + Matches content in a package. + + Takes two regular expression patterns: + + * A File Pattern + * A Content Pattern + + If neither is provided, lists parts + + If only the file pattern is provided, lists parts the match the pattern. + If both the file and content pattern are provided, outputs matches. #> param( $FilePattern, From b5f5afd2273072d07df541d58bb181bba8ec8668 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Feb 2026 23:19:25 +0000 Subject: [PATCH 139/724] docs: OpenPackage.Match ( Fixes #83 ) Adding pattern explanation --- OP.types.ps1xml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f11ff85..102b0e0 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -254,9 +254,20 @@ $matchingParts = + + + Match + + + + SetContent + + + + Astro + + <# +.SYNOPSIS + Gets Open Package Astro Files +.DESCRIPTION + Gets Astro File Content in an Open Package +#> + +param() + +$this.GetContent($this.FileList -match '\.astro$') + + + + Category + + <# +.SYNOPSIS + Gets OpenPackage `Category` +.DESCRIPTION + Gets the OpenPackage `Category` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Category + + + <# +.SYNOPSIS + Sets OpenPackage `Category` +.DESCRIPTION + Sets the OpenPackage `Category` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 +#> +param([string]$Category) + +$this.PackageProperties.Category = $Category + + + + CHANGELOG + + <# +.SYNOPSIS + Gets a package's changelog +.DESCRIPTION + Gets the content of any parts in the package named CHANGELOG.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Make the markdown a PSObject + $markdownObject = [PSObject]::new($readStream) + # add the URI + $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force + # and decorate it as `text/markdown` + $markdownObject.pstypenames.add('text/markdown') + + $markdownObject + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() +} + +# We are done. + + + param() + +$newChangelog = ($args -join [Environment]::NewLine) + [Environment]::NewLine + +$changelogParts = +@(foreach ($part in $($this.GetParts())) { + if ($part.Uri -match '/CHANGELOG\.md$') { $part} +}) + + +$stream = +if (-not $changelogParts) { + $newPart = $this.CreatePart('/CHANGELOG.md', 'text/markdown', 'Normal') + $newPart.GetStream() +} +else { + $firstChangelog = $changelogParts[0] + $changelogPart = $this.GetPart($firstChangelog.Uri) + $changelogPart.GetStream() +} + +$buffer = $OutputEncoding.GetBytes("$newChangelog") +$stream.Write($buffer,0, $buffer.Length) +$stream.Close() +$stream.Dispose() + + + + + ChocolateyInstall + + <# +.SYNOPSIS + Gets the Chocolatey Install Script +.DESCRIPTION + Gets the Chocolatey Install Script from an OpenPackage, if one is present. + + The Chocolatey install script must be located at /tools/chocolateyInstall.ps1 +#> +param() +$this.GetContent("/tools/chocolateyInstall.ps1") + + + + Config + + <# +.SYNOPSIS + Gets a package's config files +.DESCRIPTION + Gets the content of any known config files in the package: + + This includes any file named `config.*` or `_config.*` +#> +[OutputType([xml])] +param() + +$partPattern = '[/\.]_?config\.[^\.]+?$' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) + + + + + ContentStatus + + <# +.SYNOPSIS + Gets OpenPackage `ContentStatus` +.DESCRIPTION + Gets the OpenPackage `ContentStatus` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.ContentStatus + + + <# +.SYNOPSIS + Sets OpenPackage `ContentStatus` +.DESCRIPTION + Sets the OpenPackage `ContentStatus` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 +#> +param([string]$ContentStatus) + +$this.PackageProperties.ContentStatus = $ContentStatus + + + + ContentType + + <# +.SYNOPSIS + Gets OpenPackage `ContentType` +.DESCRIPTION + Gets the OpenPackage `ContentType` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.ContentType -split ';' + + + <# +.SYNOPSIS + Sets OpenPackage `ContentType` +.DESCRIPTION + Sets the OpenPackage `ContentType` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 +#> +param([string]$ContentType) + +$this.PackageProperties.ContentType = $ContentType + + + + Count + + <# +.SYNOPSIS + Gets the package files count +.DESCRIPTION + Gets the number of files in a package. +#> +return @($this.GetParts()).Length + + + + Created + + <# +.SYNOPSIS + Gets OpenPackage creation time +.DESCRIPTION + Gets the OpenPackage `Created` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Created + + + <# +.SYNOPSIS + Sets OpenPackage `Created` +.DESCRIPTION + Sets the OpenPackage `Created` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 +#> +param([DateTime]$Created) + +$this.PackageProperties.Created = $Created + + + + Creator + + <# +.SYNOPSIS + Gets OpenPackage `Creator` +.DESCRIPTION + Gets the OpenPackage `Creator` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Creator + + + <# +.SYNOPSIS + Sets OpenPackage `Creator` +.DESCRIPTION + Sets the OpenPackage `Creator` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 +#> +param([string]$Creator) + +$this.PackageProperties.Creator = $Creator + + + + Description + + return $this.PackageProperties.Description + + + $this.PackageProperties.Description = $args -join [Environment]::NewLine + + + + Dockerfile + + <# +.SYNOPSIS + Gets a package's dockerfile +.DESCRIPTION + Gets the content of any `Dockerfile`s in the package. +#> +[OutputType([xml])] +param() + +$partPattern = '[/\.]DockerFile$' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) + + + + FileContentType + + <# +.SYNOPSIS + Gets file content types +.DESCRIPTION + Gets a table of all files in the package and their associated content types. +#> +$fileContentTypes = [Ordered]@{} +foreach ($part in @($this.GetParts())) { + $fileContentTypes[$part.Uri] = $part.ContentType +} +$fileContentTypes + + + + + FileList + + <# +.SYNOPSIS + Gets OpenPackage file list +.DESCRIPTION + Gets the list of files in an OpenPackage. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.package.getparts?wt.mc_id=MVP_321542 +#> +[OutputType([string[]])] +param() + +@($this.GetParts()).Uri -as [string[]] + + + + FileSize + + <# +.SYNOPSIS + Gets file content types +.DESCRIPTION + Gets a table of all files in the package and their associated content types. +#> +$fileLengths = [Ordered]@{} +foreach ($part in @($this.GetParts())) { + $partStream = $part.GetStream() + $fileLengths[$part.Uri] = $partStream.Length + $partStream.Close() + $partStream.Dispose() +} +$fileLengths + + + + + Identifier + + $this.PackageProperties.Identifier + + + $this.PackageProperties.Identifier = $args -join ' ' + + + + ImageFileList + + <# +.SYNOPSIS + Gets package image files +.DESCRIPTION + Gets the list of image files within a package. +#> +@(foreach ($part in $this.GetParts()) { + if ($part.ContentType -match 'image/' -or + $part.Uri -match '\.(?>a?png|jpe?g|gif|tiff?|svg|ico|bmp|exr)$' + ) { + $part.Uri + } +}) -as [string[]] + + + + ImportMap + + <# +.SYNOPSIS + Gets a package's `importMap.json` +.DESCRIPTION + Gets the content of any `importMap.json` files in the package +#> +[OutputType([psobject])] +param() + + +$this.GetContent(@($this.FileList -match '\importMap\.json$')) + + + + + + Keywords + + <# +.SYNOPSIS + Gets OpenPackage `Keywords` +.DESCRIPTION + Gets the OpenPackage `Keywords` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Keywords -split '[\s\r\n]+' + + + <# +.SYNOPSIS + Sets OpenPackage `Keywords` +.DESCRIPTION + Sets the OpenPackage `Keywords` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Keywords = $args -join ' ' + + + + Language + + <# +.SYNOPSIS + Gets OpenPackage Language time +.DESCRIPTION + Gets the OpenPackage `Language` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Language + + + <# +.SYNOPSIS + Sets OpenPackage `Language` +.DESCRIPTION + Sets the OpenPackage `Language` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 +#> +param([string]$Language) + +$this.PackageProperties.Language = $Language + + + + LanguagePercent + + <# +.SYNOPSIS + Gets the language percentages of a package +.DESCRIPTION + Gets the language percentages present in the package. +.NOTES + Definitions of what constitutes a language have been quite contentious. + + For the purposes of accurately identifying what lies within a package, we want a very broad definition. + + If you believe a language should be included, file an issue. + + If you believe any given file format is or is not a language, do not file an issue. +#> +$LanguagesByLength = [Ordered]@{} + +$totalLength = 0 +$fileSizes = $this.FileSize +foreach ($part in $this.GetParts()) { + $partLength = $fileSizes[$part.Uri] + + $recognizedLanguage = + switch -regex ($part.Uri) { + '\.3mf$' { '3MF'} + '\.astro' { 'Astro' } + '\.c$' { 'C' } + '\.cpp$' { 'C++' } + '\.clixml$' { 'Clixml'} + '\.cs$' { 'C# '} + '\.csv$' { 'CSV' } + '\.csh$' { 'CShell'} + '\.css$' { 'CSS' } + '\.dll$' { 'Binary' } + '\.exe$' { 'Binary' } + '\.gif$' { 'GIF' } + '\.go$' { 'Go' } + '\.h$' { 'C' } + '\.html?$' { 'HTML' } + '\.java$' {'Java' } + '\.jpe?g$' { 'JPEG'} + '\.json$' {'Json' } + '\.js[mx]$' { 'Javascript'} + '\.(?>md|mdx|markdown)$' { 'Markdown' } + '\.midi?$' { 'MIDI' } + '\.mkv$' { 'Matroska Video'} + '\.mka$' { 'Matroska Audio'} + '\.mks$' { 'Matroska Subtitle'} + '\.mk3d$' { 'Matroska Stereoscopic Video'} + '\.mp3$' { 'MP3' } + '\.mp4$' { 'MP4' } + '\.nix$' { 'Nix' } + '\.oog$' { 'OOG' } + '\.pl$' { 'Perl' } + '\.psm?1$' { 'PowerShell' } + '\.png$' { 'PNG' } + '\.psd1$' {'PowerShellData' } + '\.ps1xml$' { 'PowerShellXml' } + '\.py$' { 'Python' } + '\.rs$' { 'Rust '} + '\.rss$' { 'RSS' } + '\.sh$' { 'BourneShell'} + '\.stl$' { 'STL'} + '\.svg$' { 'SVG' } + '\.tar$' { 'Tarfile' } + '\(?>.tar\.gz|tgz)$' { 'GZippedTarfile' } + '\.tsx?$' { 'TypeScript' } + '\.tsv$' { 'TSV' } + '\.toml$' { 'TOML' } + '\.xhtml$' { 'XHTML' } + '\.xlsx$' { 'Excel '} + '\.xsl$' { 'XSL' } + '\.xml$' { 'XML' } + '\.ya?ml$' { 'Yaml' } + '\.zip$' { 'Zip' } + default { 'Unknown' } + } + + if (-not $recognizedLanguage) { + continue + } + + + if (-not $LanguagesByLength[$recognizedLanguage]) { + $LanguagesByLength[$recognizedLanguage] = 0 + } + + $LanguagesByLength[$recognizedLanguage]+=$partLength + + $totalLength += $partLength +} + + +$SortedByLength = [Ordered]@{} + +foreach ($keyValue in $languagesByLength.GetEnumerator() | + Sort-Object Value -Descending +) { + $SortedByLength[$keyValue.Key] = $keyValue.Value / $totalLength +} + +return $SortedByLength + + + + LastModifiedBy + + <# +.SYNOPSIS + Gets OpenPackage LastModifiedBy time +.DESCRIPTION + Gets the OpenPackage `LastModifiedBy` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastmodifiedby?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.LastModifiedBy + + + <# +.SYNOPSIS + Sets OpenPackage `LastModifiedBy` +.DESCRIPTION + Sets the OpenPackage `LastModifiedBy` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.Lastmodifiedby?wt.mc_id=MVP_321542 +#> +param([string]$LastModifiedBy) + +$this.PackageProperties.LastModifiedBy = $LastModifiedBy + + + + LastPrinted + + <# +.SYNOPSIS + Gets OpenPackage LastPrinted time +.DESCRIPTION + Gets the OpenPackage `LastPrinted` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.LastPrinted + + + <# +.SYNOPSIS + Sets OpenPackage `LastPrinted` +.DESCRIPTION + Sets the OpenPackage `LastPrinted` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 +#> +param([DateTime]$LastPrinted) + +$this.PackageProperties.LastPrinted = $LastPrinted + + + + Lexicon + + <# +.SYNOPSIS + Gets any lexicons +.DESCRIPTION + Gets all at protocol lexicons in the package + + A lexicon is defined by the presence of three keys: + + * `id` + * `lexicon` + * `defs + + The output will be a table mapping ids to defs. +#> +[OutputType([Ordered])] +param() + +$allLexicons = [Ordered]@{} + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $fromJson = $readStream | ConvertFrom-Json + if ($fromJson.lexicon -and + $fromJson.id -and + $fromJson.defs + ) { + $allLexicons[$fromJson.id] = $fromJson.defs + } + +} +return $allLexicons +# We are done. + + + + ManifestJson + + <# +.SYNOPSIS + Gets a package's `manifest.json` +.DESCRIPTION + Gets the content of any `manifest.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/manifest\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. + + + + Modified + + <# +.SYNOPSIS + Gets OpenPackage modified time +.DESCRIPTION + Gets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Modified + + + <# +.SYNOPSIS + Sets OpenPackage `Modified` +.DESCRIPTION + Sets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 +#> +param([DateTime]$Modified) + +$this.PackageProperties.Modifiedd = $Modified + + + + Nix + + <# +.SYNOPSIS + Gets a package's `*.nix` files +.DESCRIPTION + Gets the content of any `*.nix` files in the package. +#> +[OutputType([string])] +param() + +$this.GetContent(@($this.FileList) -match '\.nix$') + + + + + Nuspec + + <# +.SYNOPSIS + Gets a package's nuspec +.DESCRIPTION + Gets the content of any `*.nuspec` files in the package +#> +[OutputType([xml])] +param() + +$this.GetContent( + $this.FileList -match '\.nuspec$' +) + + + + + PackageJson + + <# +.SYNOPSIS + Gets a package's `package.json` +.DESCRIPTION + Gets the content of any `package.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/package\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json +} + +# We are done. + + + + PowerShellCommandAst + + <# +.SYNOPSIS + Gets PowerShell Command References +.DESCRIPTION + Gets PowerShell Command Ast references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.CommandAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} + + + + PowerShellManifest + + <# +.SYNOPSIS + Gets a package's PowerShell manifest files +.DESCRIPTION + Gets a package's PowerShell manifest files. + + These are any `*.psd1` files in the package that: + + * Are valid PowerShell data blocks + * Contain a ModuleVersion +#> +[OutputType([PSObject])] +param() + +# Get every part +foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { + + # Skip any non-script blocks + if ($psd1 -isnot [ScriptBlock]) { continue } + # and any anything not matching ModuleVersion + if ("$psd1" -notmatch 'ModuleVersion[\s\S]{0,}=') { continue } + # and anything with more than a single statement + if ($psd1.Ast.EndBlock.Statements.Count -ne 1) { continue } + # and anything that is not a data statement + if ($psd1.Ast.EndBlock.Statements[0] -isnot + [Management.Automation.Language.DataStatementAst] -ne 1 + ) { continue } + # and anything with -supportedCommand + if ($psd1.Ast.EndBlock.Statements[0].CommandsAllowed) { + continue + } + # at long last, we can run the script and get the manifest data. + & $psd1 +} + + + + + PowerShellTypeAst + + <# +.SYNOPSIS + Gets PowerShell Type References +.DESCRIPTION + Gets PowerShell TypeExpressionAst references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.TypeExpressionAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} + + + + ProjectFile + + <# +.SYNOPSIS + Gets a package's project files +.DESCRIPTION + Gets the content of any `*.*proj` files in the package +#> +[OutputType([xml])] +param() + +$this.GetContent( + $this -match '\..+?proj$' +) + + + + README + + <# +.SYNOPSIS + Gets a package's README +.DESCRIPTION + Gets the content of any parts in the package named README.md +#> +[OutputType("text/markdown")] +param() + +# Get all Readme files +foreach ($content in $this.GetContent( + $this.FileList -match '/README\.md$' +)) { + # decorate them as text/markdown + if ($content.pstypenames -notcontains 'text/markdown') { + $content.pstypenames.insert(0, 'text/markdown') + } + # and return them. + $content +} + +return + + + + Revision + + <# +.SYNOPSIS + Gets OpenPackage `Revision` +.DESCRIPTION + Gets the OpenPackage `Revision` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Revision + + + <# +.SYNOPSIS + Sets OpenPackage `Revision` +.DESCRIPTION + Sets the OpenPackage `Revision` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 +#> +param([string]$Revision) + +$this.PackageProperties.Revision = $Revision + + + + ServiceWorker + + <# +.SYNOPSIS + Gets any service workers in a package +.DESCRIPTION + Gets any clearly named service workers in a package. + + Will find any files named `sw.js` or `ServiceWorker.js` + + +#> +[OutputType("text/javascript")] +param() + +foreach ($content in $this.GetContent( + $this.FileList -match '/(?>sw|ServiceWorker).js$' +)) { + if ($content.pstypenames -notcontains 'text/javascript') { + $content.pstypenames.insert(0, 'text/javascript') + } + $content +} + +return + + +# We are done. + + + + Subject + + <# +.SYNOPSIS + Gets OpenPackage `Subject` +.DESCRIPTION + Gets the OpenPackage `Subject` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Subject + + + <# +.SYNOPSIS + Sets OpenPackage `Subject` +.DESCRIPTION + Sets the OpenPackage `Subject` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 +#> +param([string]$Subject) + +$this.PackageProperties.Subject = $Subject + + + + TemplateJson + + <# +.SYNOPSIS + Gets a package's `template.json` +.DESCRIPTION + Gets the content of any `template.json` files in the package +#> +[OutputType([PSObject])] +param() + +$this.GetContent($this.FileList -match '/template\.json$') + + + + Title + + <# +.SYNOPSIS + Gets OpenPackage `Title` +.DESCRIPTION + Gets the OpenPackage `Title` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Title + + + <# +.SYNOPSIS + Sets OpenPackage `Title` +.DESCRIPTION + Sets the OpenPackage `Title` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 +#> +param([string]$Title) + +$this.PackageProperties.Title = $Title + + + + TypeScriptConfig + + <# +.SYNOPSIS + Gets a package's `tsconfig.json` +.DESCRIPTION + Gets the content of any TypeScript configuration `tsconfig.json` files in the package +#> +[OutputType([psobject])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/tsconfig\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. + + + + Version + + <# +.SYNOPSIS + Gets OpenPackage `Version` +.DESCRIPTION + Gets the OpenPackage `Version` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Version + + + <# +.SYNOPSIS + Sets OpenPackage `Version` +.DESCRIPTION + Sets the OpenPackage `Version` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 +#> +param([string]$Version) + +$this.PackageProperties.Version = $Version + + + + VsixManifest + + <# +.SYNOPSIS + Gets VsixManfiests from a package +.DESCRIPTION + Gets any Visual Studio Extension Manifests (*.vsixManifest) files in a package. +#> +$partPattern = '\.vsixManifest$' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) + + + + + DefaultDisplay + Identifier +FileList + + +
OpenPackage @@ -1554,6 +3121,1573 @@ $this.PackageProperties.Version = $Version #> $partPattern = '\.vsixManifest$' +$this.GetContent(@( + $this.FileList -match $partPattern +)) + + + + + DefaultDisplay + Identifier +FileList + + + + + System.IO.Packaging.Package + + + PSStandardMembers + + + DefaultDisplayPropertySet + + Identifier + FileList + + + + + + Lexicons + Lexicon + + + RemovePart + DeletePart + + + GetContent + + + + Match + + + + SetContent + + + + Astro + + <# +.SYNOPSIS + Gets Open Package Astro Files +.DESCRIPTION + Gets Astro File Content in an Open Package +#> + +param() + +$this.GetContent($this.FileList -match '\.astro$') + + + + Category + + <# +.SYNOPSIS + Gets OpenPackage `Category` +.DESCRIPTION + Gets the OpenPackage `Category` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Category + + + <# +.SYNOPSIS + Sets OpenPackage `Category` +.DESCRIPTION + Sets the OpenPackage `Category` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 +#> +param([string]$Category) + +$this.PackageProperties.Category = $Category + + + + CHANGELOG + + <# +.SYNOPSIS + Gets a package's changelog +.DESCRIPTION + Gets the content of any parts in the package named CHANGELOG.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Make the markdown a PSObject + $markdownObject = [PSObject]::new($readStream) + # add the URI + $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force + # and decorate it as `text/markdown` + $markdownObject.pstypenames.add('text/markdown') + + $markdownObject + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() +} + +# We are done. + + + param() + +$newChangelog = ($args -join [Environment]::NewLine) + [Environment]::NewLine + +$changelogParts = +@(foreach ($part in $($this.GetParts())) { + if ($part.Uri -match '/CHANGELOG\.md$') { $part} +}) + + +$stream = +if (-not $changelogParts) { + $newPart = $this.CreatePart('/CHANGELOG.md', 'text/markdown', 'Normal') + $newPart.GetStream() +} +else { + $firstChangelog = $changelogParts[0] + $changelogPart = $this.GetPart($firstChangelog.Uri) + $changelogPart.GetStream() +} + +$buffer = $OutputEncoding.GetBytes("$newChangelog") +$stream.Write($buffer,0, $buffer.Length) +$stream.Close() +$stream.Dispose() + + + + + ChocolateyInstall + + <# +.SYNOPSIS + Gets the Chocolatey Install Script +.DESCRIPTION + Gets the Chocolatey Install Script from an OpenPackage, if one is present. + + The Chocolatey install script must be located at /tools/chocolateyInstall.ps1 +#> +param() +$this.GetContent("/tools/chocolateyInstall.ps1") + + + + Config + + <# +.SYNOPSIS + Gets a package's config files +.DESCRIPTION + Gets the content of any known config files in the package: + + This includes any file named `config.*` or `_config.*` +#> +[OutputType([xml])] +param() + +$partPattern = '[/\.]_?config\.[^\.]+?$' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) + + + + + ContentStatus + + <# +.SYNOPSIS + Gets OpenPackage `ContentStatus` +.DESCRIPTION + Gets the OpenPackage `ContentStatus` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.ContentStatus + + + <# +.SYNOPSIS + Sets OpenPackage `ContentStatus` +.DESCRIPTION + Sets the OpenPackage `ContentStatus` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 +#> +param([string]$ContentStatus) + +$this.PackageProperties.ContentStatus = $ContentStatus + + + + ContentType + + <# +.SYNOPSIS + Gets OpenPackage `ContentType` +.DESCRIPTION + Gets the OpenPackage `ContentType` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.ContentType -split ';' + + + <# +.SYNOPSIS + Sets OpenPackage `ContentType` +.DESCRIPTION + Sets the OpenPackage `ContentType` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 +#> +param([string]$ContentType) + +$this.PackageProperties.ContentType = $ContentType + + + + Count + + <# +.SYNOPSIS + Gets the package files count +.DESCRIPTION + Gets the number of files in a package. +#> +return @($this.GetParts()).Length + + + + Created + + <# +.SYNOPSIS + Gets OpenPackage creation time +.DESCRIPTION + Gets the OpenPackage `Created` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Created + + + <# +.SYNOPSIS + Sets OpenPackage `Created` +.DESCRIPTION + Sets the OpenPackage `Created` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 +#> +param([DateTime]$Created) + +$this.PackageProperties.Created = $Created + + + + Creator + + <# +.SYNOPSIS + Gets OpenPackage `Creator` +.DESCRIPTION + Gets the OpenPackage `Creator` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Creator + + + <# +.SYNOPSIS + Sets OpenPackage `Creator` +.DESCRIPTION + Sets the OpenPackage `Creator` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 +#> +param([string]$Creator) + +$this.PackageProperties.Creator = $Creator + + + + Description + + return $this.PackageProperties.Description + + + $this.PackageProperties.Description = $args -join [Environment]::NewLine + + + + Dockerfile + + <# +.SYNOPSIS + Gets a package's dockerfile +.DESCRIPTION + Gets the content of any `Dockerfile`s in the package. +#> +[OutputType([xml])] +param() + +$partPattern = '[/\.]DockerFile$' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) + + + + FileContentType + + <# +.SYNOPSIS + Gets file content types +.DESCRIPTION + Gets a table of all files in the package and their associated content types. +#> +$fileContentTypes = [Ordered]@{} +foreach ($part in @($this.GetParts())) { + $fileContentTypes[$part.Uri] = $part.ContentType +} +$fileContentTypes + + + + + FileList + + <# +.SYNOPSIS + Gets OpenPackage file list +.DESCRIPTION + Gets the list of files in an OpenPackage. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.package.getparts?wt.mc_id=MVP_321542 +#> +[OutputType([string[]])] +param() + +@($this.GetParts()).Uri -as [string[]] + + + + FileSize + + <# +.SYNOPSIS + Gets file content types +.DESCRIPTION + Gets a table of all files in the package and their associated content types. +#> +$fileLengths = [Ordered]@{} +foreach ($part in @($this.GetParts())) { + $partStream = $part.GetStream() + $fileLengths[$part.Uri] = $partStream.Length + $partStream.Close() + $partStream.Dispose() +} +$fileLengths + + + + + Identifier + + $this.PackageProperties.Identifier + + + $this.PackageProperties.Identifier = $args -join ' ' + + + + ImageFileList + + <# +.SYNOPSIS + Gets package image files +.DESCRIPTION + Gets the list of image files within a package. +#> +@(foreach ($part in $this.GetParts()) { + if ($part.ContentType -match 'image/' -or + $part.Uri -match '\.(?>a?png|jpe?g|gif|tiff?|svg|ico|bmp|exr)$' + ) { + $part.Uri + } +}) -as [string[]] + + + + ImportMap + + <# +.SYNOPSIS + Gets a package's `importMap.json` +.DESCRIPTION + Gets the content of any `importMap.json` files in the package +#> +[OutputType([psobject])] +param() + + +$this.GetContent(@($this.FileList -match '\importMap\.json$')) + + + + + + Keywords + + <# +.SYNOPSIS + Gets OpenPackage `Keywords` +.DESCRIPTION + Gets the OpenPackage `Keywords` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Keywords -split '[\s\r\n]+' + + + <# +.SYNOPSIS + Sets OpenPackage `Keywords` +.DESCRIPTION + Sets the OpenPackage `Keywords` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Keywords = $args -join ' ' + + + + Language + + <# +.SYNOPSIS + Gets OpenPackage Language time +.DESCRIPTION + Gets the OpenPackage `Language` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Language + + + <# +.SYNOPSIS + Sets OpenPackage `Language` +.DESCRIPTION + Sets the OpenPackage `Language` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 +#> +param([string]$Language) + +$this.PackageProperties.Language = $Language + + + + LanguagePercent + + <# +.SYNOPSIS + Gets the language percentages of a package +.DESCRIPTION + Gets the language percentages present in the package. +.NOTES + Definitions of what constitutes a language have been quite contentious. + + For the purposes of accurately identifying what lies within a package, we want a very broad definition. + + If you believe a language should be included, file an issue. + + If you believe any given file format is or is not a language, do not file an issue. +#> +$LanguagesByLength = [Ordered]@{} + +$totalLength = 0 +$fileSizes = $this.FileSize +foreach ($part in $this.GetParts()) { + $partLength = $fileSizes[$part.Uri] + + $recognizedLanguage = + switch -regex ($part.Uri) { + '\.3mf$' { '3MF'} + '\.astro' { 'Astro' } + '\.c$' { 'C' } + '\.cpp$' { 'C++' } + '\.clixml$' { 'Clixml'} + '\.cs$' { 'C# '} + '\.csv$' { 'CSV' } + '\.csh$' { 'CShell'} + '\.css$' { 'CSS' } + '\.dll$' { 'Binary' } + '\.exe$' { 'Binary' } + '\.gif$' { 'GIF' } + '\.go$' { 'Go' } + '\.h$' { 'C' } + '\.html?$' { 'HTML' } + '\.java$' {'Java' } + '\.jpe?g$' { 'JPEG'} + '\.json$' {'Json' } + '\.js[mx]$' { 'Javascript'} + '\.(?>md|mdx|markdown)$' { 'Markdown' } + '\.midi?$' { 'MIDI' } + '\.mkv$' { 'Matroska Video'} + '\.mka$' { 'Matroska Audio'} + '\.mks$' { 'Matroska Subtitle'} + '\.mk3d$' { 'Matroska Stereoscopic Video'} + '\.mp3$' { 'MP3' } + '\.mp4$' { 'MP4' } + '\.nix$' { 'Nix' } + '\.oog$' { 'OOG' } + '\.pl$' { 'Perl' } + '\.psm?1$' { 'PowerShell' } + '\.png$' { 'PNG' } + '\.psd1$' {'PowerShellData' } + '\.ps1xml$' { 'PowerShellXml' } + '\.py$' { 'Python' } + '\.rs$' { 'Rust '} + '\.rss$' { 'RSS' } + '\.sh$' { 'BourneShell'} + '\.stl$' { 'STL'} + '\.svg$' { 'SVG' } + '\.tar$' { 'Tarfile' } + '\(?>.tar\.gz|tgz)$' { 'GZippedTarfile' } + '\.tsx?$' { 'TypeScript' } + '\.tsv$' { 'TSV' } + '\.toml$' { 'TOML' } + '\.xhtml$' { 'XHTML' } + '\.xlsx$' { 'Excel '} + '\.xsl$' { 'XSL' } + '\.xml$' { 'XML' } + '\.ya?ml$' { 'Yaml' } + '\.zip$' { 'Zip' } + default { 'Unknown' } + } + + if (-not $recognizedLanguage) { + continue + } + + + if (-not $LanguagesByLength[$recognizedLanguage]) { + $LanguagesByLength[$recognizedLanguage] = 0 + } + + $LanguagesByLength[$recognizedLanguage]+=$partLength + + $totalLength += $partLength +} + + +$SortedByLength = [Ordered]@{} + +foreach ($keyValue in $languagesByLength.GetEnumerator() | + Sort-Object Value -Descending +) { + $SortedByLength[$keyValue.Key] = $keyValue.Value / $totalLength +} + +return $SortedByLength + + + + LastModifiedBy + + <# +.SYNOPSIS + Gets OpenPackage LastModifiedBy time +.DESCRIPTION + Gets the OpenPackage `LastModifiedBy` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastmodifiedby?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.LastModifiedBy + + + <# +.SYNOPSIS + Sets OpenPackage `LastModifiedBy` +.DESCRIPTION + Sets the OpenPackage `LastModifiedBy` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.Lastmodifiedby?wt.mc_id=MVP_321542 +#> +param([string]$LastModifiedBy) + +$this.PackageProperties.LastModifiedBy = $LastModifiedBy + + + + LastPrinted + + <# +.SYNOPSIS + Gets OpenPackage LastPrinted time +.DESCRIPTION + Gets the OpenPackage `LastPrinted` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.LastPrinted + + + <# +.SYNOPSIS + Sets OpenPackage `LastPrinted` +.DESCRIPTION + Sets the OpenPackage `LastPrinted` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 +#> +param([DateTime]$LastPrinted) + +$this.PackageProperties.LastPrinted = $LastPrinted + + + + Lexicon + + <# +.SYNOPSIS + Gets any lexicons +.DESCRIPTION + Gets all at protocol lexicons in the package + + A lexicon is defined by the presence of three keys: + + * `id` + * `lexicon` + * `defs + + The output will be a table mapping ids to defs. +#> +[OutputType([Ordered])] +param() + +$allLexicons = [Ordered]@{} + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $fromJson = $readStream | ConvertFrom-Json + if ($fromJson.lexicon -and + $fromJson.id -and + $fromJson.defs + ) { + $allLexicons[$fromJson.id] = $fromJson.defs + } + +} +return $allLexicons +# We are done. + + + + ManifestJson + + <# +.SYNOPSIS + Gets a package's `manifest.json` +.DESCRIPTION + Gets the content of any `manifest.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/manifest\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. + + + + Modified + + <# +.SYNOPSIS + Gets OpenPackage modified time +.DESCRIPTION + Gets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Modified + + + <# +.SYNOPSIS + Sets OpenPackage `Modified` +.DESCRIPTION + Sets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 +#> +param([DateTime]$Modified) + +$this.PackageProperties.Modifiedd = $Modified + + + + Nix + + <# +.SYNOPSIS + Gets a package's `*.nix` files +.DESCRIPTION + Gets the content of any `*.nix` files in the package. +#> +[OutputType([string])] +param() + +$this.GetContent(@($this.FileList) -match '\.nix$') + + + + + Nuspec + + <# +.SYNOPSIS + Gets a package's nuspec +.DESCRIPTION + Gets the content of any `*.nuspec` files in the package +#> +[OutputType([xml])] +param() + +$this.GetContent( + $this.FileList -match '\.nuspec$' +) + + + + + PackageJson + + <# +.SYNOPSIS + Gets a package's `package.json` +.DESCRIPTION + Gets the content of any `package.json` files in the package +#> +[OutputType([xml])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/package\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json +} + +# We are done. + + + + PowerShellCommandAst + + <# +.SYNOPSIS + Gets PowerShell Command References +.DESCRIPTION + Gets PowerShell Command Ast references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.CommandAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} + + + + PowerShellManifest + + <# +.SYNOPSIS + Gets a package's PowerShell manifest files +.DESCRIPTION + Gets a package's PowerShell manifest files. + + These are any `*.psd1` files in the package that: + + * Are valid PowerShell data blocks + * Contain a ModuleVersion +#> +[OutputType([PSObject])] +param() + +# Get every part +foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { + + # Skip any non-script blocks + if ($psd1 -isnot [ScriptBlock]) { continue } + # and any anything not matching ModuleVersion + if ("$psd1" -notmatch 'ModuleVersion[\s\S]{0,}=') { continue } + # and anything with more than a single statement + if ($psd1.Ast.EndBlock.Statements.Count -ne 1) { continue } + # and anything that is not a data statement + if ($psd1.Ast.EndBlock.Statements[0] -isnot + [Management.Automation.Language.DataStatementAst] -ne 1 + ) { continue } + # and anything with -supportedCommand + if ($psd1.Ast.EndBlock.Statements[0].CommandsAllowed) { + continue + } + # at long last, we can run the script and get the manifest data. + & $psd1 +} + + + + + PowerShellTypeAst + + <# +.SYNOPSIS + Gets PowerShell Type References +.DESCRIPTION + Gets PowerShell TypeExpressionAst references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.TypeExpressionAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} + + + + ProjectFile + + <# +.SYNOPSIS + Gets a package's project files +.DESCRIPTION + Gets the content of any `*.*proj` files in the package +#> +[OutputType([xml])] +param() + +$this.GetContent( + $this -match '\..+?proj$' +) + + + + README + + <# +.SYNOPSIS + Gets a package's README +.DESCRIPTION + Gets the content of any parts in the package named README.md +#> +[OutputType("text/markdown")] +param() + +# Get all Readme files +foreach ($content in $this.GetContent( + $this.FileList -match '/README\.md$' +)) { + # decorate them as text/markdown + if ($content.pstypenames -notcontains 'text/markdown') { + $content.pstypenames.insert(0, 'text/markdown') + } + # and return them. + $content +} + +return + + + + Revision + + <# +.SYNOPSIS + Gets OpenPackage `Revision` +.DESCRIPTION + Gets the OpenPackage `Revision` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Revision + + + <# +.SYNOPSIS + Sets OpenPackage `Revision` +.DESCRIPTION + Sets the OpenPackage `Revision` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 +#> +param([string]$Revision) + +$this.PackageProperties.Revision = $Revision + + + + ServiceWorker + + <# +.SYNOPSIS + Gets any service workers in a package +.DESCRIPTION + Gets any clearly named service workers in a package. + + Will find any files named `sw.js` or `ServiceWorker.js` + + +#> +[OutputType("text/javascript")] +param() + +foreach ($content in $this.GetContent( + $this.FileList -match '/(?>sw|ServiceWorker).js$' +)) { + if ($content.pstypenames -notcontains 'text/javascript') { + $content.pstypenames.insert(0, 'text/javascript') + } + $content +} + +return + + +# We are done. + + + + Subject + + <# +.SYNOPSIS + Gets OpenPackage `Subject` +.DESCRIPTION + Gets the OpenPackage `Subject` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Subject + + + <# +.SYNOPSIS + Sets OpenPackage `Subject` +.DESCRIPTION + Sets the OpenPackage `Subject` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 +#> +param([string]$Subject) + +$this.PackageProperties.Subject = $Subject + + + + TemplateJson + + <# +.SYNOPSIS + Gets a package's `template.json` +.DESCRIPTION + Gets the content of any `template.json` files in the package +#> +[OutputType([PSObject])] +param() + +$this.GetContent($this.FileList -match '/template\.json$') + + + + Title + + <# +.SYNOPSIS + Gets OpenPackage `Title` +.DESCRIPTION + Gets the OpenPackage `Title` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Title + + + <# +.SYNOPSIS + Sets OpenPackage `Title` +.DESCRIPTION + Sets the OpenPackage `Title` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 +#> +param([string]$Title) + +$this.PackageProperties.Title = $Title + + + + TypeScriptConfig + + <# +.SYNOPSIS + Gets a package's `tsconfig.json` +.DESCRIPTION + Gets the content of any TypeScript configuration `tsconfig.json` files in the package +#> +[OutputType([psobject])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/tsconfig\.json$') { continue } + + # Read the stream + $partStream = $part.GetStream() + $streamReader = [IO.StreamReader]::new($partStream) + + $readStream = $streamReader.ReadToEnd() + + # Close the reader + $streamReader.Close() + $streamReader.Dispose() + + # and the stream. + $partStream.Close() + $partStream.Dispose() + + # Convert the part from json. + $readStream | ConvertFrom-Json | + Add-Member NoteProperty Uri $part.Uri -Force -PassThru +} + +# We are done. + + + + Version + + <# +.SYNOPSIS + Gets OpenPackage `Version` +.DESCRIPTION + Gets the OpenPackage `Version` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 +#> +param() + +$this.PackageProperties.Version + + + <# +.SYNOPSIS + Sets OpenPackage `Version` +.DESCRIPTION + Sets the OpenPackage `Version` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 +#> +param([string]$Version) + +$this.PackageProperties.Version = $Version + + + + VsixManifest + + <# +.SYNOPSIS + Gets VsixManfiests from a package +.DESCRIPTION + Gets any Visual Studio Extension Manifests (*.vsixManifest) files in a package. +#> +$partPattern = '\.vsixManifest$' + $this.GetContent(@( $this.FileList -match $partPattern )) From 49cc6b17acc3c2cde490b5b4f9282ae215cacb0c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Feb 2026 12:40:32 -0800 Subject: [PATCH 151/724] feat: OpenPackage.get_Eleventy ( Fixes #90 ) --- Types/OpenPackage/Alias.psd1 | 1 + Types/OpenPackage/get_Eleventy.ps1 | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 Types/OpenPackage/get_Eleventy.ps1 diff --git a/Types/OpenPackage/Alias.psd1 b/Types/OpenPackage/Alias.psd1 index 1d93d2c..c96299a 100644 --- a/Types/OpenPackage/Alias.psd1 +++ b/Types/OpenPackage/Alias.psd1 @@ -1,4 +1,5 @@ @{ RemovePart = "DeletePart" Lexicons = "Lexicon" + 11ty = "Eleventy" } \ No newline at end of file diff --git a/Types/OpenPackage/get_Eleventy.ps1 b/Types/OpenPackage/get_Eleventy.ps1 new file mode 100644 index 0000000..75ab7d8 --- /dev/null +++ b/Types/OpenPackage/get_Eleventy.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Gets a package's eleventy files +.DESCRIPTION + Gets the content of any elevent config files in the package. + + This includes any files named: + * `.eleventy.js` + * `eleventy.config.js` + * `eleventy.config.mjs` + * `eleventy.config.cjs` +.LINK + https://www.11ty.dev/docs/config/ +#> +[OutputType([xml])] +param() + +$partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) From 32bcb9f2d01b8e8aa0b75bfd08ffc14ecdb61453 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Feb 2026 12:40:54 -0800 Subject: [PATCH 152/724] feat: OpenPackage.get_Eleventy ( Fixes #90 ) --- Types/OpenPackage/Alias.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Types/OpenPackage/Alias.psd1 b/Types/OpenPackage/Alias.psd1 index c96299a..69adc8e 100644 --- a/Types/OpenPackage/Alias.psd1 +++ b/Types/OpenPackage/Alias.psd1 @@ -1,5 +1,5 @@ @{ RemovePart = "DeletePart" Lexicons = "Lexicon" - 11ty = "Eleventy" + '11ty' = "Eleventy" } \ No newline at end of file From b17abe1e3f3bc7de851e5bd39a38001b93b485fa Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Feb 2026 20:41:16 +0000 Subject: [PATCH 153/724] feat: OpenPackage.get_Eleventy ( Fixes #90 ) --- OP.types.ps1xml | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 3c19c12..bf79fd3 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -15,6 +15,10 @@ + + 11ty + Eleventy + Lexicons Lexicon @@ -700,6 +704,34 @@ $this.GetContent(@( )) + + Eleventy + + <# +.SYNOPSIS + Gets a package's eleventy files +.DESCRIPTION + Gets the content of any elevent config files in the package. + + This includes any files named: + * `.eleventy.js` + * `eleventy.config.js` + * `eleventy.config.mjs` + * `eleventy.config.cjs` +.LINK + https://www.11ty.dev/docs/config/ +#> +[OutputType([xml])] +param() + +$partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) + + + FileContentType @@ -1582,6 +1614,10 @@ FileList + + 11ty + Eleventy + Lexicons Lexicon @@ -2267,6 +2303,34 @@ $this.GetContent(@( )) + + Eleventy + + <# +.SYNOPSIS + Gets a package's eleventy files +.DESCRIPTION + Gets the content of any elevent config files in the package. + + This includes any files named: + * `.eleventy.js` + * `eleventy.config.js` + * `eleventy.config.mjs` + * `eleventy.config.cjs` +.LINK + https://www.11ty.dev/docs/config/ +#> +[OutputType([xml])] +param() + +$partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) + + + FileContentType @@ -3149,6 +3213,10 @@ FileList + + 11ty + Eleventy + Lexicons Lexicon @@ -3834,6 +3902,34 @@ $this.GetContent(@( )) + + Eleventy + + <# +.SYNOPSIS + Gets a package's eleventy files +.DESCRIPTION + Gets the content of any elevent config files in the package. + + This includes any files named: + * `.eleventy.js` + * `eleventy.config.js` + * `eleventy.config.mjs` + * `eleventy.config.cjs` +.LINK + https://www.11ty.dev/docs/config/ +#> +[OutputType([xml])] +param() + +$partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) + + + FileContentType From 990f4c7aa90ea528855f410470b83826efd506e3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Feb 2026 12:49:40 -0800 Subject: [PATCH 154/724] feat: OpenPackage.get_LanguagePercent ( Fixes #61 ) Adding languages --- Types/OpenPackage/get_LanguagePercent.ps1 | 41 ++++++++++++++--------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/Types/OpenPackage/get_LanguagePercent.ps1 b/Types/OpenPackage/get_LanguagePercent.ps1 index 3856a4e..118e879 100644 --- a/Types/OpenPackage/get_LanguagePercent.ps1 +++ b/Types/OpenPackage/get_LanguagePercent.ps1 @@ -24,24 +24,31 @@ foreach ($part in $this.GetParts()) { '\.3mf$' { '3MF'} '\.astro' { 'Astro' } '\.c$' { 'C' } + '\.cast$' { 'Asciiema' } + '\.clixml$' { 'Clixml'} + '\.cjs$' { 'Common JavaScript'} '\.cpp$' { 'C++' } - '\.clixml$' { 'Clixml'} '\.cs$' { 'C# '} - '\.csv$' { 'CSV' } + '\.csv$' { 'Comma Separated Values' } '\.csh$' { 'CShell'} - '\.css$' { 'CSS' } + '\.css$' { 'Cascading Stylesheets' } + '(?>/word/.+?\.xml|\.docx?)$' { 'Word '} '\.dll$' { 'Binary' } '\.exe$' { 'Binary' } '\.gif$' { 'GIF' } - '\.go$' { 'Go' } - '\.h$' { 'C' } - '\.html?$' { 'HTML' } + '\.go$' { 'Go Language' } + '\.h$' { 'C Header' } + '\.html?$' { 'Hypertext Markup Language' } '\.java$' {'Java' } - '\.jpe?g$' { 'JPEG'} - '\.json$' {'Json' } - '\.js[mx]$' { 'Javascript'} + '\.jpe?g$' { 'Joint Pictures Expert Group'} + '\.json$' {'JavaScript Object Notation' } + '\.jsonc$' {'Commented JavaScript Object Notation' } + '\.jsonl$' {'JavaScript Object Notation Lines' } + '\.js$' { 'Javascript'} + '\.jsx$' { 'JavaScript XML'} '\.(?>md|mdx|markdown)$' { 'Markdown' } '\.midi?$' { 'MIDI' } + '\.(?>jsm|mjs)$' { 'JavaScript Module'} '\.mkv$' { 'Matroska Video'} '\.mka$' { 'Matroska Audio'} '\.mks$' { 'Matroska Subtitle'} @@ -51,10 +58,11 @@ foreach ($part in $this.GetParts()) { '\.nix$' { 'Nix' } '\.oog$' { 'OOG' } '\.pl$' { 'Perl' } + '\.png$' { 'Portable Network Graphics' } + '(?>/ppt/.+?\.xml|\.pptx?)$' { 'PowerPoint'} '\.psm?1$' { 'PowerShell' } - '\.png$' { 'PNG' } - '\.psd1$' {'PowerShellData' } - '\.ps1xml$' { 'PowerShellXml' } + '\.psd1$' {'PowerShell Data Language' } + '\.ps1xml$' { 'PowerShell Xml' } '\.py$' { 'Python' } '\.rs$' { 'Rust '} '\.rss$' { 'RSS' } @@ -64,14 +72,17 @@ foreach ($part in $this.GetParts()) { '\.tar$' { 'Tarfile' } '\(?>.tar\.gz|tgz)$' { 'GZippedTarfile' } '\.tsx?$' { 'TypeScript' } - '\.tsv$' { 'TSV' } - '\.toml$' { 'TOML' } + '\.tsv$' { 'Tab Separated Values' } + '\.toml$' { 'Tom''s Obvious Minimal Language' } '\.xhtml$' { 'XHTML' } - '\.xlsx$' { 'Excel '} + '(?>/xl/.+?\.xml|\.xlsx?)$' { 'Excel'} '\.xsl$' { 'XSL' } '\.xml$' { 'XML' } '\.ya?ml$' { 'Yaml' } '\.zip$' { 'Zip' } + '\.webm' { 'Web Movie' } + '\.weba' { 'Web Audio' } + '\.webp' { 'Web Photo' } default { 'Unknown' } } From a59150f805a6429d1858f4ee251047aa4aa62012 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Feb 2026 20:54:18 +0000 Subject: [PATCH 155/724] feat: OpenPackage.get_LanguagePercent ( Fixes #61 ) Adding languages --- OP.types.ps1xml | 123 ++++++++++++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 45 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index bf79fd3..a19ca79 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -918,24 +918,31 @@ foreach ($part in $this.GetParts()) { '\.3mf$' { '3MF'} '\.astro' { 'Astro' } '\.c$' { 'C' } + '\.cast$' { 'Asciiema' } + '\.clixml$' { 'Clixml'} + '\.cjs$' { 'Common JavaScript'} '\.cpp$' { 'C++' } - '\.clixml$' { 'Clixml'} '\.cs$' { 'C# '} - '\.csv$' { 'CSV' } + '\.csv$' { 'Comma Separated Values' } '\.csh$' { 'CShell'} - '\.css$' { 'CSS' } + '\.css$' { 'Cascading Stylesheets' } + '(?>/word/.+?\.xml|\.docx?)$' { 'Word '} '\.dll$' { 'Binary' } '\.exe$' { 'Binary' } '\.gif$' { 'GIF' } - '\.go$' { 'Go' } - '\.h$' { 'C' } - '\.html?$' { 'HTML' } + '\.go$' { 'Go Language' } + '\.h$' { 'C Header' } + '\.html?$' { 'Hypertext Markup Language' } '\.java$' {'Java' } - '\.jpe?g$' { 'JPEG'} - '\.json$' {'Json' } - '\.js[mx]$' { 'Javascript'} + '\.jpe?g$' { 'Joint Pictures Expert Group'} + '\.json$' {'JavaScript Object Notation' } + '\.jsonc$' {'Commented JavaScript Object Notation' } + '\.jsonl$' {'JavaScript Object Notation Lines' } + '\.js$' { 'Javascript'} + '\.jsx$' { 'JavaScript XML'} '\.(?>md|mdx|markdown)$' { 'Markdown' } '\.midi?$' { 'MIDI' } + '\.(?>jsm|mjs)$' { 'JavaScript Module'} '\.mkv$' { 'Matroska Video'} '\.mka$' { 'Matroska Audio'} '\.mks$' { 'Matroska Subtitle'} @@ -945,10 +952,11 @@ foreach ($part in $this.GetParts()) { '\.nix$' { 'Nix' } '\.oog$' { 'OOG' } '\.pl$' { 'Perl' } + '\.png$' { 'Portable Network Graphics' } + '(?>/ppt/.+?\.xml|\.pptx?)$' { 'PowerPoint'} '\.psm?1$' { 'PowerShell' } - '\.png$' { 'PNG' } - '\.psd1$' {'PowerShellData' } - '\.ps1xml$' { 'PowerShellXml' } + '\.psd1$' {'PowerShell Data Language' } + '\.ps1xml$' { 'PowerShell Xml' } '\.py$' { 'Python' } '\.rs$' { 'Rust '} '\.rss$' { 'RSS' } @@ -958,14 +966,17 @@ foreach ($part in $this.GetParts()) { '\.tar$' { 'Tarfile' } '\(?>.tar\.gz|tgz)$' { 'GZippedTarfile' } '\.tsx?$' { 'TypeScript' } - '\.tsv$' { 'TSV' } - '\.toml$' { 'TOML' } + '\.tsv$' { 'Tab Separated Values' } + '\.toml$' { 'Tom''s Obvious Minimal Language' } '\.xhtml$' { 'XHTML' } - '\.xlsx$' { 'Excel '} + '(?>/xl/.+?\.xml|\.xlsx?)$' { 'Excel'} '\.xsl$' { 'XSL' } '\.xml$' { 'XML' } '\.ya?ml$' { 'Yaml' } '\.zip$' { 'Zip' } + '\.webm' { 'Web Movie' } + '\.weba' { 'Web Audio' } + '\.webp' { 'Web Photo' } default { 'Unknown' } } @@ -2517,24 +2528,31 @@ foreach ($part in $this.GetParts()) { '\.3mf$' { '3MF'} '\.astro' { 'Astro' } '\.c$' { 'C' } + '\.cast$' { 'Asciiema' } + '\.clixml$' { 'Clixml'} + '\.cjs$' { 'Common JavaScript'} '\.cpp$' { 'C++' } - '\.clixml$' { 'Clixml'} '\.cs$' { 'C# '} - '\.csv$' { 'CSV' } + '\.csv$' { 'Comma Separated Values' } '\.csh$' { 'CShell'} - '\.css$' { 'CSS' } + '\.css$' { 'Cascading Stylesheets' } + '(?>/word/.+?\.xml|\.docx?)$' { 'Word '} '\.dll$' { 'Binary' } '\.exe$' { 'Binary' } '\.gif$' { 'GIF' } - '\.go$' { 'Go' } - '\.h$' { 'C' } - '\.html?$' { 'HTML' } + '\.go$' { 'Go Language' } + '\.h$' { 'C Header' } + '\.html?$' { 'Hypertext Markup Language' } '\.java$' {'Java' } - '\.jpe?g$' { 'JPEG'} - '\.json$' {'Json' } - '\.js[mx]$' { 'Javascript'} + '\.jpe?g$' { 'Joint Pictures Expert Group'} + '\.json$' {'JavaScript Object Notation' } + '\.jsonc$' {'Commented JavaScript Object Notation' } + '\.jsonl$' {'JavaScript Object Notation Lines' } + '\.js$' { 'Javascript'} + '\.jsx$' { 'JavaScript XML'} '\.(?>md|mdx|markdown)$' { 'Markdown' } '\.midi?$' { 'MIDI' } + '\.(?>jsm|mjs)$' { 'JavaScript Module'} '\.mkv$' { 'Matroska Video'} '\.mka$' { 'Matroska Audio'} '\.mks$' { 'Matroska Subtitle'} @@ -2544,10 +2562,11 @@ foreach ($part in $this.GetParts()) { '\.nix$' { 'Nix' } '\.oog$' { 'OOG' } '\.pl$' { 'Perl' } + '\.png$' { 'Portable Network Graphics' } + '(?>/ppt/.+?\.xml|\.pptx?)$' { 'PowerPoint'} '\.psm?1$' { 'PowerShell' } - '\.png$' { 'PNG' } - '\.psd1$' {'PowerShellData' } - '\.ps1xml$' { 'PowerShellXml' } + '\.psd1$' {'PowerShell Data Language' } + '\.ps1xml$' { 'PowerShell Xml' } '\.py$' { 'Python' } '\.rs$' { 'Rust '} '\.rss$' { 'RSS' } @@ -2557,14 +2576,17 @@ foreach ($part in $this.GetParts()) { '\.tar$' { 'Tarfile' } '\(?>.tar\.gz|tgz)$' { 'GZippedTarfile' } '\.tsx?$' { 'TypeScript' } - '\.tsv$' { 'TSV' } - '\.toml$' { 'TOML' } + '\.tsv$' { 'Tab Separated Values' } + '\.toml$' { 'Tom''s Obvious Minimal Language' } '\.xhtml$' { 'XHTML' } - '\.xlsx$' { 'Excel '} + '(?>/xl/.+?\.xml|\.xlsx?)$' { 'Excel'} '\.xsl$' { 'XSL' } '\.xml$' { 'XML' } '\.ya?ml$' { 'Yaml' } '\.zip$' { 'Zip' } + '\.webm' { 'Web Movie' } + '\.weba' { 'Web Audio' } + '\.webp' { 'Web Photo' } default { 'Unknown' } } @@ -4116,24 +4138,31 @@ foreach ($part in $this.GetParts()) { '\.3mf$' { '3MF'} '\.astro' { 'Astro' } '\.c$' { 'C' } + '\.cast$' { 'Asciiema' } + '\.clixml$' { 'Clixml'} + '\.cjs$' { 'Common JavaScript'} '\.cpp$' { 'C++' } - '\.clixml$' { 'Clixml'} '\.cs$' { 'C# '} - '\.csv$' { 'CSV' } + '\.csv$' { 'Comma Separated Values' } '\.csh$' { 'CShell'} - '\.css$' { 'CSS' } + '\.css$' { 'Cascading Stylesheets' } + '(?>/word/.+?\.xml|\.docx?)$' { 'Word '} '\.dll$' { 'Binary' } '\.exe$' { 'Binary' } '\.gif$' { 'GIF' } - '\.go$' { 'Go' } - '\.h$' { 'C' } - '\.html?$' { 'HTML' } + '\.go$' { 'Go Language' } + '\.h$' { 'C Header' } + '\.html?$' { 'Hypertext Markup Language' } '\.java$' {'Java' } - '\.jpe?g$' { 'JPEG'} - '\.json$' {'Json' } - '\.js[mx]$' { 'Javascript'} + '\.jpe?g$' { 'Joint Pictures Expert Group'} + '\.json$' {'JavaScript Object Notation' } + '\.jsonc$' {'Commented JavaScript Object Notation' } + '\.jsonl$' {'JavaScript Object Notation Lines' } + '\.js$' { 'Javascript'} + '\.jsx$' { 'JavaScript XML'} '\.(?>md|mdx|markdown)$' { 'Markdown' } '\.midi?$' { 'MIDI' } + '\.(?>jsm|mjs)$' { 'JavaScript Module'} '\.mkv$' { 'Matroska Video'} '\.mka$' { 'Matroska Audio'} '\.mks$' { 'Matroska Subtitle'} @@ -4143,10 +4172,11 @@ foreach ($part in $this.GetParts()) { '\.nix$' { 'Nix' } '\.oog$' { 'OOG' } '\.pl$' { 'Perl' } + '\.png$' { 'Portable Network Graphics' } + '(?>/ppt/.+?\.xml|\.pptx?)$' { 'PowerPoint'} '\.psm?1$' { 'PowerShell' } - '\.png$' { 'PNG' } - '\.psd1$' {'PowerShellData' } - '\.ps1xml$' { 'PowerShellXml' } + '\.psd1$' {'PowerShell Data Language' } + '\.ps1xml$' { 'PowerShell Xml' } '\.py$' { 'Python' } '\.rs$' { 'Rust '} '\.rss$' { 'RSS' } @@ -4156,14 +4186,17 @@ foreach ($part in $this.GetParts()) { '\.tar$' { 'Tarfile' } '\(?>.tar\.gz|tgz)$' { 'GZippedTarfile' } '\.tsx?$' { 'TypeScript' } - '\.tsv$' { 'TSV' } - '\.toml$' { 'TOML' } + '\.tsv$' { 'Tab Separated Values' } + '\.toml$' { 'Tom''s Obvious Minimal Language' } '\.xhtml$' { 'XHTML' } - '\.xlsx$' { 'Excel '} + '(?>/xl/.+?\.xml|\.xlsx?)$' { 'Excel'} '\.xsl$' { 'XSL' } '\.xml$' { 'XML' } '\.ya?ml$' { 'Yaml' } '\.zip$' { 'Zip' } + '\.webm' { 'Web Movie' } + '\.weba' { 'Web Audio' } + '\.webp' { 'Web Photo' } default { 'Unknown' } } From 4fa395783849f7ebb042785a6d869e4003f35805 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Feb 2026 12:02:24 -0800 Subject: [PATCH 156/724] feat: Explicitly exporting aliases, adding intro ( Fixes #1 ) --- Commands/Copy-OpenPackage.ps1 | 2 +- Commands/Expand-OpenPackage.ps1 | 1 + Commands/Export-OpenPackage.ps1 | 5 +- Commands/Get-OpenPackage.ps1 | 142 +++----------------------------- Commands/Join-OpenPackage.ps1 | 2 +- Commands/Read-OpenPackage.ps1 | 8 +- Commands/Remove-OpenPackage.ps1 | 2 +- Commands/Select-OpenPackage.ps1 | 2 +- Commands/Set-OpenPackage.ps1 | 2 +- Commands/Split-OpenPackage.ps1 | 1 + Commands/Start-OpenPackage.ps1 | 4 +- Commands/Write-OpenPackage.ps1 | 4 +- OP.psd1 | 80 ++++++++++++++++-- 13 files changed, 103 insertions(+), 152 deletions(-) diff --git a/Commands/Copy-OpenPackage.ps1 b/Commands/Copy-OpenPackage.ps1 index c350129..4e30aa0 100644 --- a/Commands/Copy-OpenPackage.ps1 +++ b/Commands/Copy-OpenPackage.ps1 @@ -8,7 +8,7 @@ function Copy-OpenPackage .EXAMPLE Copy-OpenPackage -DestinationPath ./Examples/Copy.docx -InputObject ./Examples/Sample.docx -Force #> - [Alias('Copy-OP','cpop')] + [Alias('Copy-OP','cpop','cpOpenPackage')] param( # The destination. # If this is not a `[IO.Packaging.Package]`, it will be considered a file path. diff --git a/Commands/Expand-OpenPackage.ps1 b/Commands/Expand-OpenPackage.ps1 index 319891b..f3264dd 100644 --- a/Commands/Expand-OpenPackage.ps1 +++ b/Commands/Expand-OpenPackage.ps1 @@ -9,6 +9,7 @@ function Expand-OpenPackage Expand-Archive #> [CmdletBinding(PositionalBinding=$false)] + [Alias('Expand-OP', 'enop','enOpenPackage')] param( # The destination path. This should be a directory. [Parameter(Position=0)] diff --git a/Commands/Export-OpenPackage.ps1 b/Commands/Export-OpenPackage.ps1 index 09dcb4e..f5e5beb 100644 --- a/Commands/Export-OpenPackage.ps1 +++ b/Commands/Export-OpenPackage.ps1 @@ -5,10 +5,11 @@ function Export-OpenPackage { .DESCRIPTION Exports loaded packages to a file or directory. #> - [Alias('Save-OpenPackage','epop','svop')] + [Alias('Export-OP','epop','epOpenPackage', 'Save-OpenPackage','Save-OP','svop', 'svOpenPackage')] param( # The package file path. - # If this has no extension, it will be considered a directory name, and + # If this has no extension, it will be considered a directory name. + # If the path already exists and is a directory, it will be considered a directory name. [Parameter(Mandatory,ValueFromPipelineByPropertyName)] [Alias('Path','FilePath','Fullname')] [string] diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 5dbe080..8d91617 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -55,6 +55,9 @@ function Get-OpenPackage .EXAMPLE # Get a package from nuget $dependencyInjectionPackage = OP https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection + .EXAMPLE + # Get a package from Chocolatey + $chocoPackage = op https://community.chocolatey.org/packages/chocolatey .EXAMPLE # Get a package from the PowerShell gallery $turtlePackage = op https://powershellgallery.com/packages/Turtle @@ -64,16 +67,19 @@ function Get-OpenPackage .EXAMPLE # Create a package from multiple URLs by piping back to ourself $svgAndPng = op https://MrPowerShell.com/MrPowerShell.png | - op https://MrPowerShell.com/MrPowerShell.svg + op https://MrPowerShell.com/MrPowerShell.svg .EXAMPLE # Get a package from an at protocol URI $atPost = op at://mrpowershell.com/app.bsky.feed.post/3k4hf5dy6nf2g .EXAMPLE # Get the most recent 50 posts $atLast50 = op at://mrpowershell.com/app.bsky.feed.post/ -First 50 + .EXAMPLE + # Get all standard.site.documents + $standardSiteDocuments = op at://mrpowershell.com/site.standard.document/ #> [CmdletBinding(PositionalBinding=$false,DefaultParameterSetName='Any',SupportsPaging)] - [Alias('Get-OP', 'OP', 'OpenPackage')] + [Alias('Get-OP', 'OP', 'OpenPackage','gOpenPackage')] param( # Any unnamed arguments to the command. # Each argument will be treated as a potential -FilePath or -Uri. @@ -87,7 +93,7 @@ function Get-OpenPackage [Parameter(Mandatory,ParameterSetName='FilePath',ValueFromPipelineByPropertyName)] [Alias('Fullname')] [string] - $FilePath, + $FilePath, # A URI to package. # If this URI is a git repository, will make a package out of the repository @@ -248,133 +254,7 @@ function Get-OpenPackage $currentPackage.pstypenames.insert(0, 'OpenPackage') } $currentPackage - } - - filter getPartContent { - $thisPart = $_ - - # If this part has no stream, return now - if (-not $thisPart.GetStream) { - return - } - - # Get the stream - $partStream = $thisPart.GetStream() - - # And copy it right into a memory stream - $memoryStream = [IO.MemoryStream]::new() - $partStream.CopyTo($memoryStream) - # and clean up and close out (this minimizes read time) - $partStream.Close() - $partStream.Dispose() - - # Then get that stream - [byte[]]$partBytes = $memoryStream.ToArray() - - # Seek back to the beginning - $null = $memoryStream.Seek(0, 'begin') - # and read it as text - $partStreamReader = [IO.StreamReader]::new($memoryStream) - $partString = $partStreamReader.ReadToEnd() - # then close and dispose of our memory. - $partStreamReader.Close() - $partStreamReader.Dispose() - $memoryStream.Close() - $memoryStream.Dispose() - - # We will base our parsing off of the content type and uri - - # Starting off with one of the easy content types: - # `x-www-form-urlencoded` (aka form data) - if ($thisPart.ContentType -eq 'application/x-www-form-urlencoded') { - # Let's parse this - $formData = [Web.HttpUtility]::ParseQueryString($partString) - # into a nice ordered dictionary - $formDataObject = [Ordered]@{} - # Remember that multiple values are passed with multiple keys. - foreach ($key in $formData.Keys) { - if ($null -eq $formDataObject[$key]) { - $formDataObject[$key] = $formData[$key] - } else { - $formDataObject[$key] = @($formDataObject[$key]) + $formData[$key] - } - } - # and output our form data dictionary. - return $formDataObject - } - - # If the content type was some `xml` type or the uri ends with `xml` - if (($thisPart.ContentType -match '[/\+]xml$' -or $thisPart.Uri -match 'xml$')) { - # try casting it to XML - $partAsXml = $partString -as [xml] - if ($partASXml.Objs) { - try { - [Management.Automation.PSSerializer]::Deserialize($partString) - return - } catch { - Write-Warning "$($thisPart.Uri) was not clixml" - } - } - - if ($null -ne $partAsXml) { - return $partAsXml - } - } - - # If the content type is json or the uri ends with json - if ($thisPart.ContentType -match '[/\+].json$' -or $thisPart.Uri -match 'json$') { - # try to convert it from json - $partAsJson = try { - ConvertFrom-Json -InputObject $partString -ErrorAction Ignore - } catch { - Write-Warning "'$($thisPart.Uri)' was not json: $_" - } - - if ($null -ne $partAsJson) { - $partAsJson - return - } - } - - # If the content type was powershell data (or the part ended with .psd1) - if ($thisPart.ContentType -in 'text/x-powershell-data', - 'application/x-powershell-data' -or $thisPart.Uri -match '\.psd1$') { - - # Try to create a data block - $dataBlockOutput = try { - $scriptBlock = [ScriptBlock]::Create($partString) - $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") - & $dataScriptBlock - } catch { - Write-Warning "$($thisPart.Uri) is not a valid PowerShell data file: $_" - } - - if ($null -ne $dataBlockOutput) { - $dataBlockOutput - return - } - } - - # If the content type is PowerShell or this is a .ps1/psm1 - if ($thisPart.ContentType -in 'text/x-powershell', - 'application/x-powershell' -or $thisPart.Uri -match '\.psm?1$') { - - # try to create a script block. - try { - [ScriptBlock]::Create($partString) - } catch { - Write-Warning "$($thisPart.Uri) is not a valid PowerShell script: $_" - } - return - } - - - if ($thisPart.ContentType -match '^text/') { - $partString - } else { - ,$partBytes - } - } + } filter packAt { param() @@ -1231,7 +1111,7 @@ function Get-OpenPackage } $currentPackage - } + } $gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git', 'Application') diff --git a/Commands/Join-OpenPackage.ps1 b/Commands/Join-OpenPackage.ps1 index 6f61397..eb99d4e 100644 --- a/Commands/Join-OpenPackage.ps1 +++ b/Commands/Join-OpenPackage.ps1 @@ -6,7 +6,7 @@ function Join-OpenPackage .DESCRIPTION Joins multiple open packages into a single open package #> - [Alias('Join-OP','jop')] + [Alias('Join-OP','jop','jOpenPackage')] [CmdletBinding(PositionalBinding=$false)] param( [Parameter(ValueFromPipeline)] diff --git a/Commands/Read-OpenPackage.ps1 b/Commands/Read-OpenPackage.ps1 index 862bc63..0bd194b 100644 --- a/Commands/Read-OpenPackage.ps1 +++ b/Commands/Read-OpenPackage.ps1 @@ -19,6 +19,7 @@ function Read-OpenPackage Read-OpenPackage -Uri /hello.txt -RangeStart 0 -RangeEnd 5) -as 'char[]' -join '' #> [OutputType([byte[]])] + [Alias('Read-OP', 'rdop', 'rdOpenPackage')] param( [Parameter(Mandatory,ValueFromPipelineByPropertyName)] [Alias('PartUri')] @@ -42,8 +43,9 @@ function Read-OpenPackage ) process { + # If the input was not a package, if ($InputObject -isnot [IO.Packaging.Package]) { - return $InputObject + return $InputObject # pass it thru. } $SlashPart = "$uri" -replace '^/?', '/' @@ -82,9 +84,7 @@ function Read-OpenPackage $memoryStream.Close() $memoryStream.Dispose() - - - + return ,$partBytes } } \ No newline at end of file diff --git a/Commands/Remove-OpenPackage.ps1 b/Commands/Remove-OpenPackage.ps1 index 050c2e6..3b9dad5 100644 --- a/Commands/Remove-OpenPackage.ps1 +++ b/Commands/Remove-OpenPackage.ps1 @@ -6,7 +6,7 @@ function Remove-OpenPackage { Removes content parts from an open package. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] - [Alias('Remove-OP','rop')] + [Alias('Remove-OP','rop','rOpenPackage')] param( # One or more URIs to remove [Parameter(ValueFromPipelineByPropertyName)] diff --git a/Commands/Select-OpenPackage.ps1 b/Commands/Select-OpenPackage.ps1 index 646371b..140a53d 100644 --- a/Commands/Select-OpenPackage.ps1 +++ b/Commands/Select-OpenPackage.ps1 @@ -10,7 +10,7 @@ function Select-OpenPackage .LINK Select-Xml #> - [Alias('Select-OP','scop')] + [Alias('Select-OP','scop', 'scOpenPackage')] [CmdletBinding(DefaultParameterSetName='All')] param( [Parameter(ParameterSetName='Select-String',ValueFromPipelineByPropertyName)] diff --git a/Commands/Set-OpenPackage.ps1 b/Commands/Set-OpenPackage.ps1 index c4f282d..e2de6cc 100644 --- a/Commands/Set-OpenPackage.ps1 +++ b/Commands/Set-OpenPackage.ps1 @@ -13,7 +13,7 @@ function Set-OpenPackage .LINK Get-OpenPackage #> - [Alias('Set-OP','sop')] + [Alias('Set-OP','sop','sOpenPackage')] param( # The uri to set [Parameter(Mandatory,ValueFromPipelineByPropertyName)] diff --git a/Commands/Split-OpenPackage.ps1 b/Commands/Split-OpenPackage.ps1 index b79a8f5..d54963e 100644 --- a/Commands/Split-OpenPackage.ps1 +++ b/Commands/Split-OpenPackage.ps1 @@ -14,6 +14,7 @@ function Split-OpenPackage { Select-OpenPackage #> [CmdletBinding(PositionalBinding=$false)] + [Alias('Split-OP','slop', 'slOpenPackage')] param( # One or more properties to group. [Parameter(ValueFromRemainingArguments)] diff --git a/Commands/Start-OpenPackage.ps1 b/Commands/Start-OpenPackage.ps1 index d15f992..d2b9435 100644 --- a/Commands/Start-OpenPackage.ps1 +++ b/Commands/Start-OpenPackage.ps1 @@ -19,7 +19,7 @@ function Start-OpenPackage { .EXAMPLE #> - [Alias('Start-OP')] + [Alias('Start-OP','stOpenPackage')] [CmdletBinding(PositionalBinding=$false)] param( # The path to an OpenXML file, or a glob that matches multiple OpenXML files. @@ -173,7 +173,7 @@ function Start-OpenPackage { :nextRequest while ($httpListener.IsListening) { $getContextAsync = $httpListener.GetContextAsync() # wait in short increments to minimize CPU impact and stay snappy - while (-not $getContextAsync.Wait(7)) { + while (-not $getContextAsync.Wait(23)) { } # Get our listener context diff --git a/Commands/Write-OpenPackage.ps1 b/Commands/Write-OpenPackage.ps1 index b2316bf..6ade579 100644 --- a/Commands/Write-OpenPackage.ps1 +++ b/Commands/Write-OpenPackage.ps1 @@ -19,6 +19,7 @@ function Write-OpenPackage .LINK Set-OpenPackage #> + [Alias('Write-OP', 'wrop', 'wrOpenPackage')] param( # The Package Part Uri. This is the path to the content within a package. [Parameter(Mandatory,ValueFromPipelineByPropertyName)] @@ -65,8 +66,7 @@ function Write-OpenPackage $null = $partStream.Seek($RangeStart, 'Begin') } - $partStream.Write($Buffer, 0, $Buffer.Length) - + $partStream.Write($Buffer, 0, $Buffer.Length) $partStream.Close() $partStream.Dispose() diff --git a/OP.psd1 b/OP.psd1 index 81d2396..8bff1bc 100644 --- a/OP.psd1 +++ b/OP.psd1 @@ -69,16 +69,46 @@ TypesToProcess = @('OP.types.ps1xml') # 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 = '*' +FunctionsToExport = @( + 'Copy-OpenPackage' + 'Expand-OpenPackage' + 'Export-OpenPackage' + 'Get-OpenPackage' + 'Join-OpenPackage' + 'New-OpenPackage' + 'Read-OpenPackage' + 'Remove-OpenPackage' + 'Select-OpenPackage' + 'Set-OpenPackage' + 'Split-OpenPackage' + 'Start-OpenPackage' + 'Write-OpenPackage' +) + # 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 = '*' +CmdletsToExport = @() # Variables to export from this module -VariablesToExport = '*' +VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '*' +AliasesToExport = @( + 'Copy-OP','cpop','cpOpenPackage' + 'Expand-OP', 'enop','enOpenPackage' + 'Export-OP','epop','epOpenPackage', 'Save-OpenPackage','Save-OP','svop', 'svOpenPackage' + 'Get-OP', 'OP', 'OpenPackage','gOpenPackage' + 'Join-OP','jop','jOpenPackage' + 'New-OP','nop', 'nOpenPackage' + 'Read-OP', 'rdop', 'rdOpenPackage' + 'Remove-OP','rop','rOpenPackage' + 'Select-OP','scop', 'scOpenPackage' + 'Set-OP','sop','sOpenPackage' + 'Split-OP','slop', 'slOpenPackage' + 'Start-OP','stOpenPackage' + + +) # DSC resources to export from this module # DscResourcesToExport = @() @@ -98,10 +128,10 @@ PrivateData = @{ Tags = @('OpenPackages','Zip','Nuget','PowerShellGallery','Git','AtProto','WebServer','Overpowered') # A URL to the license for this module. - LicenseUri = 'https://github.com/PowerShellWeb/OP/tree/main/LICENSE' + LicenseUri = 'https://github.com/PoshWeb/OP/tree/main/LICENSE' # A URL to the main website for this project. - ProjectUri = 'https://github.com/PowerShellWeb/OP' + ProjectUri = 'https://github.com/PoshWeb/OP' # A URL to an icon representing this module. # IconUri = '' @@ -118,6 +148,44 @@ PrivateData = @{ # External dependent modules of this module # ExternalModuleDependencies = @() + PSIntro = @' +Anything can be a package. + +This command helps you make anything into a package. + +The following types of packages are currently supported: + +* Any [Open Packaging Convention](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) files +* Any directory +* Any `*.zip` file +* Any `*.tar.gz` file +* Any nuget package +* Any git repository +* Any public at protocol URI +* Any dictionary (including nested dictionaries) +* Any url +* Any file + +_Anything can be a package_. + +Once we start to treat anything as a package, we can do amazing things with packages. + +Like: + +* Inspect any packages before we work with them. +* Modify the packages to customize their content. +* Split packages +* Filter our components. +* Join them back together. +* Search package content. +* Work with compressed trees of data. +* Have an in-memory containerized virtual filesystem. +* Serve a package from memory. +* Store data to N package layers. + +To put it simply, open packages are overpowered. +'@ + } # End of PSData hashtable } # End of PrivateData hashtable From c61b3f3349a4e4901d77dba274a725174bb4cb73 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Feb 2026 12:11:39 -0800 Subject: [PATCH 157/724] feat: New-OpenPackage ( Fixes #92 ) --- Commands/New-OpenPackage.ps1 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Commands/New-OpenPackage.ps1 diff --git a/Commands/New-OpenPackage.ps1 b/Commands/New-OpenPackage.ps1 new file mode 100644 index 0000000..3273b72 --- /dev/null +++ b/Commands/New-OpenPackage.ps1 @@ -0,0 +1,20 @@ +function New-OpenPackage { + <# + .SYNOPSIS + Creates an empty open package + .DESCRIPTION + Creates an new empty open package + #> + [Alias('New-OP','nop', 'nOpenPackage')] + param() + + process { + $memoryStream = [IO.MemoryStream]::new() + $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate') + $package | + Add-Member NoteProperty MemoryStream $memoryStream -Force -PassThru + } + + + +} \ No newline at end of file From dc3cb0a1d175d1b02fc2091da1808943dd031673 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Feb 2026 18:05:15 -0800 Subject: [PATCH 158/724] feat: Install-OpenPackage ( Fixes #52 ) --- ...penPackage.ps1 => Install-OpenPackage.ps1} | 47 ++++++++++++++++--- OP.psd1 | 21 +++++++-- 2 files changed, 58 insertions(+), 10 deletions(-) rename Commands/{Expand-OpenPackage.ps1 => Install-OpenPackage.ps1} (73%) diff --git a/Commands/Expand-OpenPackage.ps1 b/Commands/Install-OpenPackage.ps1 similarity index 73% rename from Commands/Expand-OpenPackage.ps1 rename to Commands/Install-OpenPackage.ps1 index f3264dd..9cbeeca 100644 --- a/Commands/Expand-OpenPackage.ps1 +++ b/Commands/Install-OpenPackage.ps1 @@ -1,4 +1,4 @@ -function Expand-OpenPackage +function Install-OpenPackage { <# .SYNOPSIS @@ -9,7 +9,10 @@ function Expand-OpenPackage Expand-Archive #> [CmdletBinding(PositionalBinding=$false)] - [Alias('Expand-OP', 'enop','enOpenPackage')] + [Alias( + 'Install-OP', 'inop', 'inOpenPackage', + 'Expand-OpenPackage','Expand-OP', 'enop','enOpenPackage' + )] param( # The destination path. This should be a directory. [Parameter(Position=0)] @@ -81,7 +84,27 @@ function Expand-OpenPackage # Pass thru any input that is not a package. if ($InputObject -isnot [IO.Packaging.Package]) { return $InputObject } + $package = $inputObject + if (-not $PSBoundParameters['DestinationPath']) { + if ((-not $package.Identifier) -or + (-not $package.Version) -or + (-not $env:OpenPackagePath) + ) { + Write-Error "Must provide -DestinationPath or have a package identifier and version" + return + } else { + $PSBoundParameters['DestinationPath'] = $destinationPath = + Join-Path ( + Join-Path ( + @($env:OpenPackagePath -split $( + if (-not ($IsLinux -or $IsMacOS)) { ';' } + else { ':' } + ))[0] + ) $package.Identifier + ) $package.Version + } + } # Copy our parameters to Select-OpenPackage $selectSplat = [Ordered]@{InputObject=$InputObject} @@ -102,6 +125,7 @@ function Expand-OpenPackage Id = Get-Random } + $existingFiles = @() # Go over each part :nextPart foreach ($part in $inputParts) { @@ -112,13 +136,15 @@ function Expand-OpenPackage $Progress.PercentComplete = $counter * 100 / $total # and write progress. Write-Progress @Progress - # Then check if it exists + # Then check if it exists. $fileInfo = if ((Test-Path -LiteralPath $partDestination)) { if (-not $Force) { - # and warn if they are not -Forcing the point - Write-Warning "$PartDestination exists, use -Force to overwrite" - continue nextPart + # We will warn when we're done, + # but don't -Force the point by warning each time. + # Add it to the list of existing files + $existingFiles += Get-Item -LiteralPath $partDestination + continue nextPart # (and continue to the next part). } Get-Item -LiteralPath $partDestination } else { @@ -155,12 +181,19 @@ function Expand-OpenPackage Add-Member NoteProperty PartUri $part.Uri -Force -PassThru | Add-Member NoteProperty PartContentType $part.ContentType -Force -PassThru } - } + } # After we have expanded all of the parts $Progress.Remove('PercentComplete') $Progress.Completed = $true # complete our progress Write-Progress @Progress + + if ($existingFiles) { + Write-Warning "$($existingFiles.Length) Files Exist (Use ``-Force`` to overwrite):$( + [Environment]::NewLine + $existingFiles -join [Environment]::NewLine + )" + } } } \ No newline at end of file diff --git a/OP.psd1 b/OP.psd1 index 8bff1bc..771ac71 100644 --- a/OP.psd1 +++ b/OP.psd1 @@ -70,10 +70,10 @@ TypesToProcess = @('OP.types.ps1xml') # 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 = @( - 'Copy-OpenPackage' - 'Expand-OpenPackage' + 'Copy-OpenPackage' 'Export-OpenPackage' 'Get-OpenPackage' + 'Install-OpenPackage' 'Join-OpenPackage' 'New-OpenPackage' 'Read-OpenPackage' @@ -95,16 +95,31 @@ VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. AliasesToExport = @( 'Copy-OP','cpop','cpOpenPackage' + 'Expand-OP', 'enop','enOpenPackage' - 'Export-OP','epop','epOpenPackage', 'Save-OpenPackage','Save-OP','svop', 'svOpenPackage' + + 'Export-OP','epop','epOpenPackage', + 'Save-OpenPackage','Save-OP','svop', 'svOpenPackage' + 'Get-OP', 'OP', 'OpenPackage','gOpenPackage' + + 'Install-OP', 'inop', 'inOpenPackage', + 'Expand-OpenPackage','Expand-OP', 'enop','enOpenPackage' + 'Join-OP','jop','jOpenPackage' + 'New-OP','nop', 'nOpenPackage' + 'Read-OP', 'rdop', 'rdOpenPackage' + 'Remove-OP','rop','rOpenPackage' + 'Select-OP','scop', 'scOpenPackage' + 'Set-OP','sop','sOpenPackage' + 'Split-OP','slop', 'slOpenPackage' + 'Start-OP','stOpenPackage' From be1d01be16124006c5e72a471563473864c08323 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 11:30:51 -0800 Subject: [PATCH 159/724] feat: Lock-OpenPackage ( Fixes #93 ) --- Commands/Lock-OpenPackage.ps1 | 44 +++++++++++++++++++++++++++++++++++ OP.psd1 | 3 +++ 2 files changed, 47 insertions(+) create mode 100644 Commands/Lock-OpenPackage.ps1 diff --git a/Commands/Lock-OpenPackage.ps1 b/Commands/Lock-OpenPackage.ps1 new file mode 100644 index 0000000..5794fcb --- /dev/null +++ b/Commands/Lock-OpenPackage.ps1 @@ -0,0 +1,44 @@ +function Lock-OpenPackage +{ + <# + .SYNOPSIS + Locks an Open Package + .DESCRIPTION + Locks an Open Package. + + Closes the package and copies it into a read-only package. + .EXAMPLE + # Import OP, make it a package, and lock it + Import-Module OP -PassThru | + Get-OpenPackage | + Lock-OpenPackage + .EXAMPLE + # Import OP, make it a package, and lock it + impo OP -PassThru | op | lkop + #> + [CmdletBinding(ConfirmImpact='Medium')] + [Alias('Lock-OP', 'lkop','lkOpenPackage')] + param( + # The input object. This should be a package. + [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)] + [Alias('Package')] + [PSObject] + $InputObject + ) + + process { + if ($InputObject -isnot [IO.Packaging.Package]) { + return $InputObject + } + if ($InputObject.MemoryStream -isnot [IO.MemoryStream]) { + return $InputObject + } + + $InputObject.Close() + $null = $InputObject.MemoryStream.Seek(0,'begin') + [byte[]]$Buffer = $InputObject.MemoryStream.ToArray() + $newStream = [IO.MemoryStream]::new($Buffer) + $newPackage = [IO.Packaging.Package]::Open($newStream, 'Open', 'Read') + $newPackage + } +} \ No newline at end of file diff --git a/OP.psd1 b/OP.psd1 index 771ac71..de05dc4 100644 --- a/OP.psd1 +++ b/OP.psd1 @@ -75,6 +75,7 @@ FunctionsToExport = @( 'Get-OpenPackage' 'Install-OpenPackage' 'Join-OpenPackage' + 'Lock-OpenPackage' 'New-OpenPackage' 'Read-OpenPackage' 'Remove-OpenPackage' @@ -107,6 +108,8 @@ AliasesToExport = @( 'Expand-OpenPackage','Expand-OP', 'enop','enOpenPackage' 'Join-OP','jop','jOpenPackage' + + 'Lock-OP', 'lkop','lkOpenPackage' 'New-OP','nop', 'nOpenPackage' From 9f34fbb971efa5e64a427101135bfe7db88bebfc Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 11:36:39 -0800 Subject: [PATCH 160/724] feat: Export-OpenPackage ( Fixes #39 ) Spreading aliases --- Commands/Export-OpenPackage.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Commands/Export-OpenPackage.ps1 b/Commands/Export-OpenPackage.ps1 index f5e5beb..16a418f 100644 --- a/Commands/Export-OpenPackage.ps1 +++ b/Commands/Export-OpenPackage.ps1 @@ -5,7 +5,10 @@ function Export-OpenPackage { .DESCRIPTION Exports loaded packages to a file or directory. #> - [Alias('Export-OP','epop','epOpenPackage', 'Save-OpenPackage','Save-OP','svop', 'svOpenPackage')] + [Alias( + 'Export-OP','epop','epOpenPackage', + 'Save-OpenPackage','Save-OP','svop', 'svOpenPackage' + )] param( # The package file path. # If this has no extension, it will be considered a directory name. @@ -61,7 +64,7 @@ function Export-OpenPackage { # If there is no input return if (-not $InputObject) { return } # If the input is not a package, pass it thru - if ($InputObject -isnot [IO.Packaging.Package]) { return $InputObject } + if ($InputObject -isnot [IO.Packaging.Package]) { return $InputObject } # Get the item if it already exists $destinationItem = Get-Item $DestinationPath -ErrorAction Ignore From d0f333e2db62c1f32773260c808f211e61a7cbb0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:24:20 -0800 Subject: [PATCH 161/724] feat: OpenPackage.get_Version ( Fixes #17 ) Adding automatic version detection --- Types/OpenPackage/get_Version.ps1 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage/get_Version.ps1 b/Types/OpenPackage/get_Version.ps1 index 0a1d22b..4db930f 100644 --- a/Types/OpenPackage/get_Version.ps1 +++ b/Types/OpenPackage/get_Version.ps1 @@ -3,9 +3,24 @@ Gets OpenPackage `Version` .DESCRIPTION Gets the OpenPackage `Version` property. + + If this has not been explicitly set, looks for potential version informatin .LINK https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 #> param() -$this.PackageProperties.Version \ No newline at end of file +if ($this.PackageProperties.Version) { + return $this.PackageProperties.Version +} + +$moduleManifest = $this.PowerShellManifest +if ($moduleManifest) { + $this.PackageProperties.Version = $moduleManifest.ModuleVersion + return $this.PackageProperties.Version +} + +$packageJson = $this.PackageJson +if ($packageJson -and $packageJson.version) { + $this.PackageProperties.Version = $packageJson.version +} \ No newline at end of file From 0867ea4542aaf7681ed42681d9953474309f6f12 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:24:40 +0000 Subject: [PATCH 162/724] feat: OpenPackage.get_Version ( Fixes #17 ) Adding automatic version detection --- OP.types.ps1xml | 51 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index a19ca79..a3afb2b 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1565,12 +1565,27 @@ foreach ($part in $this.GetParts()) { Gets OpenPackage `Version` .DESCRIPTION Gets the OpenPackage `Version` property. + + If this has not been explicitly set, looks for potential version informatin .LINK https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 #> param() -$this.PackageProperties.Version +if ($this.PackageProperties.Version) { + return $this.PackageProperties.Version +} + +$moduleManifest = $this.PowerShellManifest +if ($moduleManifest) { + $this.PackageProperties.Version = $moduleManifest.ModuleVersion + return $this.PackageProperties.Version +} + +$packageJson = $this.PackageJson +if ($packageJson -and $packageJson.version) { + $this.PackageProperties.Version = $packageJson.version +} <# @@ -3175,12 +3190,27 @@ foreach ($part in $this.GetParts()) { Gets OpenPackage `Version` .DESCRIPTION Gets the OpenPackage `Version` property. + + If this has not been explicitly set, looks for potential version informatin .LINK https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 #> param() -$this.PackageProperties.Version +if ($this.PackageProperties.Version) { + return $this.PackageProperties.Version +} + +$moduleManifest = $this.PowerShellManifest +if ($moduleManifest) { + $this.PackageProperties.Version = $moduleManifest.ModuleVersion + return $this.PackageProperties.Version +} + +$packageJson = $this.PackageJson +if ($packageJson -and $packageJson.version) { + $this.PackageProperties.Version = $packageJson.version +} <# @@ -4785,12 +4815,27 @@ foreach ($part in $this.GetParts()) { Gets OpenPackage `Version` .DESCRIPTION Gets the OpenPackage `Version` property. + + If this has not been explicitly set, looks for potential version informatin .LINK https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 #> param() -$this.PackageProperties.Version +if ($this.PackageProperties.Version) { + return $this.PackageProperties.Version +} + +$moduleManifest = $this.PowerShellManifest +if ($moduleManifest) { + $this.PackageProperties.Version = $moduleManifest.ModuleVersion + return $this.PackageProperties.Version +} + +$packageJson = $this.PackageJson +if ($packageJson -and $packageJson.version) { + $this.PackageProperties.Version = $packageJson.version +} <# From 194deda803e535823c54ebe5c54c63a27dd56ebf Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:27:59 -0800 Subject: [PATCH 163/724] feat: `OpenPackage.Part` Default Display ( Fixes #96 ) --- Types/OpenPackage.Part/DefaultDisplay.txt | 2 ++ Types/OpenPackage.Part/PSTypeNames.txt | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 Types/OpenPackage.Part/DefaultDisplay.txt create mode 100644 Types/OpenPackage.Part/PSTypeNames.txt diff --git a/Types/OpenPackage.Part/DefaultDisplay.txt b/Types/OpenPackage.Part/DefaultDisplay.txt new file mode 100644 index 0000000..8bff9b6 --- /dev/null +++ b/Types/OpenPackage.Part/DefaultDisplay.txt @@ -0,0 +1,2 @@ +Uri +ContentType \ No newline at end of file diff --git a/Types/OpenPackage.Part/PSTypeNames.txt b/Types/OpenPackage.Part/PSTypeNames.txt new file mode 100644 index 0000000..5990265 --- /dev/null +++ b/Types/OpenPackage.Part/PSTypeNames.txt @@ -0,0 +1,3 @@ +OP.Part +OpenPackage.Part +System.IO.Packaging.PackagePart \ No newline at end of file From 2ec85b4af22bf847cddd70390e6de481c1d68aa9 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:28:17 +0000 Subject: [PATCH 164/724] feat: `OpenPackage.Part` Default Display ( Fixes #96 ) --- OP.types.ps1xml | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index a3afb2b..5b7a02c 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5021,4 +5021,70 @@ if ($TypeMap.Count -eq 0) { + + OP.Part + + + PSStandardMembers + + + DefaultDisplayPropertySet + + Uri + ContentType + + + + + + DefaultDisplay + Uri +ContentType + + + + + OpenPackage.Part + + + PSStandardMembers + + + DefaultDisplayPropertySet + + Uri + ContentType + + + + + + DefaultDisplay + Uri +ContentType + + + + + System.IO.Packaging.PackagePart + + + PSStandardMembers + + + DefaultDisplayPropertySet + + Uri + ContentType + + + + + + DefaultDisplay + Uri +ContentType + + + \ No newline at end of file From 21a7308e49552e912eb4681f7140476ecab03661 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:29:15 -0800 Subject: [PATCH 165/724] feat: `OpenPackage.Part.get_Reader` ( Fixes #94 ) --- Types/OpenPackage.Part/get_Reader.ps1 | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 Types/OpenPackage.Part/get_Reader.ps1 diff --git a/Types/OpenPackage.Part/get_Reader.ps1 b/Types/OpenPackage.Part/get_Reader.ps1 new file mode 100644 index 0000000..dbc5e0f --- /dev/null +++ b/Types/OpenPackage.Part/get_Reader.ps1 @@ -0,0 +1,50 @@ +if ($this.'#Readers') { + return $this.'#Readers' +} +$readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { + if ($method.Name -notmatch 'Read.+?') { + continue + } + if (-not $method.Script) { continue } + $script = $method.Script + if (-not $script.Attributes) { continue } + $attributeData = [Ordered]@{} + foreach ($attribute in $script.Attributes) { + if ($attribute.Key -and $attribute.value) { + if (-not $attributeData[$attribute.key]) { + $attributeData[$attribute.key] = $attribute.value + } else { + $attributeData[$attribute.key] = @($attributeData[$attribute.key]) + $attribute.Value + } + } + } + + try { + if ($attributeData.Order -as [int]) { + Add-Member -InputObject $method -MemberType NoteProperty -Name Order -Value ( + $attributeData.Order -as [int] + ) -Force + + } + if ( + ( + $attributeData.FilePattern -and ( + $this.Uri -match $attributeData.FilePattern + ) + ) -or ( + $attributeData.ContentType -and ( + $this.ContentType -match $attributeData.ContentType + ) + ) + ) { + $method + } else { + continue nextMethod + } + } catch { + + } +} + +$this | Add-Member NoteProperty '#Readers' $readMethods -Force +return $this.'#Readers' \ No newline at end of file From fa59ea833829e4b6f3588a308d49c93838fc30aa Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:29:30 +0000 Subject: [PATCH 166/724] feat: `OpenPackage.Part.get_Reader` ( Fixes #94 ) --- OP.types.ps1xml | 165 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 5b7a02c..f48e732 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5036,6 +5036,61 @@ if ($TypeMap.Count -eq 0) { + + Reader + + if ($this.'#Readers') { + return $this.'#Readers' +} +$readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { + if ($method.Name -notmatch 'Read.+?') { + continue + } + if (-not $method.Script) { continue } + $script = $method.Script + if (-not $script.Attributes) { continue } + $attributeData = [Ordered]@{} + foreach ($attribute in $script.Attributes) { + if ($attribute.Key -and $attribute.value) { + if (-not $attributeData[$attribute.key]) { + $attributeData[$attribute.key] = $attribute.value + } else { + $attributeData[$attribute.key] = @($attributeData[$attribute.key]) + $attribute.Value + } + } + } + + try { + if ($attributeData.Order -as [int]) { + Add-Member -InputObject $method -MemberType NoteProperty -Name Order -Value ( + $attributeData.Order -as [int] + ) -Force + + } + if ( + ( + $attributeData.FilePattern -and ( + $this.Uri -match $attributeData.FilePattern + ) + ) -or ( + $attributeData.ContentType -and ( + $this.ContentType -match $attributeData.ContentType + ) + ) + ) { + $method + } else { + continue nextMethod + } + } catch { + + } +} + +$this | Add-Member NoteProperty '#Readers' $readMethods -Force +return $this.'#Readers' + + DefaultDisplay Uri @@ -5058,6 +5113,61 @@ ContentType + + Reader + + if ($this.'#Readers') { + return $this.'#Readers' +} +$readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { + if ($method.Name -notmatch 'Read.+?') { + continue + } + if (-not $method.Script) { continue } + $script = $method.Script + if (-not $script.Attributes) { continue } + $attributeData = [Ordered]@{} + foreach ($attribute in $script.Attributes) { + if ($attribute.Key -and $attribute.value) { + if (-not $attributeData[$attribute.key]) { + $attributeData[$attribute.key] = $attribute.value + } else { + $attributeData[$attribute.key] = @($attributeData[$attribute.key]) + $attribute.Value + } + } + } + + try { + if ($attributeData.Order -as [int]) { + Add-Member -InputObject $method -MemberType NoteProperty -Name Order -Value ( + $attributeData.Order -as [int] + ) -Force + + } + if ( + ( + $attributeData.FilePattern -and ( + $this.Uri -match $attributeData.FilePattern + ) + ) -or ( + $attributeData.ContentType -and ( + $this.ContentType -match $attributeData.ContentType + ) + ) + ) { + $method + } else { + continue nextMethod + } + } catch { + + } +} + +$this | Add-Member NoteProperty '#Readers' $readMethods -Force +return $this.'#Readers' + + DefaultDisplay Uri @@ -5080,6 +5190,61 @@ ContentType + + Reader + + if ($this.'#Readers') { + return $this.'#Readers' +} +$readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { + if ($method.Name -notmatch 'Read.+?') { + continue + } + if (-not $method.Script) { continue } + $script = $method.Script + if (-not $script.Attributes) { continue } + $attributeData = [Ordered]@{} + foreach ($attribute in $script.Attributes) { + if ($attribute.Key -and $attribute.value) { + if (-not $attributeData[$attribute.key]) { + $attributeData[$attribute.key] = $attribute.value + } else { + $attributeData[$attribute.key] = @($attributeData[$attribute.key]) + $attribute.Value + } + } + } + + try { + if ($attributeData.Order -as [int]) { + Add-Member -InputObject $method -MemberType NoteProperty -Name Order -Value ( + $attributeData.Order -as [int] + ) -Force + + } + if ( + ( + $attributeData.FilePattern -and ( + $this.Uri -match $attributeData.FilePattern + ) + ) -or ( + $attributeData.ContentType -and ( + $this.ContentType -match $attributeData.ContentType + ) + ) + ) { + $method + } else { + continue nextMethod + } + } catch { + + } +} + +$this | Add-Member NoteProperty '#Readers' $readMethods -Force +return $this.'#Readers' + + DefaultDisplay Uri From 551dc290c9d0626947570204cc57fb13f54b4e05 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:31:10 -0800 Subject: [PATCH 167/724] feat: `OpenPackage.Part.Read()` ( Fixes #95 ) --- Types/OpenPackage.Part/Read.ps1 | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Types/OpenPackage.Part/Read.ps1 diff --git a/Types/OpenPackage.Part/Read.ps1 b/Types/OpenPackage.Part/Read.ps1 new file mode 100644 index 0000000..830d746 --- /dev/null +++ b/Types/OpenPackage.Part/Read.ps1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS + Reads Open Package Part +.DESCRIPTION + Reads Open Package Parts, using the `.Reader` associated with this part. +#> +filter addPackageAndPart { + $_ | + Add-Member NoteProperty Package $this.Package -Force -PassThru | + Add-Member NoteProperty PartUri $this.Uri -Force -PassThru +} + +$readMethods = @($this.Reader) + +$orderedMethods = @($readMethods| Sort-Object Order) + +if (-not $orderedMethods) { + Write-Warning "No reader found for $($this.Uri)" + return +} +$method = $orderedMethods[0] +return $( + $method.Invoke() | addPackageAndPart +) + + From bcf68608b3ef88d42b3833bc7c74b7e6f1ae66a3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:31:27 +0000 Subject: [PATCH 168/724] feat: `OpenPackage.Part.Read()` ( Fixes #95 ) --- OP.types.ps1xml | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f48e732..87eada1 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5036,6 +5036,38 @@ if ($TypeMap.Count -eq 0) { + + Read + + Reader @@ -5113,6 +5145,38 @@ ContentType + + Read + + Reader @@ -5190,6 +5254,38 @@ ContentType + + Read + + Reader From 485498ebb08446736cc75c7ddaa3d87699b30db9 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:33:22 -0800 Subject: [PATCH 169/724] feat: `OpenPackage.Part.ReadText()` ( Fixes #97 ) --- Types/OpenPackage.Part/ReadText.ps1 | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadText.ps1 diff --git a/Types/OpenPackage.Part/ReadText.ps1 b/Types/OpenPackage.Part/ReadText.ps1 new file mode 100644 index 0000000..433c20c --- /dev/null +++ b/Types/OpenPackage.Part/ReadText.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Reads Part Content as Text +.DESCRIPTION + Reads Package Part Content as Text +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to .txt files + 'FilePattern', + '\.txt?$' +)] +[Reflection.AssemblyMetadata( + # This should automatically apply to any text/ content types + 'ContentTypePattern', + '^text/' +)] +[Reflection.AssemblyMetadata( + # This has a higher order, indicating it should be run later than most. + 'Order', + 100 +)] +param([IO.Packaging.PackagePart]$Part = $this) + +# If there is no part, return +if (-not $part) { return } + +$partStream = $part.GetStream() + +$streamReader = [IO.StreamReader]::new($partStream, $true) + +$partText = $streamReader.ReadToEnd() + +$partStream.Close() +$partStream.Dispose() + +$streamReader.Close() +$streamReader.Dispose() + +$partText \ No newline at end of file From f88c65098acdabedfdface97b0496e86e232adee Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:33:40 +0000 Subject: [PATCH 170/724] feat: `OpenPackage.Part.ReadText()` ( Fixes #97 ) --- OP.types.ps1xml | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 87eada1..8b11861 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5066,6 +5066,50 @@ return $( + + + + ReadText + @@ -5175,6 +5219,50 @@ return $( + + + + ReadText + @@ -5284,6 +5372,50 @@ return $( + + + + ReadText + From b27bcdafddadbd35bb2fd866943c244933f5ad4c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:35:34 -0800 Subject: [PATCH 171/724] feat: `OpenPackage.Part.ReadXml()` ( Fixes #98 ) --- Types/OpenPackage.Part/ReadXML.ps1 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadXML.ps1 diff --git a/Types/OpenPackage.Part/ReadXML.ps1 b/Types/OpenPackage.Part/ReadXML.ps1 new file mode 100644 index 0000000..0c060e8 --- /dev/null +++ b/Types/OpenPackage.Part/ReadXML.ps1 @@ -0,0 +1,20 @@ +<# +.SYNOPSIS + Reads Part Content XML +.DESCRIPTION + Reads an OpenPackage Part's Content as XML +#> +[Reflection.AssemblyMetadata( + 'FilePattern', '\.(?>svg|ps1xml|xml|xhtml)?$' +)] +[Reflection.AssemblyMetadata( + 'ContentTypePattern', '[/\+]xml?$' +)] +param([IO.Packaging.PackagePart]$Part = $this) + +if (-not $part) { return } +$partText = $part.ReadText($part) + +return [xml]$partText + + From 017359c82560656f6b659a000603b98be2f54f74 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:35:55 +0000 Subject: [PATCH 172/724] feat: `OpenPackage.Part.ReadXml()` ( Fixes #98 ) --- OP.types.ps1xml | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 8b11861..9483df2 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5110,6 +5110,32 @@ $streamReader.Close() $streamReader.Dispose() $partText + + + + ReadXML + @@ -5263,6 +5289,32 @@ $streamReader.Close() $streamReader.Dispose() $partText + + + + ReadXML + @@ -5416,6 +5468,32 @@ $streamReader.Close() $streamReader.Dispose() $partText + + + + ReadXML + From 18109097d493fdb2a3c165a196d127c83f5b6f81 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:38:02 -0800 Subject: [PATCH 173/724] feat: `OpenPackage.Part.ReadJson()` ( Fixes #99 ) --- Types/OpenPackage.Part/ReadJson.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadJson.ps1 diff --git a/Types/OpenPackage.Part/ReadJson.ps1 b/Types/OpenPackage.Part/ReadJson.ps1 new file mode 100644 index 0000000..0b1d2f4 --- /dev/null +++ b/Types/OpenPackage.Part/ReadJson.ps1 @@ -0,0 +1,16 @@ +<# +.SYNOPSIS + Reads Part Content as Json +.DESCRIPTION + Reads Open Package Part Content as Json +#> +[Reflection.AssemblyMetadata('FilePattern', '\.jsonc?$')] +[Reflection.AssemblyMetadata('ContenTypePattern', '[/\+]jsonc?$')] +param([IO.Packaging.PackagePart]$Part = $this) + +if (-not $part) { return } +$partText = $part.ReadText($part) + +ConvertFrom-Json -InputObject $partText + + From 2fa8b8cf87a41318ae5c09ab73feef60e1800eac Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:38:22 +0000 Subject: [PATCH 174/724] feat: `OpenPackage.Part.ReadJson()` ( Fixes #99 ) --- OP.types.ps1xml | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9483df2..e830a12 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5066,6 +5066,28 @@ return $( + + + + ReadJson + @@ -5245,6 +5267,28 @@ return $( + + + + ReadJson + @@ -5424,6 +5468,28 @@ return $( + + + + ReadJson + From 834eda9adc42480278fbb548d7e4f3f8e6269bc4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:40:14 -0800 Subject: [PATCH 175/724] feat: `OpenPackage.Part.ReadPowerShell()` ( Fixes #100 ) --- Types/OpenPackage.Part/ReadPowerShell.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadPowerShell.ps1 diff --git a/Types/OpenPackage.Part/ReadPowerShell.ps1 b/Types/OpenPackage.Part/ReadPowerShell.ps1 new file mode 100644 index 0000000..479a762 --- /dev/null +++ b/Types/OpenPackage.Part/ReadPowerShell.ps1 @@ -0,0 +1,16 @@ +<# +.SYNOPSIS + Reads Part Content as PowerShell +.DESCRIPTION + Reads Open Package Part Content as PowerShell +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to `.ps1` and `.psm1` files + 'FilePattern', + '\.psm?1$' +)] +param([IO.Packaging.PackagePart]$part = $this ) + +[ScriptBlock]::Create($part.ReadText($part)) + + From 678f3dd3a71ca281ac33e6db5d7eccc6fe02730a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:40:33 +0000 Subject: [PATCH 176/724] feat: `OpenPackage.Part.ReadPowerShell()` ( Fixes #100 ) --- OP.types.ps1xml | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index e830a12..584259f 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5088,6 +5088,28 @@ ConvertFrom-Json -InputObject $partText + + + + ReadPowerShell + @@ -5289,6 +5311,28 @@ ConvertFrom-Json -InputObject $partText + + + + ReadPowerShell + @@ -5490,6 +5534,28 @@ ConvertFrom-Json -InputObject $partText + + + + ReadPowerShell + From da01b17d26398a825c361fe63c14dddc52cc3b7f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:45:37 -0800 Subject: [PATCH 177/724] feat: `OpenPackage.Part.ReadPowerShellData()` ( Fixes #101 ) --- Types/OpenPackage.Part/ReadPowerShellData.ps1 | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadPowerShellData.ps1 diff --git a/Types/OpenPackage.Part/ReadPowerShellData.ps1 b/Types/OpenPackage.Part/ReadPowerShellData.ps1 new file mode 100644 index 0000000..17079e8 --- /dev/null +++ b/Types/OpenPackage.Part/ReadPowerShellData.ps1 @@ -0,0 +1,27 @@ +<# +.SYNOPSIS + Reads Part Content as PowerShell Data +.DESCRIPTION + Reads Package Part Content as a PowerShell Data block +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to .txt files + 'FilePattern', + '\.psd1$' +)] +param([IO.Packaging.PackagePart]$part = $this ) + +$datablock = [ScriptBlock]::Create("data {$($part.ReadText($part)) }") +if ($datablock.Ast.EndBlock.Statements.Count -gt 1) { return } +if ($datablock.Ast.EndBlock.Statements[0] -isnot + [Management.Automation.Language.DataStatementAst] +) { + return +} +if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { + return +} +& $datablock + + + From 7d059fa809fd4369b870ca425385065966977694 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:46:10 +0000 Subject: [PATCH 178/724] feat: `OpenPackage.Part.ReadPowerShellData()` ( Fixes #101 ) --- OP.types.ps1xml | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 584259f..ed3863f 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5110,6 +5110,39 @@ param([IO.Packaging.PackagePart]$part = $this ) + + + + ReadPowerShellData + @@ -5333,6 +5366,39 @@ param([IO.Packaging.PackagePart]$part = $this ) + + + + ReadPowerShellData + @@ -5556,6 +5622,39 @@ param([IO.Packaging.PackagePart]$part = $this ) + + + + ReadPowerShellData + From b3d313884657c79cad935beb18021eef053c8629 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:46:54 -0800 Subject: [PATCH 179/724] feat: `OpenPackage.Part` Default Display ( Fixes #96 ) Adding Reader --- Types/OpenPackage.Part/DefaultDisplay.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/DefaultDisplay.txt b/Types/OpenPackage.Part/DefaultDisplay.txt index 8bff9b6..a79dd72 100644 --- a/Types/OpenPackage.Part/DefaultDisplay.txt +++ b/Types/OpenPackage.Part/DefaultDisplay.txt @@ -1,2 +1,3 @@ Uri -ContentType \ No newline at end of file +ContentType +Reader \ No newline at end of file From 099344f355c4b2bd7d6f8faa798639d7f5574338 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:47:24 +0000 Subject: [PATCH 180/724] feat: `OpenPackage.Part` Default Display ( Fixes #96 ) Adding Reader --- OP.types.ps1xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index ed3863f..081eee3 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5032,6 +5032,7 @@ if ($TypeMap.Count -eq 0) { Uri ContentType + Reader @@ -5273,7 +5274,8 @@ return $this.'#Readers' DefaultDisplay Uri -ContentType +ContentType +Reader
@@ -5288,6 +5290,7 @@ ContentType Uri ContentType + Reader @@ -5529,7 +5532,8 @@ return $this.'#Readers' DefaultDisplay Uri -ContentType +ContentType +Reader @@ -5544,6 +5548,7 @@ ContentType Uri ContentType + Reader @@ -5785,7 +5790,8 @@ return $this.'#Readers' DefaultDisplay Uri -ContentType +ContentType +Reader From c6f820889cc1433701253470fa1cbe649a252439 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:48:29 -0800 Subject: [PATCH 181/724] feat: `OpenPackage.Part.ReadClixml()` ( Fixes #102 ) --- Types/OpenPackage.Part/ReadCliXml.ps1 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadCliXml.ps1 diff --git a/Types/OpenPackage.Part/ReadCliXml.ps1 b/Types/OpenPackage.Part/ReadCliXml.ps1 new file mode 100644 index 0000000..8b27862 --- /dev/null +++ b/Types/OpenPackage.Part/ReadCliXml.ps1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS + Reads Part Content as Clixml +.DESCRIPTION + Reads Open Package Part Content as Clixml +#> +[Reflection.AssemblyMetadata('FilePattern', '\.(?>clixml|clix)?$')] +param([IO.Packaging.PackagePart]$Part = $this) + +if (-not $part) { return } +$partText = $part.ReadText($part) + +return [Management.Automation.PSSerializer]::Deserialize($partText) + + From 1b248a231237ff879c874f2c489f3c90fa496f76 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:48:53 +0000 Subject: [PATCH 182/724] feat: `OpenPackage.Part.ReadClixml()` ( Fixes #102 ) --- OP.types.ps1xml | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 081eee3..0d38493 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5067,6 +5067,27 @@ return $( + + + + ReadCliXml + @@ -5325,6 +5346,27 @@ return $( + + + + ReadCliXml + @@ -5583,6 +5625,27 @@ return $( + + + + ReadCliXml + From aefdb2a2eca6619755aead272ded175f7524b1da Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:49:55 -0800 Subject: [PATCH 183/724] feat: `OpenPackage.Part.ReadCSharp()` ( Fixes #103 ) --- Types/OpenPackage.Part/ReadCSharp.ps1 | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadCSharp.ps1 diff --git a/Types/OpenPackage.Part/ReadCSharp.ps1 b/Types/OpenPackage.Part/ReadCSharp.ps1 new file mode 100644 index 0000000..002d27a --- /dev/null +++ b/Types/OpenPackage.Part/ReadCSharp.ps1 @@ -0,0 +1,21 @@ +<# +.SYNOPSIS + Reads Part Content as CSharp +.DESCRIPTION + Reads Package Part Content as CSharp code +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to .cs files + 'FilePattern', + '\.cs$' +)] +param() +$partString = $Part.ReadText($part) +$null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru + +if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { + $partString | + Add-Member NoteProperty SyntaxTree ( + [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) + ) -Force -PassThru +} \ No newline at end of file From 8a9a901c200f954ebf667314aa6aedbef5bcb40f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:50:14 +0000 Subject: [PATCH 184/724] feat: `OpenPackage.Part.ReadCSharp()` ( Fixes #103 ) --- OP.types.ps1xml | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0d38493..2d9fa84 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5088,6 +5088,32 @@ return [Management.Automation.PSSerializer]::Deserialize($partText) + + + + ReadCSharp + @@ -5367,6 +5393,32 @@ return [Management.Automation.PSSerializer]::Deserialize($partText) + + + + ReadCSharp + @@ -5646,6 +5698,32 @@ return [Management.Automation.PSSerializer]::Deserialize($partText) + + + + ReadCSharp + From fe175103372613e139b7e0c8985af903581b2c87 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:54:15 -0800 Subject: [PATCH 185/724] feat: `OpenPackage.Part.ReadJsonL()` ( Fixes #104 ) --- Types/OpenPackage.Part/ReadJsonL.ps1 | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadJsonL.ps1 diff --git a/Types/OpenPackage.Part/ReadJsonL.ps1 b/Types/OpenPackage.Part/ReadJsonL.ps1 new file mode 100644 index 0000000..a7e2726 --- /dev/null +++ b/Types/OpenPackage.Part/ReadJsonL.ps1 @@ -0,0 +1,17 @@ +<# +.SYNOPSIS + Reads Part Content as Json Lines +.DESCRIPTION + Reads Open Package Part Content as Json Lines +#> +[Reflection.AssemblyMetadata( + 'FilePattern', + '\.(?>cast|jsonl)?$' +)] +param([IO.Packaging.PackagePart]$Part = $this) + +if (-not $part) { return } +$partText = $part.ReadText($part) +$partText -split '(?>\r\n|\n)' | ConvertFrom-Json + + From a0a50c812536822ed060da74e8777c516decc7c1 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:54:27 +0000 Subject: [PATCH 186/724] feat: `OpenPackage.Part.ReadJsonL()` ( Fixes #104 ) --- OP.types.ps1xml | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 2d9fa84..8c5aedd 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5136,6 +5136,29 @@ ConvertFrom-Json -InputObject $partText + + + + ReadJsonL + @@ -5441,6 +5464,29 @@ ConvertFrom-Json -InputObject $partText + + + + ReadJsonL + @@ -5746,6 +5792,29 @@ ConvertFrom-Json -InputObject $partText + + + + ReadJsonL + From b033337b435ea77673affb32245af341edd018f4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:55:37 -0800 Subject: [PATCH 187/724] feat: `OpenPackage.Part.ReadMarkdown()` ( Fixes #105 ) --- Types/OpenPackage.Part/ReadMarkdown.ps1 | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadMarkdown.ps1 diff --git a/Types/OpenPackage.Part/ReadMarkdown.ps1 b/Types/OpenPackage.Part/ReadMarkdown.ps1 new file mode 100644 index 0000000..b48ddda --- /dev/null +++ b/Types/OpenPackage.Part/ReadMarkdown.ps1 @@ -0,0 +1,33 @@ +<# +.SYNOPSIS + Reads Part Content as Markdown +.DESCRIPTION + Reads Package Part Content as Markdown +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to .md or .markdown files + 'FilePattern', + '\.(?>md|markdown)$' +)] +[Reflection.AssemblyMetadata( + # This should automatically apply to markdown content types + 'ContentTypePattern', + '[/\+]markdown' +)] +param([IO.Packaging.PackagePart]$Part = $this) +$convertFromMarkdownCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Markdown', 'Cmdlet,Function') +$partString = $Part.ReadText() +if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parameters.InputObject) { + Write-Warning "ConvertFrom-Markdown not found" + $partString = [PSObject]::new($partString) + $partString.pstypenames.insert(0, 'text/markdown') + $partString +} else { + try { + $partString | + & $convertFromMarkdownCommand -ErrorAction Stop | + Add-Member NoteProperty Markdown "$partString" -Force -PassThru + } catch { + Write-Warning "'$($thisPart.Uri)' was not valid markdown: $_" + } +} \ No newline at end of file From 66a91a066d303df4ebf576d484659ed835f3bbcc Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:55:58 +0000 Subject: [PATCH 188/724] feat: `OpenPackage.Part.ReadMarkdown()` ( Fixes #105 ) --- OP.types.ps1xml | 114 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 8c5aedd..db63be2 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5159,6 +5159,44 @@ $partText -split '(?>\r\n|\n)' | ConvertFrom-Json + + + + ReadMarkdown + @@ -5487,6 +5525,44 @@ $partText -split '(?>\r\n|\n)' | ConvertFrom-Json + + + + ReadMarkdown + @@ -5815,6 +5891,44 @@ $partText -split '(?>\r\n|\n)' | ConvertFrom-Json + + + + ReadMarkdown + From f6e280f1ea59791a52bcd8b10cd60e0ad1ebe3c4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 13:59:14 -0800 Subject: [PATCH 189/724] feat: `OpenPackage.Part.get_Reader` ( Fixes #94 ) Improving content type matches and sorting --- Types/OpenPackage.Part/Read.ps1 | 4 +--- Types/OpenPackage.Part/get_Reader.ps1 | 10 ++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Types/OpenPackage.Part/Read.ps1 b/Types/OpenPackage.Part/Read.ps1 index 830d746..3d52e7f 100644 --- a/Types/OpenPackage.Part/Read.ps1 +++ b/Types/OpenPackage.Part/Read.ps1 @@ -10,9 +10,7 @@ filter addPackageAndPart { Add-Member NoteProperty PartUri $this.Uri -Force -PassThru } -$readMethods = @($this.Reader) - -$orderedMethods = @($readMethods| Sort-Object Order) +$orderedMethods = @($this.Reader) if (-not $orderedMethods) { Write-Warning "No reader found for $($this.Uri)" diff --git a/Types/OpenPackage.Part/get_Reader.ps1 b/Types/OpenPackage.Part/get_Reader.ps1 index dbc5e0f..d80cc21 100644 --- a/Types/OpenPackage.Part/get_Reader.ps1 +++ b/Types/OpenPackage.Part/get_Reader.ps1 @@ -1,7 +1,7 @@ if ($this.'#Readers') { return $this.'#Readers' } -$readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { +$readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { if ($method.Name -notmatch 'Read.+?') { continue } @@ -32,8 +32,8 @@ $readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { $this.Uri -match $attributeData.FilePattern ) ) -or ( - $attributeData.ContentType -and ( - $this.ContentType -match $attributeData.ContentType + $attributeData.ContentTypePattern -and ( + $this.ContentType -match $attributeData.ContentTypePattern ) ) ) { @@ -44,7 +44,9 @@ $readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { } catch { } -} +}) + +$readMethods = @($readMethods | Sort-Object Order) $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' \ No newline at end of file From 74dcc55b2e97b3831ecf5bef78a5279f685a8cd4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 21:59:32 +0000 Subject: [PATCH 190/724] feat: `OpenPackage.Part.get_Reader` ( Fixes #94 ) Improving content type matches and sorting --- OP.types.ps1xml | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index db63be2..56f8354 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5052,9 +5052,7 @@ filter addPackageAndPart { Add-Member NoteProperty PartUri $this.Uri -Force -PassThru } -$readMethods = @($this.Reader) - -$orderedMethods = @($readMethods| Sort-Object Order) +$orderedMethods = @($this.Reader) if (-not $orderedMethods) { Write-Warning "No reader found for $($this.Uri)" @@ -5330,7 +5328,7 @@ return [xml]$partText if ($this.'#Readers') { return $this.'#Readers' } -$readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { +$readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { if ($method.Name -notmatch 'Read.+?') { continue } @@ -5361,8 +5359,8 @@ $readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { $this.Uri -match $attributeData.FilePattern ) ) -or ( - $attributeData.ContentType -and ( - $this.ContentType -match $attributeData.ContentType + $attributeData.ContentTypePattern -and ( + $this.ContentType -match $attributeData.ContentTypePattern ) ) ) { @@ -5373,7 +5371,9 @@ $readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { } catch { } -} +}) + +$readMethods = @($readMethods | Sort-Object Order) $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' @@ -5418,9 +5418,7 @@ filter addPackageAndPart { Add-Member NoteProperty PartUri $this.Uri -Force -PassThru } -$readMethods = @($this.Reader) - -$orderedMethods = @($readMethods| Sort-Object Order) +$orderedMethods = @($this.Reader) if (-not $orderedMethods) { Write-Warning "No reader found for $($this.Uri)" @@ -5696,7 +5694,7 @@ return [xml]$partText if ($this.'#Readers') { return $this.'#Readers' } -$readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { +$readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { if ($method.Name -notmatch 'Read.+?') { continue } @@ -5727,8 +5725,8 @@ $readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { $this.Uri -match $attributeData.FilePattern ) ) -or ( - $attributeData.ContentType -and ( - $this.ContentType -match $attributeData.ContentType + $attributeData.ContentTypePattern -and ( + $this.ContentType -match $attributeData.ContentTypePattern ) ) ) { @@ -5739,7 +5737,9 @@ $readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { } catch { } -} +}) + +$readMethods = @($readMethods | Sort-Object Order) $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' @@ -5784,9 +5784,7 @@ filter addPackageAndPart { Add-Member NoteProperty PartUri $this.Uri -Force -PassThru } -$readMethods = @($this.Reader) - -$orderedMethods = @($readMethods| Sort-Object Order) +$orderedMethods = @($this.Reader) if (-not $orderedMethods) { Write-Warning "No reader found for $($this.Uri)" @@ -6062,7 +6060,7 @@ return [xml]$partText if ($this.'#Readers') { return $this.'#Readers' } -$readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { +$readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { if ($method.Name -notmatch 'Read.+?') { continue } @@ -6093,8 +6091,8 @@ $readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { $this.Uri -match $attributeData.FilePattern ) ) -or ( - $attributeData.ContentType -and ( - $this.ContentType -match $attributeData.ContentType + $attributeData.ContentTypePattern -and ( + $this.ContentType -match $attributeData.ContentTypePattern ) ) ) { @@ -6105,7 +6103,9 @@ $readMethods = :nextMethod foreach ($method in $this.PSObject.Methods) { } catch { } -} +}) + +$readMethods = @($readMethods | Sort-Object Order) $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' From 4a8513876c5b828d849d22dda1379144823be102 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 14:06:56 -0800 Subject: [PATCH 191/724] feat: `OpenPackage.Part.get_Reader` ( Fixes #94 ) Improving content type matches and sorting --- Types/OpenPackage.Part/get_Reader.ps1 | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Types/OpenPackage.Part/get_Reader.ps1 b/Types/OpenPackage.Part/get_Reader.ps1 index d80cc21..a587687 100644 --- a/Types/OpenPackage.Part/get_Reader.ps1 +++ b/Types/OpenPackage.Part/get_Reader.ps1 @@ -19,13 +19,7 @@ $readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { } } - try { - if ($attributeData.Order -as [int]) { - Add-Member -InputObject $method -MemberType NoteProperty -Name Order -Value ( - $attributeData.Order -as [int] - ) -Force - - } + try { if ( ( $attributeData.FilePattern -and ( @@ -46,7 +40,14 @@ $readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { } }) -$readMethods = @($readMethods | Sort-Object Order) +$readMethods = @($readMethods | Sort-Object { + $in = $_ + foreach ($attr in $in.Attributes) { + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + $attr.Value -as [int] + } + } +}) $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' \ No newline at end of file From 5c8b4b367f7224c3b248301a342bbc2841ce43b7 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 22:07:15 +0000 Subject: [PATCH 192/724] feat: `OpenPackage.Part.get_Reader` ( Fixes #94 ) Improving content type matches and sorting --- OP.types.ps1xml | 51 ++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 56f8354..53b6911 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5346,13 +5346,7 @@ $readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { } } - try { - if ($attributeData.Order -as [int]) { - Add-Member -InputObject $method -MemberType NoteProperty -Name Order -Value ( - $attributeData.Order -as [int] - ) -Force - - } + try { if ( ( $attributeData.FilePattern -and ( @@ -5373,7 +5367,14 @@ $readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { } }) -$readMethods = @($readMethods | Sort-Object Order) +$readMethods = @($readMethods | Sort-Object { + $in = $_ + foreach ($attr in $in.Attributes) { + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + $attr.Value -as [int] + } + } +}) $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' @@ -5712,13 +5713,7 @@ $readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { } } - try { - if ($attributeData.Order -as [int]) { - Add-Member -InputObject $method -MemberType NoteProperty -Name Order -Value ( - $attributeData.Order -as [int] - ) -Force - - } + try { if ( ( $attributeData.FilePattern -and ( @@ -5739,7 +5734,14 @@ $readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { } }) -$readMethods = @($readMethods | Sort-Object Order) +$readMethods = @($readMethods | Sort-Object { + $in = $_ + foreach ($attr in $in.Attributes) { + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + $attr.Value -as [int] + } + } +}) $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' @@ -6078,13 +6080,7 @@ $readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { } } - try { - if ($attributeData.Order -as [int]) { - Add-Member -InputObject $method -MemberType NoteProperty -Name Order -Value ( - $attributeData.Order -as [int] - ) -Force - - } + try { if ( ( $attributeData.FilePattern -and ( @@ -6105,7 +6101,14 @@ $readMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { } }) -$readMethods = @($readMethods | Sort-Object Order) +$readMethods = @($readMethods | Sort-Object { + $in = $_ + foreach ($attr in $in.Attributes) { + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + $attr.Value -as [int] + } + } +}) $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' From 6c9bf64b2957a24576780c4b9a2a31f55f052873 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 14:09:17 -0800 Subject: [PATCH 193/724] feat: `OpenPackage.Part.ReadToml()` ( Fixes #106 ) --- Types/OpenPackage.Part/ReadToml.ps1 | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadToml.ps1 diff --git a/Types/OpenPackage.Part/ReadToml.ps1 b/Types/OpenPackage.Part/ReadToml.ps1 new file mode 100644 index 0000000..83d13ca --- /dev/null +++ b/Types/OpenPackage.Part/ReadToml.ps1 @@ -0,0 +1,27 @@ +<# +.SYNOPSIS + Reads Part Content as Toml +.DESCRIPTION + Reads Package Part Content as Toml +.LINK + https://toml.io/ +.LINK + https://github.com/jborean93/PSToml +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to .yaml files + 'FilePattern', + '\.toml$' +)] +param([IO.Packaging.PackagePart]$Part = $this) +$convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') +$partString = $Part.ReadText($Part) +if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { + Write-Warning "ConvertFrom-Toml not found, please install PSToml" +} else { + try { + $partString | & $convertFromTomlCommand -ErrorAction Stop + } catch { + Write-Warning "'$($thisPart.Uri)' was not valid toml: $_" + } +} \ No newline at end of file From f1ef53275d954955d82e37692a7d4d8dd7d586f4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 22:09:34 +0000 Subject: [PATCH 194/724] feat: `OpenPackage.Part.ReadToml()` ( Fixes #106 ) --- OP.types.ps1xml | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 53b6911..a308803 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5296,6 +5296,38 @@ $streamReader.Dispose() $partText + + ReadToml + + ReadXML + + ReadToml + + ReadXML + + ReadToml + + ReadXML + + + ReadYaml + @@ -5751,6 +5783,38 @@ return [xml]$partText + + + + ReadYaml + @@ -6150,6 +6214,38 @@ return [xml]$partText + + + + ReadYaml + From 9ebfaab890d656619945d28e965036e4f0359efd Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 14:14:17 -0800 Subject: [PATCH 197/724] feat: `OpenPackage.Part.ReadFormData()` ( Fixes #108 ) --- Types/OpenPackage.Part/ReadFormData.ps1 | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadFormData.ps1 diff --git a/Types/OpenPackage.Part/ReadFormData.ps1 b/Types/OpenPackage.Part/ReadFormData.ps1 new file mode 100644 index 0000000..1c50587 --- /dev/null +++ b/Types/OpenPackage.Part/ReadFormData.ps1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS + Reads Part Content as Form Data +.DESCRIPTION + Reads OpenPackage Part Content as Form Data +#> +[Reflection.AssemblyMetadata( + 'ContentTypePattern', + '[/\+]x-www-form-urlencoded' +)] +param([IO.Packaging.PackagePart]$Part = $this) + +if (-not $part) { return } +$partText = $part.ReadText($part) + +$formData = [Web.HttpUtility]::ParseQueryString($partText) +$formDataObject = [Ordered]@{} +foreach ($key in $formData.Keys) { + if ($null -eq $formDataObject[$key]) { + $formDataObject[$key] = $formData[$key] + } else { + $formDataObject[$key] = @($formDataObject[$key]) + $formData[$key] + } +} +return $formDataObject + From 1d1630ee58d6d6b676ab23d7800196047e8e30e9 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 22:14:35 +0000 Subject: [PATCH 198/724] feat: `OpenPackage.Part.ReadFormData()` ( Fixes #108 ) --- OP.types.ps1xml | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 1eeab8c..f7e29b3 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5114,6 +5114,38 @@ if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { } + + ReadFormData + + ReadJson + + ReadFormData + + ReadJson + + ReadFormData + + ReadJson + + + ReadXsd + @@ -5848,6 +5887,45 @@ return [xml]$partText + + + + ReadXsd + @@ -6312,6 +6390,45 @@ return [xml]$partText + + + + ReadXsd + From 47bc877d7c50b35848fd027341d2a4923c96d76a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 1 Mar 2026 16:40:52 -0800 Subject: [PATCH 203/724] feat: `OpenPackage.Part.ReadXslt` ( Fixes #110 ) --- Types/OpenPackage.Part/ReadXslt.ps1 | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Types/OpenPackage.Part/ReadXslt.ps1 diff --git a/Types/OpenPackage.Part/ReadXslt.ps1 b/Types/OpenPackage.Part/ReadXslt.ps1 new file mode 100644 index 0000000..ca4f81e --- /dev/null +++ b/Types/OpenPackage.Part/ReadXslt.ps1 @@ -0,0 +1,34 @@ +<# +.SYNOPSIS + Reads Part Content XSLT +.DESCRIPTION + Reads an OpenPackage Part's Content as XSLT, or eXtensible Stylesheet Language Transforms +#> +[Reflection.AssemblyMetadata( + 'FilePattern', '\.xslt?$' +)] +[Reflection.AssemblyMetadata( + 'ContentTypePattern', '[/\+]xslt?$' +)] + +param([IO.Packaging.PackagePart]$Part = $this) + +if (-not $part) { return } + +try { + $partStream = $part.GetStream() + $xmlReader = [Xml.XmlReader]::Create($partStream) + $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() + $xslTransformer.Load($xmlReader) + $xslTransformer +} catch { + $_ +} finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } + + $partStream.Close() + $partStream.Dispose() +} \ No newline at end of file From a0bc0fc64b463c21e77afee20716ecd7293bf104 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 2 Mar 2026 00:41:21 +0000 Subject: [PATCH 204/724] feat: `OpenPackage.Part.ReadXslt` ( Fixes #110 ) --- OP.types.ps1xml | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 1901619..d2d3857 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5420,6 +5420,45 @@ try { $xmlReader.Dispose() } + $partStream.Close() + $partStream.Dispose() +} + + + + ReadXslt + + + + ReadXslt + + + + ReadXslt + + + + GetNuget + @@ -1875,6 +1922,53 @@ $matchingParts = } else { $partBytes } +} + + + + GetNuget + @@ -3500,6 +3594,53 @@ $matchingParts = } else { $partBytes } +} + + + + GetNuget + From abfa2a7dc58ea217111712125638f73229ab46a5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 11:06:43 -0800 Subject: [PATCH 210/724] feat: `OpenPackage.GetAtBlob` ( Fixes #112 ) --- Types/OpenPackage/GetAtBlob.ps1 | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Types/OpenPackage/GetAtBlob.ps1 diff --git a/Types/OpenPackage/GetAtBlob.ps1 b/Types/OpenPackage/GetAtBlob.ps1 new file mode 100644 index 0000000..46810ec --- /dev/null +++ b/Types/OpenPackage/GetAtBlob.ps1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS + Gets at blobs +.DESCRIPTION + Gets blobs from the at protocol. +#> +param( +# The decentralized identifier (did) +[Parameter(Mandatory)] +[string] +$did, + +# The content identifier (cid) +[Parameter(Mandatory)] +[string] +$cid, + +# The personal data server (pds) +[string] +$pds = "https://bsky.social/" +) + + +return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" + + From aae71ffc9476cc5cdcb7aaa1f8d069c469043ccf Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 19:07:32 +0000 Subject: [PATCH 211/724] feat: `OpenPackage.GetAtBlob` ( Fixes #112 ) --- OP.types.ps1xml | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f17f701..013f7c3 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -27,6 +27,38 @@ RemovePart DeletePart + + GetAtBlob + + GetContent + GetContent + GetContent + + + GetAtRecord + @@ -1761,6 +1804,49 @@ return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did& + + + + GetAtRecord + @@ -3465,6 +3551,49 @@ return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did& + + + + GetAtRecord + From 7ce604159df6bf25feeace9676c61c8704cf851a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 11:09:22 -0800 Subject: [PATCH 214/724] feat: `OpenPackage.GetAtType` ( Fixes #114 ) --- Types/OpenPackage/GetAtType.ps1 | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 Types/OpenPackage/GetAtType.ps1 diff --git a/Types/OpenPackage/GetAtType.ps1 b/Types/OpenPackage/GetAtType.ps1 new file mode 100644 index 0000000..1e6d8ce --- /dev/null +++ b/Types/OpenPackage/GetAtType.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + Gets at records +.DESCRIPTION + Gets records from the at protocol. +.EXAMPLE + Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile +#> +param( +[Parameter(Mandatory)] +[string] +$did, + +[Parameter(Mandatory)] +[string] +$collection, + +[ValidateRange(1,100)] +[int] +$BatchSize = 100, + +[long] +$First, + +[long] +$Skip, + +[string] +$pds = "https://bsky.social/" +) + +$atPattern = 'at://(?[^/]+)/(?[^/]+)(?:/(?.+?$))?' + +$total = [long]0 +$skipped = [long]0 +$cursor = '' +$progress = [Ordered]@{Id = Get-Random} +$progress.Status = "Getting records" +:AtSync do { + $xrpcUrl = "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=$( + $did + )&collection=$( + $collection + )&limit=$BatchSize&cursor=$Cursor" + $progress.Activity = "$total " + Write-Progress @progress + # Get the page of records + $results = Invoke-RestMethod $xrpcUrl + # If we got results and have a cursor to more + if ($results -and $results.cursor) { + # set it for the next round. + $Cursor = $results.cursor + } + + # Unroll and store each record. + :nextRecord foreach ($record in $results.records) { + + # Records are sent latest to earliest. + # We can use -Skip to skip N records + if ($Skip -and + $skipped -lt $Skip + ) { + $skipped++ + continue nextRecord + } + + # If the uri is not an at uri + if ($record.uri -notmatch $atPattern) { + # continue to the next record + continue nextRecord + } + + $record + + # Increment our total + $total++ + + # If we provided -First and our total exceeds our -First, break out + if ($First -and + $total -ge $First) { + break AtSync + } + } +} while ($results -and $results.cursor) + From 08afd9e766130adaf341417cf40e84743d48211f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 19:09:50 +0000 Subject: [PATCH 215/724] feat: `OpenPackage.GetAtType` ( Fixes #114 ) --- OP.types.ps1xml | 273 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index eba77f1..f1d6f7b 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -100,6 +100,97 @@ $xrpcUrl = "$pds/xrpc/com.atproto.repo.getRecord?repo=$( Invoke-RestMethod -Uri $xrpcUrl + + + + GetAtType + @@ -1847,6 +1938,97 @@ $xrpcUrl = "$pds/xrpc/com.atproto.repo.getRecord?repo=$( Invoke-RestMethod -Uri $xrpcUrl + + + + GetAtType + @@ -3594,6 +3776,97 @@ $xrpcUrl = "$pds/xrpc/com.atproto.repo.getRecord?repo=$( Invoke-RestMethod -Uri $xrpcUrl + + + + GetAtType + From b51bf4ed527864dda16e63544e70673cb0e18bf8 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 12:51:57 -0800 Subject: [PATCH 216/724] fix: `OpenPackage.GetContent` ( Fixes #57 ) Using Reader if available --- Types/OpenPackage/GetContent.ps1 | 118 +------------------ Types/OpenPackage/get_PowerShellManifest.ps1 | 19 +-- 2 files changed, 8 insertions(+), 129 deletions(-) diff --git a/Types/OpenPackage/GetContent.ps1 b/Types/OpenPackage/GetContent.ps1 index 7af884c..1c7529b 100644 --- a/Types/OpenPackage/GetContent.ps1 +++ b/Types/OpenPackage/GetContent.ps1 @@ -56,6 +56,12 @@ $matchingParts = :nextPart foreach ($part in $matchingParts) { $thisPart = $part + + # If we have a reader, just read the content. + if ($part.Reader) { + $part.Read() + continue nextPart + } $partStream = $thisPart.GetStream() @@ -77,24 +83,6 @@ $matchingParts = $byteStream.Close() $byteStream.Dispose() - if ($thisPart.ContentType -eq 'application/x-www-form-urlencoded') { - $formData = [Web.HttpUtility]::ParseQueryString($partString) - $formDataObject = [Ordered]@{} - foreach ($key in $formData.Keys) { - if ($null -eq $formDataObject[$key]) { - $formDataObject[$key] = $formData[$key] - } else { - $formDataObject[$key] = @($formDataObject[$key]) + $formData[$key] - } - } - $formDataObject - continue nextPart - } - - # We will only try to upcast text in ways that are relatively quick. - - # We will not care what the original content type was, only if the cast succeeeds. - $partAsXml = $partString -as [xml] if ($null -ne $partAsXml) { if ($partASXml.Objs) { @@ -112,100 +100,6 @@ $matchingParts = continue nextPart } - # If the content type is json or the uri ends with json - if ($thisPart.ContentType -match '[/\+]json$' -or $thisPart.Uri -match '\.jsonc?$') { - # try to convert it from json - $partAsJson = try { - ConvertFrom-Json -InputObject $partString -ErrorAction Ignore - } catch { - Write-Warning "'$($thisPart.Uri)' was not json: $_" - } - - if ($null -ne $partAsJson) { - $partAsJson | addPackageAndPart - continue nextPart - } - } - - if ($thisPart.ContentType -match '[/\+]ya?ml' -or $thisPart.Uri -match '\.ya?ml$') { - $convertFromYamlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') - if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.InputObject) { - Write-Warning "Convert-FromYaml not found, please install YaYaml" - } else { - try { - $partString | & $convertFromYamlCommand -ErrorAction Stop | addPackageAndPart - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" - } - continue nextPart - } - } - - if ($thisPart.ContentType -match '[/\+]toml' -or - $thisPart.Uri -match '\.toml$') { - $convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') - if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { - Write-Warning "Convert-FromToml not found, please install PSToml" - } else { - try { - $partString | & $convertFromTomlCommand -ErrorAction Stop | addPackageAndPart - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" - } - continue nextPart - } - } - - # If the content type is powershell data - if ( - $thisPart.ContentType -in 'text/x-powershell-data', 'application/x-powershell-data' -or - $thisPart.Uri -match '\.psd1$' - ) { - $dataBlockOutput = try { - $scriptBlock = [ScriptBlock]::Create($partString) - $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") - $dataScriptBlock | addPackageAndPart - } catch { - $null - } - - if ($null -ne $dataBlockOutput) { - $dataBlockOutput - continue nextPart - } - } - - if ( - $thisPart.ContentType -in 'text/x-powershell', 'application/x-powershell' -or - $thisPart.Uri -match '\.ps[md]?1$' - ) { - try { - [ScriptBlock]::Create($partString) | addPackageAndPart - } catch { - Write-Warning "$($thisPart.Uri) is not a valid PowerShell script: $_" - } - continue nextPart - } - - if ($thisPart.Uri -match '\.cs$') { - $null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru - if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { - $partString | - Add-Member NoteProperty SyntaxTree ( - [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) - ) -Force -PassThru | - addPackageAndPart - - - - } else { - $partString | - addPackageAndPart - } - - continue nextPart - } - if ($thisPart.Uri -match './astro$') { $partString | addPackageAndPart diff --git a/Types/OpenPackage/get_PowerShellManifest.ps1 b/Types/OpenPackage/get_PowerShellManifest.ps1 index 0d13f6c..5b5a223 100644 --- a/Types/OpenPackage/get_PowerShellManifest.ps1 +++ b/Types/OpenPackage/get_PowerShellManifest.ps1 @@ -14,21 +14,6 @@ param() # Get every part foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { - - # Skip any non-script blocks - if ($psd1 -isnot [ScriptBlock]) { continue } - # and any anything not matching ModuleVersion - if ("$psd1" -notmatch 'ModuleVersion[\s\S]{0,}=') { continue } - # and anything with more than a single statement - if ($psd1.Ast.EndBlock.Statements.Count -ne 1) { continue } - # and anything that is not a data statement - if ($psd1.Ast.EndBlock.Statements[0] -isnot - [Management.Automation.Language.DataStatementAst] -ne 1 - ) { continue } - # and anything with -supportedCommand - if ($psd1.Ast.EndBlock.Statements[0].CommandsAllowed) { - continue - } - # at long last, we can run the script and get the manifest data. - & $psd1 + if (-not $psd1.ModuleVersion) { continue } + $psd1 } From fbaab8cf81c538e498f2b089ad0e998a52d312bd Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 20:52:27 +0000 Subject: [PATCH 217/724] fix: `OpenPackage.GetContent` ( Fixes #57 ) Using Reader if available --- OP.types.ps1xml | 411 +++--------------------------------------------- 1 file changed, 24 insertions(+), 387 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f1d6f7b..946027e 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -254,6 +254,12 @@ $matchingParts = :nextPart foreach ($part in $matchingParts) { $thisPart = $part + + # If we have a reader, just read the content. + if ($part.Reader) { + $part.Read() + continue nextPart + } $partStream = $thisPart.GetStream() @@ -275,24 +281,6 @@ $matchingParts = $byteStream.Close() $byteStream.Dispose() - if ($thisPart.ContentType -eq 'application/x-www-form-urlencoded') { - $formData = [Web.HttpUtility]::ParseQueryString($partString) - $formDataObject = [Ordered]@{} - foreach ($key in $formData.Keys) { - if ($null -eq $formDataObject[$key]) { - $formDataObject[$key] = $formData[$key] - } else { - $formDataObject[$key] = @($formDataObject[$key]) + $formData[$key] - } - } - $formDataObject - continue nextPart - } - - # We will only try to upcast text in ways that are relatively quick. - - # We will not care what the original content type was, only if the cast succeeeds. - $partAsXml = $partString -as [xml] if ($null -ne $partAsXml) { if ($partASXml.Objs) { @@ -310,100 +298,6 @@ $matchingParts = continue nextPart } - # If the content type is json or the uri ends with json - if ($thisPart.ContentType -match '[/\+]json$' -or $thisPart.Uri -match '\.jsonc?$') { - # try to convert it from json - $partAsJson = try { - ConvertFrom-Json -InputObject $partString -ErrorAction Ignore - } catch { - Write-Warning "'$($thisPart.Uri)' was not json: $_" - } - - if ($null -ne $partAsJson) { - $partAsJson | addPackageAndPart - continue nextPart - } - } - - if ($thisPart.ContentType -match '[/\+]ya?ml' -or $thisPart.Uri -match '\.ya?ml$') { - $convertFromYamlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') - if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.InputObject) { - Write-Warning "Convert-FromYaml not found, please install YaYaml" - } else { - try { - $partString | & $convertFromYamlCommand -ErrorAction Stop | addPackageAndPart - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" - } - continue nextPart - } - } - - if ($thisPart.ContentType -match '[/\+]toml' -or - $thisPart.Uri -match '\.toml$') { - $convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') - if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { - Write-Warning "Convert-FromToml not found, please install PSToml" - } else { - try { - $partString | & $convertFromTomlCommand -ErrorAction Stop | addPackageAndPart - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" - } - continue nextPart - } - } - - # If the content type is powershell data - if ( - $thisPart.ContentType -in 'text/x-powershell-data', 'application/x-powershell-data' -or - $thisPart.Uri -match '\.psd1$' - ) { - $dataBlockOutput = try { - $scriptBlock = [ScriptBlock]::Create($partString) - $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") - $dataScriptBlock | addPackageAndPart - } catch { - $null - } - - if ($null -ne $dataBlockOutput) { - $dataBlockOutput - continue nextPart - } - } - - if ( - $thisPart.ContentType -in 'text/x-powershell', 'application/x-powershell' -or - $thisPart.Uri -match '\.ps[md]?1$' - ) { - try { - [ScriptBlock]::Create($partString) | addPackageAndPart - } catch { - Write-Warning "$($thisPart.Uri) is not a valid PowerShell script: $_" - } - continue nextPart - } - - if ($thisPart.Uri -match '\.cs$') { - $null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru - if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { - $partString | - Add-Member NoteProperty SyntaxTree ( - [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) - ) -Force -PassThru | - addPackageAndPart - - - - } else { - $partString | - addPackageAndPart - } - - continue nextPart - } - if ($thisPart.Uri -match './astro$') { $partString | addPackageAndPart @@ -1512,23 +1406,8 @@ param() # Get every part foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { - - # Skip any non-script blocks - if ($psd1 -isnot [ScriptBlock]) { continue } - # and any anything not matching ModuleVersion - if ("$psd1" -notmatch 'ModuleVersion[\s\S]{0,}=') { continue } - # and anything with more than a single statement - if ($psd1.Ast.EndBlock.Statements.Count -ne 1) { continue } - # and anything that is not a data statement - if ($psd1.Ast.EndBlock.Statements[0] -isnot - [Management.Automation.Language.DataStatementAst] -ne 1 - ) { continue } - # and anything with -supportedCommand - if ($psd1.Ast.EndBlock.Statements[0].CommandsAllowed) { - continue - } - # at long last, we can run the script and get the manifest data. - & $psd1 + if (-not $psd1.ModuleVersion) { continue } + $psd1 } @@ -2092,6 +1971,12 @@ $matchingParts = :nextPart foreach ($part in $matchingParts) { $thisPart = $part + + # If we have a reader, just read the content. + if ($part.Reader) { + $part.Read() + continue nextPart + } $partStream = $thisPart.GetStream() @@ -2113,24 +1998,6 @@ $matchingParts = $byteStream.Close() $byteStream.Dispose() - if ($thisPart.ContentType -eq 'application/x-www-form-urlencoded') { - $formData = [Web.HttpUtility]::ParseQueryString($partString) - $formDataObject = [Ordered]@{} - foreach ($key in $formData.Keys) { - if ($null -eq $formDataObject[$key]) { - $formDataObject[$key] = $formData[$key] - } else { - $formDataObject[$key] = @($formDataObject[$key]) + $formData[$key] - } - } - $formDataObject - continue nextPart - } - - # We will only try to upcast text in ways that are relatively quick. - - # We will not care what the original content type was, only if the cast succeeeds. - $partAsXml = $partString -as [xml] if ($null -ne $partAsXml) { if ($partASXml.Objs) { @@ -2148,100 +2015,6 @@ $matchingParts = continue nextPart } - # If the content type is json or the uri ends with json - if ($thisPart.ContentType -match '[/\+]json$' -or $thisPart.Uri -match '\.jsonc?$') { - # try to convert it from json - $partAsJson = try { - ConvertFrom-Json -InputObject $partString -ErrorAction Ignore - } catch { - Write-Warning "'$($thisPart.Uri)' was not json: $_" - } - - if ($null -ne $partAsJson) { - $partAsJson | addPackageAndPart - continue nextPart - } - } - - if ($thisPart.ContentType -match '[/\+]ya?ml' -or $thisPart.Uri -match '\.ya?ml$') { - $convertFromYamlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') - if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.InputObject) { - Write-Warning "Convert-FromYaml not found, please install YaYaml" - } else { - try { - $partString | & $convertFromYamlCommand -ErrorAction Stop | addPackageAndPart - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" - } - continue nextPart - } - } - - if ($thisPart.ContentType -match '[/\+]toml' -or - $thisPart.Uri -match '\.toml$') { - $convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') - if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { - Write-Warning "Convert-FromToml not found, please install PSToml" - } else { - try { - $partString | & $convertFromTomlCommand -ErrorAction Stop | addPackageAndPart - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" - } - continue nextPart - } - } - - # If the content type is powershell data - if ( - $thisPart.ContentType -in 'text/x-powershell-data', 'application/x-powershell-data' -or - $thisPart.Uri -match '\.psd1$' - ) { - $dataBlockOutput = try { - $scriptBlock = [ScriptBlock]::Create($partString) - $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") - $dataScriptBlock | addPackageAndPart - } catch { - $null - } - - if ($null -ne $dataBlockOutput) { - $dataBlockOutput - continue nextPart - } - } - - if ( - $thisPart.ContentType -in 'text/x-powershell', 'application/x-powershell' -or - $thisPart.Uri -match '\.ps[md]?1$' - ) { - try { - [ScriptBlock]::Create($partString) | addPackageAndPart - } catch { - Write-Warning "$($thisPart.Uri) is not a valid PowerShell script: $_" - } - continue nextPart - } - - if ($thisPart.Uri -match '\.cs$') { - $null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru - if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { - $partString | - Add-Member NoteProperty SyntaxTree ( - [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) - ) -Force -PassThru | - addPackageAndPart - - - - } else { - $partString | - addPackageAndPart - } - - continue nextPart - } - if ($thisPart.Uri -match './astro$') { $partString | addPackageAndPart @@ -3350,23 +3123,8 @@ param() # Get every part foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { - - # Skip any non-script blocks - if ($psd1 -isnot [ScriptBlock]) { continue } - # and any anything not matching ModuleVersion - if ("$psd1" -notmatch 'ModuleVersion[\s\S]{0,}=') { continue } - # and anything with more than a single statement - if ($psd1.Ast.EndBlock.Statements.Count -ne 1) { continue } - # and anything that is not a data statement - if ($psd1.Ast.EndBlock.Statements[0] -isnot - [Management.Automation.Language.DataStatementAst] -ne 1 - ) { continue } - # and anything with -supportedCommand - if ($psd1.Ast.EndBlock.Statements[0].CommandsAllowed) { - continue - } - # at long last, we can run the script and get the manifest data. - & $psd1 + if (-not $psd1.ModuleVersion) { continue } + $psd1 } @@ -3930,6 +3688,12 @@ $matchingParts = :nextPart foreach ($part in $matchingParts) { $thisPart = $part + + # If we have a reader, just read the content. + if ($part.Reader) { + $part.Read() + continue nextPart + } $partStream = $thisPart.GetStream() @@ -3951,24 +3715,6 @@ $matchingParts = $byteStream.Close() $byteStream.Dispose() - if ($thisPart.ContentType -eq 'application/x-www-form-urlencoded') { - $formData = [Web.HttpUtility]::ParseQueryString($partString) - $formDataObject = [Ordered]@{} - foreach ($key in $formData.Keys) { - if ($null -eq $formDataObject[$key]) { - $formDataObject[$key] = $formData[$key] - } else { - $formDataObject[$key] = @($formDataObject[$key]) + $formData[$key] - } - } - $formDataObject - continue nextPart - } - - # We will only try to upcast text in ways that are relatively quick. - - # We will not care what the original content type was, only if the cast succeeeds. - $partAsXml = $partString -as [xml] if ($null -ne $partAsXml) { if ($partASXml.Objs) { @@ -3986,100 +3732,6 @@ $matchingParts = continue nextPart } - # If the content type is json or the uri ends with json - if ($thisPart.ContentType -match '[/\+]json$' -or $thisPart.Uri -match '\.jsonc?$') { - # try to convert it from json - $partAsJson = try { - ConvertFrom-Json -InputObject $partString -ErrorAction Ignore - } catch { - Write-Warning "'$($thisPart.Uri)' was not json: $_" - } - - if ($null -ne $partAsJson) { - $partAsJson | addPackageAndPart - continue nextPart - } - } - - if ($thisPart.ContentType -match '[/\+]ya?ml' -or $thisPart.Uri -match '\.ya?ml$') { - $convertFromYamlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') - if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.InputObject) { - Write-Warning "Convert-FromYaml not found, please install YaYaml" - } else { - try { - $partString | & $convertFromYamlCommand -ErrorAction Stop | addPackageAndPart - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" - } - continue nextPart - } - } - - if ($thisPart.ContentType -match '[/\+]toml' -or - $thisPart.Uri -match '\.toml$') { - $convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') - if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { - Write-Warning "Convert-FromToml not found, please install PSToml" - } else { - try { - $partString | & $convertFromTomlCommand -ErrorAction Stop | addPackageAndPart - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" - } - continue nextPart - } - } - - # If the content type is powershell data - if ( - $thisPart.ContentType -in 'text/x-powershell-data', 'application/x-powershell-data' -or - $thisPart.Uri -match '\.psd1$' - ) { - $dataBlockOutput = try { - $scriptBlock = [ScriptBlock]::Create($partString) - $dataScriptBlock = [ScriptBlock]::Create("data {$scriptBlock}") - $dataScriptBlock | addPackageAndPart - } catch { - $null - } - - if ($null -ne $dataBlockOutput) { - $dataBlockOutput - continue nextPart - } - } - - if ( - $thisPart.ContentType -in 'text/x-powershell', 'application/x-powershell' -or - $thisPart.Uri -match '\.ps[md]?1$' - ) { - try { - [ScriptBlock]::Create($partString) | addPackageAndPart - } catch { - Write-Warning "$($thisPart.Uri) is not a valid PowerShell script: $_" - } - continue nextPart - } - - if ($thisPart.Uri -match '\.cs$') { - $null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru - if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { - $partString | - Add-Member NoteProperty SyntaxTree ( - [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) - ) -Force -PassThru | - addPackageAndPart - - - - } else { - $partString | - addPackageAndPart - } - - continue nextPart - } - if ($thisPart.Uri -match './astro$') { $partString | addPackageAndPart @@ -5188,23 +4840,8 @@ param() # Get every part foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { - - # Skip any non-script blocks - if ($psd1 -isnot [ScriptBlock]) { continue } - # and any anything not matching ModuleVersion - if ("$psd1" -notmatch 'ModuleVersion[\s\S]{0,}=') { continue } - # and anything with more than a single statement - if ($psd1.Ast.EndBlock.Statements.Count -ne 1) { continue } - # and anything that is not a data statement - if ($psd1.Ast.EndBlock.Statements[0] -isnot - [Management.Automation.Language.DataStatementAst] -ne 1 - ) { continue } - # and anything with -supportedCommand - if ($psd1.Ast.EndBlock.Statements[0].CommandsAllowed) { - continue - } - # at long last, we can run the script and get the manifest data. - & $psd1 + if (-not $psd1.ModuleVersion) { continue } + $psd1 } From 487c9bea72c488c3f583fbbd5e8991c25f21d2c6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 13:54:54 -0800 Subject: [PATCH 218/724] feat: `Select-OpenPackage` ( Fixes #44 ) Honoring -List --- Commands/Select-OpenPackage.ps1 | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Commands/Select-OpenPackage.ps1 b/Commands/Select-OpenPackage.ps1 index 140a53d..f29da2b 100644 --- a/Commands/Select-OpenPackage.ps1 +++ b/Commands/Select-OpenPackage.ps1 @@ -12,7 +12,8 @@ function Select-OpenPackage #> [Alias('Select-OP','scop', 'scOpenPackage')] [CmdletBinding(DefaultParameterSetName='All')] - param( + param( + # A list of patterns to match. [Parameter(ParameterSetName='Select-String',ValueFromPipelineByPropertyName)] [PSObject[]] $Pattern, @@ -20,7 +21,7 @@ function Select-OpenPackage # Indicates that the cmdlet uses a simple match rather than a regular expression match. [Parameter(ParameterSetName='Select-String')] [switch] - $SimpleMatch, + $SimpleMatch, # Indicates that the cmdlet matches are case-sensitive. By default, pattern matches aren't case-sensitive. [Parameter(ParameterSetName='Select-String')] @@ -172,6 +173,13 @@ function Select-OpenPackage begin { $selectString = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-String','Cmdlet') $selectXml = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-Xml','Cmdlet') + filter selectOrList { + if ($list) { + $inputPart + } else { + $_ + } + } } process { @@ -259,7 +267,8 @@ function Select-OpenPackage $inputAsXml | & $selectXml @selectParameters | Add-Member NoteProperty Uri $inputPart.Uri -Force -PassThru | - Select-Object Node, Pattern, Uri + Select-Object Node, Pattern, Uri | + . selectOrList } if ($Pattern) { @@ -273,7 +282,8 @@ function Select-OpenPackage Select-String @selectParameters -InputObject $inputText | Add-Member NoteProperty Uri $inputPart.Uri -Force -PassThru | - Add-Member NoteProperty Package $InputObject -Force -PassThru + Add-Member NoteProperty Package $InputObject -Force -PassThru | + . selectOrList } $inputAsScript = @@ -289,8 +299,9 @@ function Select-OpenPackage foreach ($searchScript in $AstCondition) { $inputAsScript.Ast.FindAll($searchScript, $true) | Add-Member NoteProperty Uri $inputPart.Uri -Force -PassThru | - Add-Member NoteProperty Package $InputObject -Force -PassThru - } + Add-Member NoteProperty Package $InputObject -Force -PassThru | + . selectOrList + } } } } From 26bdc32632acdae1bf7e614ff08fe9c72bf4671d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 16:10:44 -0800 Subject: [PATCH 219/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding content types --- Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 index 24e0531..873788d 100644 --- a/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 +++ b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 @@ -12,6 +12,7 @@ '.bz2' = 'application/x-bzip2' '.cda' = 'application/x-cdf' '.csh' = 'application/x-csh' + '.csproj' = 'application/xml' '.css' = 'text/css' '.csv' = 'text/csv' '.doc' = 'application/msword' @@ -19,6 +20,7 @@ '.eot' = 'application/vnd.ms-fontobject' '.epub' = 'application/epub+zip' '.exe' = 'application/executeable' + '.fsproj' = 'application/xml' '.gz' = 'application/gzip' '.gif' = 'image/gif' '.htm' = 'text/html' @@ -31,7 +33,7 @@ '.js' = 'text/javascript' '.jsm' = 'text/javascript' '.json' = 'application/json' - '.jsonld' = 'application/ld+json' + '.jsonld' = 'application/ld+json' '.md' = 'text/markdown' '.mid' = 'audio/midi' '.midi' = 'audio/midi' @@ -67,6 +69,7 @@ '.ts' = 'application/x-typescript' '.ttf' = 'font/ttf' '.txt' = 'text/plain' + '.vbproj' = 'application/xml' '.vsd' = 'application/vnd.visio' '.wav' = 'audio/wav' '.weba' = 'audio/webm' From 4267ab612a2199c22b76874d03955e8f267cf568 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 4 Mar 2026 00:11:03 +0000 Subject: [PATCH 220/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding content types --- OP.types.ps1xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 946027e..0aaeadf 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5171,6 +5171,7 @@ FileList '.bz2' = 'application/x-bzip2' '.cda' = 'application/x-cdf' '.csh' = 'application/x-csh' + '.csproj' = 'application/xml' '.css' = 'text/css' '.csv' = 'text/csv' '.doc' = 'application/msword' @@ -5178,6 +5179,7 @@ FileList '.eot' = 'application/vnd.ms-fontobject' '.epub' = 'application/epub+zip' '.exe' = 'application/executeable' + '.fsproj' = 'application/xml' '.gz' = 'application/gzip' '.gif' = 'image/gif' '.htm' = 'text/html' @@ -5190,7 +5192,7 @@ FileList '.js' = 'text/javascript' '.jsm' = 'text/javascript' '.json' = 'application/json' - '.jsonld' = 'application/ld+json' + '.jsonld' = 'application/ld+json' '.md' = 'text/markdown' '.mid' = 'audio/midi' '.midi' = 'audio/midi' @@ -5226,6 +5228,7 @@ FileList '.ts' = 'application/x-typescript' '.ttf' = 'font/ttf' '.txt' = 'text/plain' + '.vbproj' = 'application/xml' '.vsd' = 'application/vnd.visio' '.wav' = 'audio/wav' '.weba' = 'audio/webm' From 8a96223cbe6273a3e551e2fd3a1f9c492d225208 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 17:03:24 -0800 Subject: [PATCH 221/724] feat: `OpenPackage.get_ProjectFile` ( Fixes #33 ) --- Types/OpenPackage/get_ProjectFile.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Types/OpenPackage/get_ProjectFile.ps1 b/Types/OpenPackage/get_ProjectFile.ps1 index 33b2a3d..7c7a824 100644 --- a/Types/OpenPackage/get_ProjectFile.ps1 +++ b/Types/OpenPackage/get_ProjectFile.ps1 @@ -8,5 +8,5 @@ param() $this.GetContent( - $this -match '\..+?proj$' + $this.FileList -match '\..+?proj$' ) \ No newline at end of file From b52a99f5836f6ccb481f63637218baef638527e7 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 4 Mar 2026 01:03:42 +0000 Subject: [PATCH 222/724] feat: `OpenPackage.get_ProjectFile` ( Fixes #33 ) --- OP.types.ps1xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0aaeadf..ef6f136 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1446,7 +1446,7 @@ foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { param() $this.GetContent( - $this -match '\..+?proj$' + $this.FileList -match '\..+?proj$' ) @@ -3163,7 +3163,7 @@ foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { param() $this.GetContent( - $this -match '\..+?proj$' + $this.FileList -match '\..+?proj$' ) @@ -4880,7 +4880,7 @@ foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { param() $this.GetContent( - $this -match '\..+?proj$' + $this.FileList -match '\..+?proj$' ) From 7f06ecde6f16f2cbec60a15c92b94bd68a8cb83c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 3 Mar 2026 19:02:15 -0800 Subject: [PATCH 223/724] feat: `OpenPackage.GetRepository` ( Fixes #115 ) --- Commands/Get-OpenPackage.ps1 | 214 +++++++-------------------- Types/OpenPackage/GetRepository.ps1 | 215 ++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+), 166 deletions(-) create mode 100644 Types/OpenPackage/GetRepository.ps1 diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 87a482e..423c123 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -12,10 +12,10 @@ function Get-OpenPackage The following types of packages are currently supported: - * Any [Open Packaging Convention](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) files + * Any [Open Packaging Convention](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) files * Any directory * Any `*.zip` file - * Any *.tar.gz file + * Any `*.tar.gz` file * Any url * Any git repository * Any public at protocol URI @@ -211,34 +211,53 @@ function Get-OpenPackage # Next up we initialize a type map. # This maps extensions and uris to a content type, and is used when creating parts. $typeMap = ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap + + # Gets the open package type data, as we will need this + $OpTypeData = Get-TypeData -TypeName OpenPackage + + function InvokeOpMethod { + param([string]$Name, [Collections.IDictionary]$Parameter) + + if (-not $OpTypeData.Members[$Name].Script) { + return + } + $bindableParameters = [Ordered]@{} + $function:func = $OpTypeData.Members[$Name].Script + $func = $ExecutionContext.SessionState.InvokeCommand.GetCommand('func', 'Function') + + :nextParameter foreach ($parameterName in $func.Parameters.Keys) { + if ($null -ne $parameter[$parameterName]) { + $bindableParameters[$parameterName] = $Parameter[$parameterName] + continue nextParameter + } + foreach ($aliasName in $func.Parameters[$parameterName].Aliases) { + if ($null -ne $Parameter[$aliasName]) { + $bindableParameters[$aliasName] = $Parameter[$aliasName] + continue nextParameter + } + } + } + + try { + & $func @bindableParameters + } catch { + $PSCmdlet.WriteError($_) + } + } - # - $OpenPackageTypeData = Get-TypeData -TypeName OpenPackage # The full default type map is much more robust. # If this was being used without a module context, we still want to work for _most_ scenarios # So this a secondary default, declared inline, that captures a fairly short list of common content types. - if (-not $typeMap) { + if (-not $typeMap -or -not $typeMap.Count) { $typeMap = [Ordered]@{ - ".apng" = "image/apng" - ".css" = "text/css" - ".gif" = "image/gif" - ".jpg" = "image/jpeg" - ".jpeg" = "image/jpeg" - ".js" = "text/javascript" - ".jsm" = "text/javascript" - ".html" = "text/html" + ".css" = "text/css"; ".html" = "text/html"; + ".svg" = "image/svg+xml"; '.js' = 'text/javascript';".jsm" = "text/javascript"; + ".png" = "image/png"; ".gif" = "image/gif"; + ".jpg" = "image/jpeg"; ".jpeg" = "image/jpeg"; ".md" = "text/markdown" - ".mp3" = "audio/mpeg" - ".mp4" = "video/mp4" - ".png" = "image/png" - ".ps1" = "text/x-powershell" - ".psm1" = "text/x-powershell" - ".psd1" = "text/x-powershell-data" - ".ps1xml" = "text/x-powershell+xml" - ".clixml" = "text/cli+xml" - ".svg" = "image/svg+xml" - ".xml" = "application/xml" + ".mp3" = "audio/mpeg"; ".mp4" = "video/mp4"; + ".xml" = "application/xml" } } @@ -724,137 +743,9 @@ function Get-OpenPackage filter packRepo { $Repository = $_ - $repositoryUrl = $Repository - # See if we are matching a repo or a tree or blob. - $treePattern = '/(?>tree|blob)/(?[^/]+)/' - if ($Repository -match $treePattern -and -not $NamedParameters.SparseFilter) { - # If we are matching a tree, turn the file portion into a sparse filter - $branch = $matches.branch - $NamedParameters.SparseFilter = - $SparseFilter = - $RepositoryUrl -replace "^.+?$treePattern" -replace '^/?', '/' -replace '/?$', '/' -replace '(?&1 | - . $writeGitProgress - # and push into the location - Push-Location -LiteralPath $repoDirectory - # then set our sparse filter - $SparseArgs = @($SparseFilter) - & $gitApp sparse-checkout set --no-cone @SparseArgs *>&1 | - . $writeGitProgress - # and checkout. - & $gitApp checkout @checkoutBranch *>&1 | - . $writeGitProgress - # we should now have the files, so prepare an -Include filter - if (-not $namedParameters.Include) { - $namedParameters.Include = foreach ($sparse in $SparseFilter) { - "*$sparse" - } - } - # clean up our progress - $gitProgress.Completed = $true - Write-Progress @gitProgress - # and call ourselves with the repoDirectory - & $myCommandName -FilePath $repoDirectory @namedParameters - Pop-Location - } else { - # If no sparse filters were provided, just clone - & $gitApp clone $Repository *>&1 | - . $writeGitProgress - - $gitProgress.Completed = $true - Write-Progress @gitProgress - - if ($?) { - Push-Location $repoDirectory - # and call ourself - & $myCommandName -FilePath $repoDirectory @namedParameters - Pop-Location - } - } - - Pop-Location - if (-not $?) { return } - } - else { - # If the directory already exists, go there - Push-Location $repoDirectory - # and checkout - & $gitApp checkout @checkoutBranch | . $writeGitProgress - # and if a sparse filter was provided - if ($SparseFilter) { - if (-not $namedParameters.Include) { - # try to only include those files - $namedParameters.Include = foreach ($sparse in $SparseFilter) { - "*$sparse" - } - } - } - $gitProgress.Completed = $true - Write-Progress @gitProgress - - # Call ourselves and make a package from this directory - & $myCommandName -FilePath $repoDirectory @namedParameters - Pop-Location - } + InvokeOpMethod 'GetRepository' $NamedParameters + return } filter packTar { @@ -944,7 +835,7 @@ function Get-OpenPackage ) { # pack a repository $namedParameters.Remove('Uri') - & $myCommandName -Repository $uri @namedParameters + Get-OpenPackage -Repository $uri @namedParameters return } @@ -1318,13 +1209,8 @@ function Get-OpenPackage } #region Open Package from Repository - if ($Repository) { - if (-not $gitApp) { - throw "No git" - } - - $Repository | packRepo - + if ($Repository) { + $Repository | packRepo return } #endregion Open Package from Repository @@ -1362,11 +1248,7 @@ function Get-OpenPackage #region Open Package from Nuget if ($Nuget) { # Try to call our GetNuget method, and pass the `$Nuget` - try { - & $openPackageTypeData.Members['GetNuget'].Script -NuGet $Nuget - } catch { - $PSCmdlet.WriteError($_) - } + InvokeOpMethod 'GetNuget' $PSBoundParameters return } #endregion Open Package from Nuget diff --git a/Types/OpenPackage/GetRepository.ps1 b/Types/OpenPackage/GetRepository.ps1 new file mode 100644 index 0000000..5188a84 --- /dev/null +++ b/Types/OpenPackage/GetRepository.ps1 @@ -0,0 +1,215 @@ +<# +.SYNOPSIS + Gets a repository as a package +.DESCRIPTION + Gets a repository as an open package +#> +param( +# A Repository to package. +# This can be the root of a repo or a link to a portion of the tree. +# If a portion of the tree is provided, will perform a sparse clone of the repository +[Parameter(Mandatory,ValueFromPipelineByPropertyName)] +[Alias('clone_url')] +[string] +$Repository, + +# The github branch name. +[Parameter(ValueFromPipelineByPropertyName)] +[string] +$Branch, + +# One or more optional sparse filters to a repository. +# If these are provided, only files matching these filters will be downloaded. +[Parameter(ValueFromPipelineByPropertyName)] +[string[]] +$SparseFilter, + +# A list of file wildcards to include. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Include, + +# A list of file wildcards to exclude. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Exclude, + +# A content type map. +# This maps extensions and URIs to a content type. +[Collections.IDictionary] +$TypeMap = $( + ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap +), + +# The compression option. +[IO.Packaging.CompressionOption] +[Alias('CompressionLevel')] +$CompressionOption = 'Superfast', + + +# If set, will force the redownload of various resources and remove existing files or directories +[switch] +$Force, + +# If set, will include hidden files and folders, except for files beneath `.git` +[Alias('IncludeDotFiles')] +[switch] +$IncludeHidden, + +# If set, will include the `.git` directory contents if found. +[switch] +$IncludeGit +) + + +$gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git', 'Application') +if (-not $gitApp) { + throw "No git" +} + +$myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage + +$namedParameters = [Ordered]@{} +foreach ($key in $namedParameters.Keys) { + $var = $ExecutionContext.SessionState.PSVariable.Get($key) + if ($var) { + $namedParameters[$key] = $var.value + } +} +$namedParameters.Remove('Repository') + + +$repositoryUrl = $Repository + +# See if we are matching a repo or a tree or blob. +$treePattern = '/(?>tree|blob)/(?[^/]+)/' +if ($Repository -match $treePattern -and -not $NamedParameters.SparseFilter) { + # If we are matching a tree, turn the file portion into a sparse filter + $branch = $matches.branch + $NamedParameters.SparseFilter = + $SparseFilter = + $RepositoryUrl -replace "^.+?$treePattern" -replace '^/?', '/' -replace '/?$', '/' -replace '(?&1 | + . $writeGitProgress + # and push into the location + Push-Location -LiteralPath $repoDirectory + # then set our sparse filter + $SparseArgs = @($SparseFilter) + & $gitApp sparse-checkout set --no-cone @SparseArgs *>&1 | + . $writeGitProgress + # and checkout. + & $gitApp checkout @checkoutBranch *>&1 | + . $writeGitProgress + # we should now have the files, so prepare an -Include filter + if (-not $namedParameters.Include) { + $namedParameters.Include = foreach ($sparse in $SparseFilter) { + "*$sparse" + } + } + # clean up our progress + $gitProgress.Completed = $true + Write-Progress @gitProgress + # and call ourselves with the repoDirectory + Get-OpenPackage -FilePath $repoDirectory @namedParameters + Pop-Location + } else { + # If no sparse filters were provided, just clone + & $gitApp clone $Repository *>&1 | + . $writeGitProgress + + $gitProgress.Completed = $true + Write-Progress @gitProgress + + if ($?) { + Push-Location $repoDirectory + # and call ourself + Get-OpenPackage -FilePath $repoDirectory @namedParameters + Pop-Location + } + } + + Pop-Location + if (-not $?) { return } +} +else { + # If the directory already exists, go there + Push-Location $repoDirectory + # and checkout + & $gitApp checkout @checkoutBranch | . $writeGitProgress + # and if a sparse filter was provided + if ($SparseFilter) { + if (-not $namedParameters.Include) { + # try to only include those files + $namedParameters.Include = foreach ($sparse in $SparseFilter) { + "*$sparse" + } + } + } + $gitProgress.Completed = $true + Write-Progress @gitProgress + + # Call get-openpackage and make a package from this directory + Get-OpenPackage -FilePath $repoDirectory @namedParameters + Pop-Location +} \ No newline at end of file From fd7fe98b23d4f1aefbb63fcdb0ba2cf1ab67e9a5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 4 Mar 2026 03:02:36 +0000 Subject: [PATCH 224/724] feat: `OpenPackage.GetRepository` ( Fixes #115 ) --- OP.types.ps1xml | 660 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 660 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index ef6f136..c499c27 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -357,6 +357,226 @@ if ($nuget.Segments[1] -match 'api') { } else { throw "Could not convert $($NuGet) to a package URL" return +} + + + + GetRepository + @@ -2074,6 +2294,226 @@ if ($nuget.Segments[1] -match 'api') { } else { throw "Could not convert $($NuGet) to a package URL" return +} + + + + GetRepository + @@ -3791,6 +4231,226 @@ if ($nuget.Segments[1] -match 'api') { } else { throw "Could not convert $($NuGet) to a package URL" return +} + + + + GetRepository + From ac4991e44a552383ee90b78a2a9cc9ac88cc3d98 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 4 Mar 2026 12:00:41 -0800 Subject: [PATCH 225/724] feat: `OpenPackage.GetDirectory` ( Fixes #116 ) --- Commands/Get-OpenPackage.ps1 | 172 ++-------------------- Types/OpenPackage/GetDirectory.ps1 | 226 +++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+), 161 deletions(-) create mode 100644 Types/OpenPackage/GetDirectory.ps1 diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 423c123..547e26f 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -101,7 +101,7 @@ function Get-OpenPackage # If this URI is a git repository, will make a package out of the repository # If this URI is a nuget package url or powershell gallery url, will download the package. [Parameter(Mandatory,ParameterSetName='Uri',ValueFromPipelineByPropertyName)] - [Alias('Url', 'Link', 'Href')] + [Alias('Url')] [uri] $Uri, @@ -194,8 +194,14 @@ function Get-OpenPackage $IncludeHidden, # If set, will include the `.git` directory contents if found. + [Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] [switch] - $IncludeGit + $IncludeGit, + + # If set, will include any content found in `/node_modules`. + [Alias('IncludeNodeModules')] + [switch] + $IncludeNodeModule ) begin { @@ -524,163 +530,9 @@ function Get-OpenPackage # start off by pushing into that location, for it will make operations easier Push-Location -LiteralPath $resolvedItem.FullName # Get all files beneath this point - - $gciSplat = [Ordered]@{LiteralPath=$resolvedItem.FullName;Recurse=$true;File=$true} - - if ($IncludeHidden) { $gciSplat.Force = $true } - $filesToArchive = @(Get-ChildItem @gciSplat) - - - # and create a memory stream and package. - $currentPackage = getCurrentPack - - if (-not $currentPackage.PackageProperties.Identifier) { - # The identifier will be the directory name. - $currentPackage.PackageProperties.Identifier = $resolvedItem.Name - } - - # This make take a sec, so let's create a progress bar - $Progress = [Ordered]@{ - Status = " " - Activity = "Creating Package $($resolvedItem.Name)" - Id = Get-Random - } - $total = $filesToArchive.Length - $counter = 0 - - # We will use file types to provide package metadata - - # So declare an oldest created file and newest write time. - $oldestCreationTime = [DateTime]::Now - $lastWriteTime = [DateTime]::MinValue - - #region Filter Filters - $filteredFiles = @( - # If any exclusions are present, - # security dictates we process them first. - # (deny before approve) - :filterFiles foreach ($file in $filesToArchive) { - if (-not $includeGit -and $file -match '[\\/].git[\\/]') { - continue - } - if ($exclude) { - foreach ($exclusion in $Exclude) { - if ($file.FullName -like $exclusion) { - continue filterFiles - } - } - } - - if ($include) { - $included = $false - foreach ($inclusion in $include) { - if ($file.FullName -like $inclusion) { - $included = $true - break - } - } - - if (-not $included) { continue filterFiles } - } - - $file - } - ) - #region Filter Filters - - # Go over each file we want to archive - :packingFiles foreach ($file in $filteredFiles) { - # get each file as a relative uri, and then get it's bytes - $relativeUri = $file.FullName.Substring($resolvedItem.FullName.Length) -replace '[\\/]', '/' - $fileBytes = Get-Content -AsByteStream -Raw -LiteralPath $file.FullName - - # If the file was blank - if (-not $fileBytes) { - # write a message to verbose indicating we are skipping the file. - Write-Verbose "Skipping blank file $($file.FullName)" - continue - } - - # encode our URI, - $encodedUri = [Web.HttpUtility]::UrlEncode($relativeUri) -replace - # but don't forget to fix spaces and decode slashes - '\+', '%20' -replace '%2f', '/' - # (our relative URI may already contain them) - - $relativeUri = '/' + ($encodedUri -replace '^/') - - # Determine the right content type for the extension - $fileContentType = $typeMap[$file.Extension] - # and fall back to text/plain - if (-not $fileContentType) { $fileContentType = 'text/plain'} - - # Then update our creation times / last write times as needed. - if ($file.CreationTime -lt $oldestCreationTime) { - $oldestCreationTime = $file.CreationTime - } - if ($file.LastWriteTime -gt $lastWriteTime) { - $lastWriteTime = $file.LastWriteTime - } - - # Write our progress message - $progress.PercentComplete = (++$counter * 100 / $total) - $Progress.Status = "$relativeUri" - Write-Progress @Progress - - if (-not $fileBytes) { continue } - - # Try to create a new part - try { - $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, $CompressionOption) - } catch { - # If that didn't work, - $ex = $_ - # at least one exception has some well known answers - if ($ex.Exception.HResult -eq 0x80131501) { - # Open Packaging Conventions do not allow case-sensitive collisions - # (most likely because they would not extract well on all operating systems) - # If we find an exception indicating a conflict, and multiple copies - $multipleCopies = @($filesToArchive | - Where-Object Fullname -ieq $file.FullName | - Select-Object -ExpandProperty Fullname) - if ($multipleCopies.Count) { - # warn the user - Write-Warning "Skipping '$($File.Fullname)' - Case Sensitivity conflict between:$( - @( - [Environment]::NewLine - # and provide a helpful pair of paths so they can resolve the conflict - $multipleCopies - ) -join - [Environment]::NewLine - )" - } else { - # If there were not multiple files, error (but do not return) - Write-Error -ErrorRecord $ex - } - } else { - # and if the error was unknown, error (but do not return) - Write-Error -ErrorRecord $ex - } - } - - # If we could not create the part, continue - if (-not $newPart) { continue } - $newStream = $newPart.GetStream() - $newStream.Write($fileBytes, 0, $fileBytes.Length) - $newStream.Close() - } - - Pop-Location - - $currentPackage | Add-Member NoteProperty SourceDirectory $resolvedItem -Force - - $Progress.Remove('PercentComplete') - $Progress.Completed = $true - Write-Progress @Progress - $currentPackage.PackageProperties.Created = $oldestCreationTime - $currentPackage.PackageProperties.Modified = $lastWriteTime - $currentPackage - + $namedParameters['Directory'] = $_ + InvokeOpMethod 'GetDirectory' $NamedParameters + return } filter packFile { @@ -1015,8 +867,6 @@ function Get-OpenPackage $currentPackage } - $gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git', 'Application') - $myNoun = $MyInvocation.MyCommand.Name -replace '^.+?-' } diff --git a/Types/OpenPackage/GetDirectory.ps1 b/Types/OpenPackage/GetDirectory.ps1 new file mode 100644 index 0000000..790aa0d --- /dev/null +++ b/Types/OpenPackage/GetDirectory.ps1 @@ -0,0 +1,226 @@ +param( +[string[]] +$Directory, + +# One or more optional sparse filters to a repository. +# If these are provided, only files matching these filters will be downloaded. +[Parameter(ValueFromPipelineByPropertyName)] +[string[]] +$SparseFilter, + +# A list of file wildcards to include. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Include, + +# A list of file wildcards to exclude. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Exclude, + +# A content type map. +# This maps extensions and URIs to a content type. +[Collections.IDictionary] +$TypeMap = $( + ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap +), + +# The compression option. +[IO.Packaging.CompressionOption] +[Alias('CompressionLevel')] +$CompressionOption = 'Superfast', + +# If set, will force the redownload of various resources and remove existing files or directories +[switch] +$Force, + +# If set, will include hidden files and folders, except for files beneath `.git` +[Alias('IncludeDotFiles')] +[switch] +$IncludeHidden, + +# If set, will include the `.git` directory contents if found. +[Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] +[switch] +$IncludeGit, + +# If set, will include any content found in `/node_modules`. +[Alias('IncludeNodeModules')] +[switch] +$IncludeNodeModule, + + +[IO.Packaging.Package] +$Package +) + + +$resolvedItems = Get-Item -Path $Directory + +foreach ($resolvedItem in $resolvedItems) { + if ($resolvedItem -isnot [IO.DirectoryInfo]) { + continue + } + $gciSplat = [Ordered]@{LiteralPath=$resolvedItem.FullName;Recurse=$true;File=$true} + if ($IncludeHidden) { $gciSplat.Force = $true } + $filesToArchive = @(Get-ChildItem @gciSplat) + + # This make take a sec, so let's create a progress bar + $Progress = [Ordered]@{ + Status = " " + Activity = "Creating Package $($resolvedItem.Name)" + Id = Get-Random + } + $total = $filesToArchive.Length + $counter = 0 + + # We will use file types to provide package metadata + + $memoryStream = [IO.MemoryStream]::new() + if (-not $package) { + $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') + $package.pstypenames.insert(0, 'OP') + $package.pstypenames.insert(0, 'OpenPackage') + $package.PackageProperties.Identifier = $resolvedItem.Name + Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force + } + + + # So declare an oldest created file and newest write time. + $oldestCreationTime = [DateTime]::Now + $lastWriteTime = [DateTime]::MinValue + + #region Filter Filters + $filteredFiles = @( + # If any exclusions are present, + # security dictates we process them first. + # (deny before approve) + :filterFiles foreach ($file in $filesToArchive) { + if (-not $includeGit -and $file -match '[\\/].git[\\/]') { + continue + } + if (-not $IncludeNodeModule -and $file -match '[\\/]node_modules[\\/]') { + continue + } + if ($exclude) { + foreach ($exclusion in $Exclude) { + if ($file.FullName -like $exclusion) { + continue filterFiles + } + } + } + + if ($include) { + $included = $false + foreach ($inclusion in $include) { + if ($file.FullName -like $inclusion) { + $included = $true + break + } + } + + if (-not $included) { continue filterFiles } + } + + $file + } + ) + #region Filter Filters + + # Go over each file we want to archive + :packingFiles foreach ($file in $filteredFiles) { + # get each file as a relative uri, and then get it's bytes + $relativeUri = $file.FullName.Substring($resolvedItem.FullName.Length) -replace '[\\/]', '/' + $fileBytes = Get-Content -AsByteStream -Raw -LiteralPath $file.FullName + + # If the file was blank + if (-not $fileBytes) { + # write a message to verbose indicating we are skipping the file. + Write-Verbose "Skipping blank file $($file.FullName)" + continue + } + + # encode our URI, + $encodedUri = [Web.HttpUtility]::UrlEncode($relativeUri) -replace + # but don't forget to fix spaces and decode slashes + '\+', '%20' -replace '%2f', '/' + # (our relative URI may already contain them) + + $relativeUri = '/' + ($encodedUri -replace '^/') + + # Determine the right content type for the extension + $fileContentType = $typeMap[$file.Extension] + # and fall back to text/plain + if (-not $fileContentType) { $fileContentType = 'text/plain'} + + # Then update our creation times / last write times as needed. + if ($file.CreationTime -lt $oldestCreationTime) { + $oldestCreationTime = $file.CreationTime + } + if ($file.LastWriteTime -gt $lastWriteTime) { + $lastWriteTime = $file.LastWriteTime + } + + # Write our progress message + $progress.PercentComplete = (++$counter * 100 / $total) + $Progress.Status = "$relativeUri" + Write-Progress @Progress + + if (-not $fileBytes) { continue } + + # Try to create a new part + try { + $newPart = $package.CreatePart($relativeUri, $fileContentType, $CompressionOption) + } catch { + # If that didn't work, + $ex = $_ + # at least one exception has some well known answers + if ($ex.Exception.HResult -eq 0x80131501) { + # Open Packaging Conventions do not allow case-sensitive collisions + # (most likely because they would not extract well on all operating systems) + # If we find an exception indicating a conflict, and multiple copies + $multipleCopies = @($filesToArchive | + Where-Object Fullname -ieq $file.FullName | + Select-Object -ExpandProperty Fullname) + if ($multipleCopies.Count) { + # warn the user + Write-Warning "Skipping '$($File.Fullname)' - Case Sensitivity conflict between:$( + @( + [Environment]::NewLine + # and provide a helpful pair of paths so they can resolve the conflict + $multipleCopies + ) -join + [Environment]::NewLine + ) $ex" + } else { + # If there were not multiple files, error (but do not return) + Write-Error -ErrorRecord $ex + } + } else { + # and if the error was unknown, error (but do not return) + Write-Error -ErrorRecord $ex + } + } + + # If we could not create the part, continue + if (-not $newPart) { continue } + $newStream = $newPart.GetStream() + $newStream.Write($fileBytes, 0, $fileBytes.Length) + $newStream.Close() + $null = $newStream.DisposeAsync() + } + + Pop-Location + + $Progress.Remove('PercentComplete') + $Progress.Completed = $true + Write-Progress @Progress + $package.PackageProperties.Created = $oldestCreationTime + $package.PackageProperties.Modified = $lastWriteTime + $package +} + + + \ No newline at end of file From 95230807728103892b2373b9153330815806533f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 4 Mar 2026 20:01:21 +0000 Subject: [PATCH 226/724] feat: `OpenPackage.GetDirectory` ( Fixes #116 ) --- OP.types.ps1xml | 693 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 693 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index c499c27..0574144 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -311,6 +311,237 @@ $matchingParts = $partBytes } } + + + + GetDirectory + @@ -2248,6 +2479,237 @@ $matchingParts = $partBytes } } + + + + GetDirectory + @@ -4185,6 +4647,237 @@ $matchingParts = $partBytes } } + + + + GetDirectory + From dfac79ec4147bd555630af0da5de259f2884d3e6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 4 Mar 2026 13:00:04 -0800 Subject: [PATCH 227/724] feat: `OpenPackage.get_PowerShellManifest` ( Fixes #34 ) Iterating over each part rather than failing when the manifest is the only file --- Types/OpenPackage/get_PowerShellManifest.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage/get_PowerShellManifest.ps1 b/Types/OpenPackage/get_PowerShellManifest.ps1 index 5b5a223..cda482b 100644 --- a/Types/OpenPackage/get_PowerShellManifest.ps1 +++ b/Types/OpenPackage/get_PowerShellManifest.ps1 @@ -12,8 +12,9 @@ [OutputType([PSObject])] param() -# Get every part -foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { +foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch '\.psd1$') { continue } + $psd1 = $part.Read() if (-not $psd1.ModuleVersion) { continue } $psd1 -} +} \ No newline at end of file From 9b1bc76fedee0f6d7598c52b0acdb34a5c351426 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 4 Mar 2026 21:00:25 +0000 Subject: [PATCH 228/724] feat: `OpenPackage.get_PowerShellManifest` ( Fixes #34 ) Iterating over each part rather than failing when the manifest is the only file --- OP.types.ps1xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0574144..c3e1635 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1855,12 +1855,12 @@ foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { [OutputType([PSObject])] param() -# Get every part -foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { +foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch '\.psd1$') { continue } + $psd1 = $part.Read() if (-not $psd1.ModuleVersion) { continue } $psd1 } - @@ -4023,12 +4023,12 @@ foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { [OutputType([PSObject])] param() -# Get every part -foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { +foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch '\.psd1$') { continue } + $psd1 = $part.Read() if (-not $psd1.ModuleVersion) { continue } $psd1 } - @@ -6191,12 +6191,12 @@ foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { [OutputType([PSObject])] param() -# Get every part -foreach ($psd1 in $this.GetContent($this.FileList -match '\.psd1$')) { +foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch '\.psd1$') { continue } + $psd1 = $part.Read() if (-not $psd1.ModuleVersion) { continue } $psd1 } - From 0b9bbb7bb50fe7a05ea98348c556c555c7226d8f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 4 Mar 2026 13:05:53 -0800 Subject: [PATCH 229/724] feat: `OpenPackage.GetTar` ( Fixes #117 ) --- Commands/Get-OpenPackage.ps1 | 171 ++++++++++++----------------------- Types/OpenPackage/GetTar.ps1 | 149 ++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+), 113 deletions(-) create mode 100644 Types/OpenPackage/GetTar.ps1 diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 547e26f..5a8b8be 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -531,7 +531,7 @@ function Get-OpenPackage Push-Location -LiteralPath $resolvedItem.FullName # Get all files beneath this point $namedParameters['Directory'] = $_ - InvokeOpMethod 'GetDirectory' $NamedParameters + InvokeOpMethod 'GetDirectory' $NamedParameters return } @@ -539,132 +539,77 @@ function Get-OpenPackage # If we are creating a package from a file $resolvedItem = $_ - try { - # try to open it as an archive first. - # If this was nothing like a .zip file, this will throw an exception. - $resolvedItem.FullName | packZip - } catch { - # The file is not a zip. - # Before we make a package for a single file - # Check if it was a `.tar.gz` file, first. - $packageFromTar = $resolvedItem | packTar - - if ($packageFromTar) { - return $packageFromTar - } - - # Read the file content - $fileBytes = Get-Content -AsByteStream -Raw -LiteralPath $resolvedItem.FullName - - # Get our current package - $currentPackage = getCurrentPack + $peekMagicBytes = Get-Content -AsByteStream -LiteralPath $resolvedItem.FullName -First 5 + + if ($peekMagicBytes[0,1] -as 'char[]' -join '' -eq 'PK') { + return $resolvedItem.FullName | packZip + } + elseif ( + $peekMagicBytes[0,1] -as 'char[]' -join '' -eq './' + ) { + return $resolvedItem | packTar + } + elseif ($peekMagicBytes[0] -eq 31 -and + $peekMagicBytes[1] -eq 139 -and + $peekMagicBytes[2] -eq 8 + ) { + return $resolvedItem | packTar + } + - # Make the file name an encoded URI - $relativeUri = [Web.HttpUtility]::UrlEncode($resolvedItem.Name) -replace '\+', '%20' - $relativeUri = '/' + ($relativeUri -replace '^/') - $relativeUri = $relativeUri -replace '\s', '%20' + # Read the file content + $fileBytes = Get-Content -AsByteStream -Raw -LiteralPath $resolvedItem.FullName + + # Get our current package + $currentPackage = getCurrentPack + + # Make the file name an encoded URI + $relativeUri = [Web.HttpUtility]::UrlEncode($resolvedItem.Name) -replace '\+', '%20' + $relativeUri = '/' + ($relativeUri -replace '^/') + $relativeUri = $relativeUri -replace '\s', '%20' - # If the package has no identifier, - if (-not $currentPackage.PackageProperties.Identifier) { - # set it to the file name - $currentPackage.PackageProperties.Identifier = $resolvedItem.Name - } - + # If the package has no identifier, + if (-not $currentPackage.PackageProperties.Identifier) { + # set it to the file name $currentPackage.PackageProperties.Identifier = $resolvedItem.Name - - # And determine the right extension content type - - $fileContentType = $typeMap[$resolvedItem.Extension] - - # and create a part - $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, $CompressionOption) - if (-not $newPart) { - continue - } - # then write the file to the part - $newStream = $newPart.GetStream() - $newStream.Write($fileBytes, 0, $fileBytes.Length) - $newStream.Close() - - # and set the package properties based off of the resolved info. - $currentPackage.PackageProperties.Created = $resolvedItem.CreationTime - $currentPackage.PackageProperties.Modified = $resolvedItem.LastWriteTime - $currentPackage } + + $currentPackage.PackageProperties.Identifier = $resolvedItem.Name + + # And determine the right extension content type + + $fileContentType = $typeMap[$resolvedItem.Extension] + + # and create a part + $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, $CompressionOption) + if (-not $newPart) { + continue + } + # then write the file to the part + $newStream = $newPart.GetStream() + $newStream.Write($fileBytes, 0, $fileBytes.Length) + $newStream.Close() + + # and set the package properties based off of the resolved info. + $currentPackage.PackageProperties.Created = $resolvedItem.CreationTime + $currentPackage.PackageProperties.Modified = $resolvedItem.LastWriteTime + $currentPackage } filter packRepo { - $Repository = $_ + $NamedParameters['Repository'] = $Repository = $_ InvokeOpMethod 'GetRepository' $NamedParameters return } filter packTar { - - $resolvedItem = $_ - # First lets peek at the magic bytes that might give us a clue - $peekMagicBytes = Get-Content -AsByteStream -LiteralPath $resolvedItem.FullName -First 5 - - # If it starts with 31, 139, and 8, it may be a gzipped tarball - if (-not ($peekMagicBytes -and - $peekMagicBytes[0] -eq 31 -and - $peekMagicBytes[1] -eq 139 -and - $peekMagicBytes[2] -eq 8)) { - return - } - - # Put tar files into their own subdirectory - $tarDestination = Join-Path $myAppData 'tar' - if (-not (Test-Path $tarDestination)) { - $newDir = New-Item -ItemType Directory -Path $tarDestination -Force - if (-not $newDir) { return } - } + + # Get all files beneath this point + $namedParameters['TarFile'] = $_ + InvokeOpMethod 'GetTar' $NamedParameters - # and each destination in its own subdirectory - $thisTarDestination = Join-Path $tarDestination ($resolvedItem.Name -replace '.\tar\.gz$') - if (-not (Test-Path $thisTarDestination)) { - $newDir = New-Item -ItemType Directory -Path $thisTarDestination -Force - if (-not $newDir) { return } - } - - # If the engine supports tarfiles - if ('Formats.Tar.TarFile' -as [Type]) { - # read the file as a stream - $openFile = [IO.File]::OpenRead($resolvedItem.FullName) - # and read that as a decompressed gzip stream - $gzipStream = [IO.Compression.GZipStream]::new($openFile, [IO.Compression.CompressionMode]'Decompress') - if (-not $gzipStream) { - $openFile.Close() - } - - # and extract the tarfile to that directory - [Formats.Tar.TarFile]::ExtractToDirectory($gzipStream, $thisTarDestination, $true) - # Track it it worked - $worked = $? - # and close up - $gzipStream.Close() - $openFile.Close() - # If it worked - if ($worked) { - # pack that directory - return Get-Item -LiteralPath "$thisTarDestination" | packDir - } - # otherwise, keep going - } elseif ($( - $tarApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('tar', 'Application') - $tarApp - )) { - # Alternatively, if the tar application installed, we can use that - $null = & $tarApp -xvf $resolvedItem.FullName -C "$thisTarDestination" - if ($?) { - # and then pack the directory - return Get-Item -LiteralPath "$thisTarDestination" | packDir - } - } else { - # If neither of those paths work, write a warning. - Write-Warning "$($resolvedItem.FullName) is a tar gz, but Formats.Tar.TarFile is not loaded and `tar` app does not exist" - } + return } filter packUri { diff --git a/Types/OpenPackage/GetTar.ps1 b/Types/OpenPackage/GetTar.ps1 new file mode 100644 index 0000000..7671438 --- /dev/null +++ b/Types/OpenPackage/GetTar.ps1 @@ -0,0 +1,149 @@ +<# +.SYNOPSIS + Gets a tarfile as a package +.DESCRIPTION + Gets a tarfile as an open package. +#> +param( +# The path to a tarfile. +[Parameter(Mandatory,ValueFromPipelineByPropertyName)] +[Alias('clone_url')] +[string] +$TarFile, + +# A list of file wildcards to include. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Include, + +# A list of file wildcards to exclude. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Exclude, + +# A content type map. +# This maps extensions and URIs to a content type. +[Collections.IDictionary] +$TypeMap = $( + ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap +), + +# The compression option. +[IO.Packaging.CompressionOption] +[Alias('CompressionLevel')] +$CompressionOption = 'Superfast', + +# If set, will force the redownload of various resources and remove existing files or directories +[switch] +$Force, + +# If set, will include hidden files and folders, except for files beneath `.git` +[Alias('IncludeDotFiles')] +[switch] +$IncludeHidden, + +# If set, will include the `.git` directory contents if found. +[switch] +$IncludeGit, + +# If set, will include any content found in `/node_modules`. +[Alias('IncludeNodeModules')] +[switch] +$IncludeNodeModule +) + +$namedParameters = [Ordered]@{} +foreach ($key in $namedParameters.Keys) { + $var = $ExecutionContext.SessionState.PSVariable.Get($key) + if ($var) { + $namedParameters[$key] = $var.value + } +} +$namedParameters.Remove('Repository') + +$myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage +# Put tar files into their own subdirectory + +foreach ($resolvedItem in Get-Item -Path $TarFile) { + # First lets peek at the magic bytes that might give us a clue + $peekMagicBytes = Get-Content -AsByteStream -LiteralPath $resolvedItem.FullName -First 5 + + $tarDestination = Join-Path $myAppData 'tar' + if (-not (Test-Path $tarDestination)) { + $newDir = New-Item -ItemType Directory -Path $tarDestination -Force + if (-not $newDir) { return } + } + + # and each destination in its own subdirectory + $thisTarDestination = Join-Path $tarDestination ($resolvedItem.Name -replace '\.gz$' -replace '\.tar$') + if (-not (Test-Path $thisTarDestination)) { + $newDir = New-Item -ItemType Directory -Path $thisTarDestination -Force + if (-not $newDir) { return } + } + + $tarFormat = + if ( + $peekMagicBytes[0] -as [char] -eq '.' -and + $peekMagicBytes[1] -as [char] -eq '/' + ) { + # classic tarfile without gzipping + 'tar' + } elseif ($peekMagicBytes -and + $peekMagicBytes[0] -eq 31 -and + $peekMagicBytes[1] -eq 139 -and + $peekMagicBytes[2] -eq 8 + ) { + # gzipped tarball + 'tar.gz' + } else { + Write-Error "$($resolvedItem.Name) does not appear to be a .tar or .tar.gz file" + continue + } + + # If the engine supports tarfiles + if ('Formats.Tar.TarFile' -as [Type]) { + # read the file as a stream + $openFile = [IO.File]::OpenRead($resolvedItem.FullName) + if ($tarFormat -eq 'tar.gz') { + # and read that as a decompressed gzip stream + $gzipStream = [IO.Compression.GZipStream]::new($openFile, [IO.Compression.CompressionMode]'Decompress') + if (-not $gzipStream) { + $openFile.Close() + } + [Formats.Tar.TarFile]::ExtractToDirectory($gzipStream, $thisTarDestination, $true) + # Track if it worked + $worked = $? + # and close up + $gzipStream.Close() + $null = $gzipStream.DisposeAsync() + $openFile.Close() + $null = $openFile.DisposeAsync() + } else { + [Formats.Tar.TarFile]::ExtractToDirectory($openFile, $thisTarDestination, $true) + $worked = $? + $openFile.Close() + $null = $openFile.DisposeAsync() + } + + # If it worked + if ($worked) { + # pack that directory + Get-Item -LiteralPath "$thisTarDestination" | Get-OpenPackage @namedParameters + } + } elseif ($( + $tarApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('tar', 'Application') + $tarApp + )) { + # Alternatively, if the tar application installed, we can use that + $null = & $tarApp -xvf $resolvedItem.FullName -C "$thisTarDestination" + if ($?) { + # and then pack the directory + Get-Item -LiteralPath "$thisTarDestination" | Get-OpenPackage @namedParameters + } + } else { + # If neither of those paths work, write a warning. + Write-Warning "$($resolvedItem.FullName) is a tar gz, but Formats.Tar.TarFile is not loaded and `tar` app does not exist" + } +} From 697244f93401346558244375f1a797da53060822 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 4 Mar 2026 21:06:09 +0000 Subject: [PATCH 230/724] feat: `OpenPackage.GetTar` ( Fixes #117 ) --- OP.types.ps1xml | 465 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index c3e1635..1723b42 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -811,6 +811,161 @@ else { } + + GetTar + + Match + + GetTar + + Match + + GetTar + + Match + + GetZip + + Match + + GetZip + + Match + + GetZip + + Match + + GetDictionary + + GetDirectory + + GetDictionary + + GetDirectory + + GetDictionary + + GetDirectory + + + GetUrl + @@ -3765,6 +3976,217 @@ foreach ($resolvedItem in Get-Item -Path $TarFile) { } } + + + + GetUrl + @@ -6402,6 +6824,217 @@ foreach ($resolvedItem in Get-Item -Path $TarFile) { } } + + + + GetUrl + From 6d998e06f0c9540fed0953d3ab6225e898ba2165 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 5 Mar 2026 20:40:05 -0800 Subject: [PATCH 247/724] feat: `OpenPackage.GetTar` ( Fixes #117 ) Adding -BasePath support --- Types/OpenPackage/GetTar.ps1 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage/GetTar.ps1 b/Types/OpenPackage/GetTar.ps1 index 7671438..0d36317 100644 --- a/Types/OpenPackage/GetTar.ps1 +++ b/Types/OpenPackage/GetTar.ps1 @@ -7,7 +7,6 @@ param( # The path to a tarfile. [Parameter(Mandatory,ValueFromPipelineByPropertyName)] -[Alias('clone_url')] [string] $TarFile, @@ -23,6 +22,11 @@ $Include, [string[]] $Exclude, +# The base path within the package. +# Content should be added beneath this base path. +[string] +$BasePath = '/', + # A content type map. # This maps extensions and URIs to a content type. [Collections.IDictionary] @@ -55,13 +59,13 @@ $IncludeNodeModule ) $namedParameters = [Ordered]@{} -foreach ($key in $namedParameters.Keys) { +foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { $var = $ExecutionContext.SessionState.PSVariable.Get($key) if ($var) { $namedParameters[$key] = $var.value } } -$namedParameters.Remove('Repository') +$namedParameters.Remove('TarFile') $myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage # Put tar files into their own subdirectory From 17a72e5e706d9bd9f4f89778a76ab804f14b2387 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 6 Mar 2026 04:40:25 +0000 Subject: [PATCH 248/724] feat: `OpenPackage.GetTar` ( Fixes #117 ) Adding -BasePath support --- OP.types.ps1xml | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 597871d..8aca58a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -987,7 +987,6 @@ else { param( # The path to a tarfile. [Parameter(Mandatory,ValueFromPipelineByPropertyName)] -[Alias('clone_url')] [string] $TarFile, @@ -1003,6 +1002,11 @@ $Include, [string[]] $Exclude, +# The base path within the package. +# Content should be added beneath this base path. +[string] +$BasePath = '/', + # A content type map. # This maps extensions and URIs to a content type. [Collections.IDictionary] @@ -1035,13 +1039,13 @@ $IncludeNodeModule ) $namedParameters = [Ordered]@{} -foreach ($key in $namedParameters.Keys) { +foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { $var = $ExecutionContext.SessionState.PSVariable.Get($key) if ($var) { $namedParameters[$key] = $var.value } } -$namedParameters.Remove('Repository') +$namedParameters.Remove('TarFile') $myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage # Put tar files into their own subdirectory @@ -3835,7 +3839,6 @@ else { param( # The path to a tarfile. [Parameter(Mandatory,ValueFromPipelineByPropertyName)] -[Alias('clone_url')] [string] $TarFile, @@ -3851,6 +3854,11 @@ $Include, [string[]] $Exclude, +# The base path within the package. +# Content should be added beneath this base path. +[string] +$BasePath = '/', + # A content type map. # This maps extensions and URIs to a content type. [Collections.IDictionary] @@ -3883,13 +3891,13 @@ $IncludeNodeModule ) $namedParameters = [Ordered]@{} -foreach ($key in $namedParameters.Keys) { +foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { $var = $ExecutionContext.SessionState.PSVariable.Get($key) if ($var) { $namedParameters[$key] = $var.value } } -$namedParameters.Remove('Repository') +$namedParameters.Remove('TarFile') $myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage # Put tar files into their own subdirectory @@ -6683,7 +6691,6 @@ else { param( # The path to a tarfile. [Parameter(Mandatory,ValueFromPipelineByPropertyName)] -[Alias('clone_url')] [string] $TarFile, @@ -6699,6 +6706,11 @@ $Include, [string[]] $Exclude, +# The base path within the package. +# Content should be added beneath this base path. +[string] +$BasePath = '/', + # A content type map. # This maps extensions and URIs to a content type. [Collections.IDictionary] @@ -6731,13 +6743,13 @@ $IncludeNodeModule ) $namedParameters = [Ordered]@{} -foreach ($key in $namedParameters.Keys) { +foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { $var = $ExecutionContext.SessionState.PSVariable.Get($key) if ($var) { $namedParameters[$key] = $var.value } } -$namedParameters.Remove('Repository') +$namedParameters.Remove('TarFile') $myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage # Put tar files into their own subdirectory From 1b1eeb6e2734daf8cb912c9a78917723ed08c582 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 6 Mar 2026 17:59:43 -0800 Subject: [PATCH 249/724] feat: `OpenPackage.Part.ReadText()` ( Fixes #97 ) Adding Dockerfile and Modelfile to text file pattern --- Types/OpenPackage.Part/ReadText.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Part/ReadText.ps1 b/Types/OpenPackage.Part/ReadText.ps1 index 433c20c..882136d 100644 --- a/Types/OpenPackage.Part/ReadText.ps1 +++ b/Types/OpenPackage.Part/ReadText.ps1 @@ -5,9 +5,9 @@ Reads Package Part Content as Text #> [Reflection.AssemblyMetadata( - # This should automatically apply to .txt files + # This should automatically apply to .txt files, modelfiles, and dockerfiles. 'FilePattern', - '\.txt?$' + '(?>[/\.]Dockerfile|[/\.]Modelfile|\.txt)$' )] [Reflection.AssemblyMetadata( # This should automatically apply to any text/ content types From bf1218ac7e8b11d7411b52b1c140df6e44b1fb8f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 6 Mar 2026 18:04:31 -0800 Subject: [PATCH 250/724] feat: `OpenPackage.get_Modelfile` ( Fixes #123 ) --- Types/OpenPackage/get_Modelfile.ps1 | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Types/OpenPackage/get_Modelfile.ps1 diff --git a/Types/OpenPackage/get_Modelfile.ps1 b/Types/OpenPackage/get_Modelfile.ps1 new file mode 100644 index 0000000..0985576 --- /dev/null +++ b/Types/OpenPackage/get_Modelfile.ps1 @@ -0,0 +1,18 @@ +<# +.SYNOPSIS + Gets any Modelfiles in a package +.DESCRIPTION + Gets any Ollama model files within a package +.LINK + https://docs.ollama.com/modelfile +#> +[OutputType([string])] +param() + +$partPattern = '[/\.]Modelfile$' + +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + $part.Read() + } +} \ No newline at end of file From 4a5600429f8c1d756c84f23ca0f25203edc2d811 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 7 Mar 2026 02:04:53 +0000 Subject: [PATCH 251/724] feat: `OpenPackage.get_Modelfile` ( Fixes #123 ) --- OP.types.ps1xml | 81 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 8aca58a..b8a66f2 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2399,6 +2399,29 @@ foreach ($part in $this.GetParts()) { # We are done. + + Modelfile + + <# +.SYNOPSIS + Gets any Modelfiles in a package +.DESCRIPTION + Gets any Ollama model files within a package +.LINK + https://docs.ollama.com/modelfile +#> +[OutputType([string])] +param() + +$partPattern = '[/\.]Modelfile$' + +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + $part.Read() + } +} + + Modified @@ -5251,6 +5274,29 @@ foreach ($part in $this.GetParts()) { # We are done. + + Modelfile + + <# +.SYNOPSIS + Gets any Modelfiles in a package +.DESCRIPTION + Gets any Ollama model files within a package +.LINK + https://docs.ollama.com/modelfile +#> +[OutputType([string])] +param() + +$partPattern = '[/\.]Modelfile$' + +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + $part.Read() + } +} + + Modified @@ -8103,6 +8149,29 @@ foreach ($part in $this.GetParts()) { # We are done. + + Modelfile + + <# +.SYNOPSIS + Gets any Modelfiles in a package +.DESCRIPTION + Gets any Ollama model files within a package +.LINK + https://docs.ollama.com/modelfile +#> +[OutputType([string])] +param() + +$partPattern = '[/\.]Modelfile$' + +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + $part.Read() + } +} + + Modified @@ -8980,9 +9049,9 @@ if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { Reads Package Part Content as Text #> [Reflection.AssemblyMetadata( - # This should automatically apply to .txt files + # This should automatically apply to .txt files, modelfiles, and dockerfiles. 'FilePattern', - '\.txt?$' + '(?>[/\.]Dockerfile|[/\.]Modelfile|\.txt)$' )] [Reflection.AssemblyMetadata( # This should automatically apply to any text/ content types @@ -9522,9 +9591,9 @@ if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { Reads Package Part Content as Text #> [Reflection.AssemblyMetadata( - # This should automatically apply to .txt files + # This should automatically apply to .txt files, modelfiles, and dockerfiles. 'FilePattern', - '\.txt?$' + '(?>[/\.]Dockerfile|[/\.]Modelfile|\.txt)$' )] [Reflection.AssemblyMetadata( # This should automatically apply to any text/ content types @@ -10064,9 +10133,9 @@ if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { Reads Package Part Content as Text #> [Reflection.AssemblyMetadata( - # This should automatically apply to .txt files + # This should automatically apply to .txt files, modelfiles, and dockerfiles. 'FilePattern', - '\.txt?$' + '(?>[/\.]Dockerfile|[/\.]Modelfile|\.txt)$' )] [Reflection.AssemblyMetadata( # This should automatically apply to any text/ content types From 2e79f689d8484203998f7216b12dd6d58f5b8d2f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 6 Mar 2026 18:07:00 -0800 Subject: [PATCH 252/724] fix: `Get-OpenPackage` ( Fixes #2 ) Improving inner splatting --- Commands/Get-OpenPackage.ps1 | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 0854f06..5c3c64c 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -443,7 +443,7 @@ function Get-OpenPackage filter packFile { # If we are creating a package from a file - $resolvedItem = $_ + $resolvedItem = $_ $peekMagicBytes = Get-Content -AsByteStream -LiteralPath $resolvedItem.FullName -First 5 @@ -499,13 +499,7 @@ function Get-OpenPackage $currentPackage.PackageProperties.Created = $resolvedItem.CreationTime $currentPackage.PackageProperties.Modified = $resolvedItem.LastWriteTime $currentPackage - } - - filter packRepo { - $NamedParameters['Repository'] = $_ - InvokeOpMethod 'GetRepository' $NamedParameters - return - } + } filter packTar { $namedParameters['TarFile'] = $_ @@ -555,6 +549,7 @@ function Get-OpenPackage $packages = @() $loadedModules = @(Get-Module) $outputPackages = $true + $namedParameters.Remove('ArgumentList') # Walk over each argument :nextArgument for ($argNumber = 0; $argNumber -lt $ArgumentList.Length; $argNumber++) { @@ -577,6 +572,7 @@ function Get-OpenPackage ) { # see if the path exists $slashKey = $arg -replace '^\.?/?', '/' -replace '/$' + # If the input was a package, and it exists in the package if ( $InputObject -is [IO.Packaging.Package] -and @@ -715,8 +711,9 @@ function Get-OpenPackage } #region Open Package from Repository - if ($Repository) { - $Repository | packRepo + if ($Repository) { + $NamedParameters['Repository'] = $Repository + InvokeOpMethod 'GetRepository' $NamedParameters return } #endregion Open Package from Repository @@ -741,7 +738,8 @@ function Get-OpenPackage if ($module -is [Management.Automation.PSModuleInfo] -and $module.Path) { $namedParameters.Remove('Module') - & $myCommandName -FilePath ($module.Path | Split-Path) @namedParameters + $namedParameters.FilePath = $module.Path | Split-Path + & $myCommandName @namedParameters return } } @@ -761,6 +759,7 @@ function Get-OpenPackage #endregion Open Package from Nuget if ($filePath) { + $namedParameters.Remove('FilePath') # Try to resolve the file path $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath) # If we could not resolve the path, exit @@ -781,8 +780,8 @@ function Get-OpenPackage # We want a package from a directory # Push into that location, for it will make operations easier Push-Location -LiteralPath $resolvedItem.FullName - # Get all files beneath this point - $namedParameters['Directory'] = $_ + # Get all files beneath this point + $namedParameters['Directory'] = $resolvedItem.FullName InvokeOpMethod 'GetDirectory' $NamedParameters Pop-Location # make packages from the directory From de09e2d5cbabe3dfa932bf9838dac49bdf39fea3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 6 Mar 2026 18:10:19 -0800 Subject: [PATCH 253/724] feat: `OpenPackage.GetHierarchy` ( Fixes #121 ) --- Types/OpenPackage/GetHierarchy.ps1 | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 Types/OpenPackage/GetHierarchy.ps1 diff --git a/Types/OpenPackage/GetHierarchy.ps1 b/Types/OpenPackage/GetHierarchy.ps1 new file mode 100644 index 0000000..e189711 --- /dev/null +++ b/Types/OpenPackage/GetHierarchy.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS + Gets the package hierarchy +.DESCRIPTION + Gets the hierarchy of all files in a package matching a pattern. +#> +[OutputType([Collections.IDictionary])] +param( +# The file pattern +[string] +$FilePattern = '.' +) + +if (-not $this.getParts) { return } + +# Get all of the files in the tree. +$filesInTree = @( + foreach ($part in $this.GetParts()) { + if ($part.Uri -match $filePattern) { + $part.Uri + } + } +) + +# If there were no files, return an empty directory. +$fileCount = $filesInTree.Length +if (-not $fileCount) { return [Ordered]@{} } + +$fileTree = [Ordered]@{} +foreach ($relativePath in $filesInTree) { + $hierarchy = @($relativePath -replace '^/' -split '[\\/]' -ne '') + $pointer = $fileTree + for ($index = 0; $index -lt ($hierarchy.Length - 1); $index++) { + $subdirectory = $hierarchy[$index] -replace '_' + if (-not $pointer[$subdirectory]) { + $pointer[$subdirectory] = [Ordered]@{} + } + $pointer = $pointer[$subdirectory] + } + $pointer[$hierarchy[-1]] = $relativePath +} + + +return $fileTree From 6600616534d71881920cb1de83a8daa8ca70145d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 7 Mar 2026 02:10:39 +0000 Subject: [PATCH 254/724] feat: `OpenPackage.GetHierarchy` ( Fixes #121 ) --- OP.types.ps1xml | 150 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index b8a66f2..0d3ffcf 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -694,6 +694,56 @@ foreach ($resolvedItem in $resolvedItems) { + + + + GetHierarchy + @@ -3569,6 +3619,56 @@ foreach ($resolvedItem in $resolvedItems) { + + + + GetHierarchy + @@ -6444,6 +6544,56 @@ foreach ($resolvedItem in $resolvedItems) { + + + + GetHierarchy + From 805c81f5210f7a038d3f6d6b417d7e1147704826 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 7 Mar 2026 11:39:08 -0800 Subject: [PATCH 255/724] feat: `OpenPackage.get_Dockerfile` ( Fixes #35 ) Using Part.Read --- Types/OpenPackage/get_Dockerfile.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Types/OpenPackage/get_Dockerfile.ps1 b/Types/OpenPackage/get_Dockerfile.ps1 index 8ee11c6..0cd0a85 100644 --- a/Types/OpenPackage/get_Dockerfile.ps1 +++ b/Types/OpenPackage/get_Dockerfile.ps1 @@ -4,11 +4,13 @@ .DESCRIPTION Gets the content of any `Dockerfile`s in the package. #> -[OutputType([xml])] +[OutputType([string])] param() $partPattern = '[/\.]DockerFile$' -$this.GetContent(@( - $this.FileList -match $partPattern -)) \ No newline at end of file +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + $part.Read() + } +} \ No newline at end of file From f9b09503a0e86bd30cffc2f9a07c5a0e0b235177 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 7 Mar 2026 19:39:31 +0000 Subject: [PATCH 256/724] feat: `OpenPackage.get_Dockerfile` ( Fixes #35 ) Using Part.Read --- OP.types.ps1xml | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0d3ffcf..6ae126f 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1985,14 +1985,16 @@ $this.PackageProperties.Creator = $Creator .DESCRIPTION Gets the content of any `Dockerfile`s in the package. #> -[OutputType([xml])] +[OutputType([string])] param() $partPattern = '[/\.]DockerFile$' -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + $part.Read() + } +} @@ -4910,14 +4912,16 @@ $this.PackageProperties.Creator = $Creator .DESCRIPTION Gets the content of any `Dockerfile`s in the package. #> -[OutputType([xml])] +[OutputType([string])] param() $partPattern = '[/\.]DockerFile$' -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + $part.Read() + } +} @@ -7835,14 +7839,16 @@ $this.PackageProperties.Creator = $Creator .DESCRIPTION Gets the content of any `Dockerfile`s in the package. #> -[OutputType([xml])] +[OutputType([string])] param() $partPattern = '[/\.]DockerFile$' -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + $part.Read() + } +} From ff35b7212b0e77602459d689eadd11625f5ffa13 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 7 Mar 2026 16:13:02 -0800 Subject: [PATCH 257/724] feat: `OpenPackage.GetAtProto` ( Fixes #125 ) --- Commands/Get-OpenPackage.ps1 | 167 +++---------------------------- Types/OpenPackage/GetAtProto.ps1 | 167 +++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 152 deletions(-) create mode 100644 Types/OpenPackage/GetAtProto.ps1 diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 5c3c64c..8e60cb7 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -290,155 +290,6 @@ function Get-OpenPackage $currentPackage.pstypenames.insert(0, 'OpenPackage') } $currentPackage - } - - filter packAt { - param() - # Capture the current input - $whereAt = $_ - # and declare a pattern to pick apart an at uri - $atPattern = 'at://(?[^/]+)/(?[^/]+)(?:/(?.+?$))?' - - # If this does not match the pattern or we don't have a type, we are done. - if ($whereAt -notmatch $atPattern -or - -not $matches.type) { return } - - # Store our match information before anything else needs to `-match`. - $atMatch = [Ordered]@{} + $Matches - - # Create a package in memory - $currentPackage = getCurrentPack - if (-not $currentPackage.PackageProperties.Identifier) { - $currentPackage.PackageProperties.Identifier = $atMatch.did - } - - # If we have a type and rkey, we are after a single record. - if ($matches.type -and $matches.rkey) { - . packAtRecord - } - else { - . packAtType - } - } - - filter packAtRecord { - # Construct the XRPC url to that record - $xrpcUrl = "https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=$( - $matches.did - )&collection=$( - $matches.type - )&rkey=$( - $matches.rkey - )" - - # and go fetch. - $atRecord = Invoke-RestMethod -Uri $xrpcUrl - - # Declare a package uri for the segment. - # (make sure to switch did colons to something else, so that the files can unpack cleanly regardless of OS) - $currentPackageUri = "/$($matches.did -replace ':','_')/$($matches.type)/$($matches.rkey).json" - - # If the part exists, - if ($currentPackage.PartExists($currentPackageUri)) { - $currentPackage.DeletePart($currentPackageUri) # recreate it. - } - $atPart = $currentPackage.CreatePart($currentPackageUri, 'application/json', $CompressionOption) - - # Get the stream. - $atStream = $atPart.GetStream() - # Turn our message into json, and get the bytes. - $atJsonBytes = $outputEncoding.GetBytes( - ($atRecord | ConvertTo-Json -Depth 100) - ) - - # Then write them to the stream, - $atStream.Write($atJsonBytes, 0, $atJsonBytes.Count) - - # clean up, - $atStream.Close() - $atStream.Dispose() - - # and emit the result - $currentPackage - } - - filter packAtType { - # Otherwise, we are getting everything of a type - $total = [long]0 - $skipped = [long]0 - $cursor = '' - $progress = [Ordered]@{Id = Get-Random} - $BatchSize = 100 - $progress.Status = "Getting records" - :AtSync do { - $xrpcUrl = "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=$( - $atMatch.did - )&collection=$( - $atMatch.type - )&cursor=$Cursor&limit=$BatchSize" - $progress.Activity = "$total " - Write-Progress @progress - # Get the page of records - $results = Invoke-RestMethod $xrpcUrl - # If we got results and have a cursor to more - if ($results -and $results.cursor) { - # set it for the next round. - $Cursor = $results.cursor - } - - # Unroll and store each record. - :nextRecord foreach ($record in $results.records) { - - # Records are sent latest to earliest. - # We can use -Skip to skip N records - if ($PSCmdlet.PagingParameters.Skip -and - $skipped -lt $PSCmdlet.PagingParameters.Skip - ) { - $skipped++ - continue nextRecord - } - - # If the uri is not an at uri - if ($record.uri -notmatch $atPattern) { - # continue to the next record - continue nextRecord - } - - # Construct the URI within the package. - $currentPackageUri = "/$($atMatch.did -replace ':','-')/$($matches.type)/$($matches.rkey).json" - - # Create or recreate the part for the at content - $atPart = if ($currentPackage.PartExists($currentPackageUri)) { - $currentPackage.DeletePart($currentPackageUri) - $currentPackage.CreatePart($currentPackageUri, 'application/json', $CompressionOption) - } else { - $currentPackage.CreatePart($currentPackageUri, 'application/json', $CompressionOption) - } - - # Store the json - $atStream = $atPart.GetStream() - $atJsonBytes = $outputEncoding.GetBytes( - ($record | ConvertTo-Json -Depth 100) - ) - $atStream.Write($atJsonBytes, 0, $atJsonBytes.Count) - # and clean up - $atStream.Close() - $atStream.Dispose() - - # Increment our total - $total++ - - # If we provided -First and our total exceeds our -First, break out - if ($PSCmdlet.PagingParameters.First -and - $total -ge $PSCmdlet.PagingParameters.First) { - break AtSync - } - } - } while ($results -and $results.cursor) - - $progress.Completed = $true - Write-Progress @progress - $currentPackage } filter packFile { @@ -469,9 +320,9 @@ function Get-OpenPackage $currentPackage = getCurrentPack # Make the file name an encoded URI - $relativeUri = [Web.HttpUtility]::UrlEncode($resolvedItem.Name) -replace '\+', '%20' + $relativeUri = [Web.HttpUtility]::UrlEncode($resolvedItem.Name) $relativeUri = '/' + ($relativeUri -replace '^/') - $relativeUri = $relativeUri -replace '\s', '%20' + # $relativeUri = $relativeUri -replace '\s', '%20' # If the package has no identifier, if (-not $currentPackage.PackageProperties.Identifier) { @@ -516,6 +367,12 @@ function Get-OpenPackage process { $namedParameters = [Ordered]@{} + $PSBoundParameters + if ($PSCmdlet.PagingParameters.First) { + $namedParameters['First'] = $psCmdlet.PagingParameters.First + } + if ($PSCmdlet.PagingParameters.Skip) { + $namedParameters['Skip'] = $psCmdlet.PagingParameters.Skip + } $messageData = [Ordered]@{} + $PSBoundParameters $getOpenPackageEvent = New-Event -SourceIdentifier Get-OpenPackage -MessageData ( @@ -700,6 +557,10 @@ function Get-OpenPackage return # and return. } + + if ($inputObject -is [IO.Packaging.Package]) { + $namedParameters['Package'] = $InputObject + } # If we are passed a uri if ($Uri) { @@ -720,7 +581,9 @@ function Get-OpenPackage #region Open Package from At Uri if ($AtUri) { - $AtUri | packAt + $NamedParameters['AtUri'] = $AtUri + + InvokeOpMethod 'GetAtProto' $NamedParameters return } #endregion Open Package From At Uri diff --git a/Types/OpenPackage/GetAtProto.ps1 b/Types/OpenPackage/GetAtProto.ps1 new file mode 100644 index 0000000..901ed15 --- /dev/null +++ b/Types/OpenPackage/GetAtProto.ps1 @@ -0,0 +1,167 @@ +<# +.SYNOPSIS + Gets at protocol data +.DESCRIPTION + Gets data from the at protocol. +.EXAMPLE + Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile +#> +param( +[Parameter(Mandatory)] +[string[]] +$AtUri, + +# A list of file wildcards to include. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Include, + +# A list of file wildcards to exclude. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Exclude, + +# The base path within the package. +# Content should be added beneath this base path. +[string] +$BasePath = '/', + +# A content type map. +# This maps extensions and URIs to a content type. +[Collections.IDictionary] +$TypeMap = $( + ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap +), + +# The compression option. +[IO.Packaging.CompressionOption] +[Alias('CompressionLevel')] +$CompressionOption = 'Superfast', + +[uri] +$pds = "https://bsky.social", + +# The current package +[IO.Packaging.Package] +$Package, + +[ValidateRange(1,100)] +[int] +$BatchSize = 100, + +[long] +$First, + +[long] +$Skip +) + + +if (-not $package) { + $memoryStream = [IO.MemoryStream]::new() + $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') + $package.pstypenames.insert(0, 'OP') + $package.pstypenames.insert(0, 'OpenPackage') + Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force +} + +if (-not $this) {$this = $package} + +filter packAtProtoRecord { + # Declare a package uri for the segment. + # (make sure to switch did colons to something else, so that the files can unpack cleanly regardless of OS) + $currentPackageUri = "/$($atMatch.did -replace ':','_')/$($matches.type)/$($matches.rkey).json" + + # If the part exists, + if ($Package.PartExists($currentPackageUri)) { + $Package.DeletePart($currentPackageUri) # recreate it. + if (-not $?) { return } + } + $atPart = $Package.CreatePart($currentPackageUri, 'application/json', $CompressionOption) + if (-not $atPart) { continue } + # Get the stream. + $atStream = $atPart.GetStream() + if (-not $atStream) { continue } + # Turn our message into json, and get the bytes. + $atJsonBytes = $outputEncoding.GetBytes( + ($atRecord | ConvertTo-Json -Depth 100) + ) + + # Then write them to the stream, + $atStream.Write($atJsonBytes, 0, $atJsonBytes.Count) + + # clean up, + $atStream.Close() + $atStream.Dispose() +} + +foreach ($at in $AtUri) { + # and declare a pattern to pick apart an at uri + $atPattern = 'at://(?[^/]+)/(?[^/]+)(?:/(?.+?$))?' + + # If this does not match the pattern or we don't have a type, we are done. + if ($at -notmatch $atPattern -or + -not $matches.type) { return } + + # Store our match information before anything else needs to `-match`. + $atMatch = [Ordered]@{} + $Matches + # Create a package in memory + + if (-not $package.PackageProperties.Identifier) { + $package.PackageProperties.Identifier = $atMatch.did + } + + # If we have a type and rkey, we are after a single record. + if ($atMatch.type -and $atMatch.rkey) { + $atRecord = $this.GetAtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds) + if (-not $atRecord) { continue } + packAtProtoRecord + } + elseif ($atMatch.type -match '\.') { + # If there are dots in the type, it is a collection + foreach ($atRecord in $this.GetAtType( + $atMatch.did, $atMatch.type, $BatchSize, $First, $Skip, $pds + )) { + # If the uri is not an at uri + if ($atRecord.uri -notmatch $atPattern) { + # continue to the next record + continue + } + packAtProtoRecord + } + } + else + { + try { + $atBlob = $this.GetAtBlob($matches.did, $matches.type) + $atContentType = $atBlob.Headers.'Content-Type' + if (-not $atContentType) { + continue + } + $currentPackageUri = "/$($matches.did -replace ':','_')/$($matches.type).$(@($atContentType -split '[/\+]')[-1])" + $blobPart = $Package.CreatePart($currentPackageUri, $atContentType, $CompressionOption) + $blobStream = $blobPart.GetStream() + $memoryStream = + if ($atBlob.Content -is [byte[]]) { + [IO.MemoryStream]::new($atBlob.Content) + } else { + [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($atBlob.Content)) + } + $memoryStream.CopyTo($blobStream) + $memoryStream.Close() + $null = $memoryStream.DisposeAsync() + + $blobStream.Close() + $null = $blobStream.DisposeAsync() + + } catch { + Write-Debug "Unable to get $at : $_" + continue + } + } +} + + +return $Package From 21669be1c3bd8e4fcf85ffa7032c639f18468b02 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 8 Mar 2026 00:14:29 +0000 Subject: [PATCH 258/724] feat: `OpenPackage.GetAtProto` ( Fixes #125 ) --- OP.types.ps1xml | 519 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 519 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 6ae126f..e6d3b86 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -57,6 +57,179 @@ return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did& + + + + GetAtProto + @@ -2984,6 +3157,179 @@ return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did& + + + + GetAtProto + @@ -5911,6 +6257,179 @@ return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did& + + + + GetAtProto + From 559eb3e8f05aef1efceed479732415a01585efe8 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 7 Mar 2026 16:29:51 -0800 Subject: [PATCH 259/724] feat: `OpenPackage.GetTree` ( Fixes #121 ) Renaming and returning nodes --- Types/OpenPackage/{GetHierarchy.ps1 => GetTree.ps1} | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) rename Types/OpenPackage/{GetHierarchy.ps1 => GetTree.ps1} (75%) diff --git a/Types/OpenPackage/GetHierarchy.ps1 b/Types/OpenPackage/GetTree.ps1 similarity index 75% rename from Types/OpenPackage/GetHierarchy.ps1 rename to Types/OpenPackage/GetTree.ps1 index e189711..7b7c183 100644 --- a/Types/OpenPackage/GetHierarchy.ps1 +++ b/Types/OpenPackage/GetTree.ps1 @@ -1,8 +1,8 @@ <# .SYNOPSIS - Gets the package hierarchy + Gets the package tree .DESCRIPTION - Gets the hierarchy of all files in a package matching a pattern. + Gets the tree of all files in a package matching a pattern. #> [OutputType([Collections.IDictionary])] param( @@ -17,7 +17,7 @@ if (-not $this.getParts) { return } $filesInTree = @( foreach ($part in $this.GetParts()) { if ($part.Uri -match $filePattern) { - $part.Uri + $part } } ) @@ -27,17 +27,18 @@ $fileCount = $filesInTree.Length if (-not $fileCount) { return [Ordered]@{} } $fileTree = [Ordered]@{} -foreach ($relativePath in $filesInTree) { +foreach ($filePart in $filesInTree) { + $relativePath = $filePart.Uri $hierarchy = @($relativePath -replace '^/' -split '[\\/]' -ne '') $pointer = $fileTree for ($index = 0; $index -lt ($hierarchy.Length - 1); $index++) { - $subdirectory = $hierarchy[$index] -replace '_' + $subdirectory = $hierarchy[$index] if (-not $pointer[$subdirectory]) { $pointer[$subdirectory] = [Ordered]@{} } $pointer = $pointer[$subdirectory] } - $pointer[$hierarchy[-1]] = $relativePath + $pointer[$hierarchy[-1]] = $filePart } From 815fa1719144cdb99dac1666d88153ee8e373879 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 8 Mar 2026 00:30:14 +0000 Subject: [PATCH 260/724] feat: `OpenPackage.GetTree` ( Fixes #121 ) Renaming and returning nodes --- OP.types.ps1xml | 303 ++++++++++++++++++++++++------------------------ 1 file changed, 153 insertions(+), 150 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index e6d3b86..c94a70a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -867,56 +867,6 @@ foreach ($resolvedItem in $resolvedItems) { - - - - GetHierarchy - @@ -1357,6 +1307,57 @@ foreach ($resolvedItem in Get-Item -Path $TarFile) { + + GetTree + + GetUrl - - - GetHierarchy - @@ -4457,6 +4408,57 @@ foreach ($resolvedItem in Get-Item -Path $TarFile) { + + GetTree + + GetUrl - - - GetHierarchy - @@ -7557,6 +7509,57 @@ foreach ($resolvedItem in Get-Item -Path $TarFile) { + + GetTree + + GetUrl + + ShowTreeHtml + + Astro @@ -4961,6 +5018,63 @@ else { $partStream.Close() + + ShowTreeHtml + + Astro @@ -8062,6 +8176,63 @@ else { $partStream.Close() + + ShowTreeHtml + + Astro From f44e9346c9cdb0961e9d117e6e4ac3c26893403c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 7 Mar 2026 16:35:17 -0800 Subject: [PATCH 263/724] feat: `OpenPackage.ShowTreeHtml` ( Fixes #122 ) Fixing link --- Types/OpenPackage/ShowTreeHtml.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Types/OpenPackage/ShowTreeHtml.ps1 b/Types/OpenPackage/ShowTreeHtml.ps1 index fe2a6f4..08277e5 100644 --- a/Types/OpenPackage/ShowTreeHtml.ps1 +++ b/Types/OpenPackage/ShowTreeHtml.ps1 @@ -34,7 +34,7 @@ filter toIndex { "" } else { "
  • " - "" + "" [Web.HttpUtility]::HtmlEncode($in.Key) "" "
  • " From 438164a299613a6785af210258987dd11392973c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 8 Mar 2026 00:35:35 +0000 Subject: [PATCH 264/724] feat: `OpenPackage.ShowTreeHtml` ( Fixes #122 ) Fixing link --- OP.types.ps1xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 48c318e..e1bccb9 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1899,7 +1899,7 @@ filter toIndex { "</details>" } else { "<li>" - "<a href='$($in.Value -replace '+', '%20')'>" + "<a href='$($in.Value.Uri -replace '+', '%20')'>" [Web.HttpUtility]::HtmlEncode($in.Key) "</a>" "</li>" @@ -5057,7 +5057,7 @@ filter toIndex { "</details>" } else { "<li>" - "<a href='$($in.Value -replace '+', '%20')'>" + "<a href='$($in.Value.Uri -replace '+', '%20')'>" [Web.HttpUtility]::HtmlEncode($in.Key) "</a>" "</li>" @@ -8215,7 +8215,7 @@ filter toIndex { "</details>" } else { "<li>" - "<a href='$($in.Value -replace '+', '%20')'>" + "<a href='$($in.Value.Uri -replace '+', '%20')'>" [Web.HttpUtility]::HtmlEncode($in.Key) "</a>" "</li>" From 03e6677dc03201749f4c3c1b66e75061a1f74deb Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 7 Mar 2026 16:40:23 -0800 Subject: [PATCH 265/724] feat: `OpenPackage.ShowTreeHtml` ( Fixes #122 ) Fixing link and using GetTree --- Types/OpenPackage/ShowTreeHtml.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage/ShowTreeHtml.ps1 b/Types/OpenPackage/ShowTreeHtml.ps1 index 08277e5..1cc7480 100644 --- a/Types/OpenPackage/ShowTreeHtml.ps1 +++ b/Types/OpenPackage/ShowTreeHtml.ps1 @@ -17,7 +17,7 @@ $Style = @" "@ ) -$fileTree = $this.GetHierarchy($FilePattern) +$fileTree = $this.GetTree($FilePattern) filter toIndex { $in = $_ @@ -34,7 +34,7 @@ filter toIndex { "" } else { "
  • " - "" + "" [Web.HttpUtility]::HtmlEncode($in.Key) "" "
  • " From 3c86bba2c989cba37db256f4ce17dc948001018c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 8 Mar 2026 00:40:53 +0000 Subject: [PATCH 266/724] feat: `OpenPackage.ShowTreeHtml` ( Fixes #122 ) Fixing link and using GetTree --- OP.types.ps1xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index e1bccb9..0ecca20 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1882,7 +1882,7 @@ $Style = @" "@ ) -$fileTree = $this.GetHierarchy($FilePattern) +$fileTree = $this.GetTree($FilePattern) filter toIndex { $in = $_ @@ -1899,7 +1899,7 @@ filter toIndex { "</details>" } else { "<li>" - "<a href='$($in.Value.Uri -replace '+', '%20')'>" + "<a href='$($in.Value.Uri -replace '\+', '%20')'>" [Web.HttpUtility]::HtmlEncode($in.Key) "</a>" "</li>" @@ -5040,7 +5040,7 @@ $Style = @" "@ ) -$fileTree = $this.GetHierarchy($FilePattern) +$fileTree = $this.GetTree($FilePattern) filter toIndex { $in = $_ @@ -5057,7 +5057,7 @@ filter toIndex { "</details>" } else { "<li>" - "<a href='$($in.Value.Uri -replace '+', '%20')'>" + "<a href='$($in.Value.Uri -replace '\+', '%20')'>" [Web.HttpUtility]::HtmlEncode($in.Key) "</a>" "</li>" @@ -8198,7 +8198,7 @@ $Style = @" "@ ) -$fileTree = $this.GetHierarchy($FilePattern) +$fileTree = $this.GetTree($FilePattern) filter toIndex { $in = $_ @@ -8215,7 +8215,7 @@ filter toIndex { "</details>" } else { "<li>" - "<a href='$($in.Value.Uri -replace '+', '%20')'>" + "<a href='$($in.Value.Uri -replace '\+', '%20')'>" [Web.HttpUtility]::HtmlEncode($in.Key) "</a>" "</li>" From 90306fb5f51358e6cfb9ba2919b89a3cd1554b8f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 10 Mar 2026 23:31:59 -0700 Subject: [PATCH 267/724] fix: `Get-OpenPackage` ( Fixes #2 ) Improving inner splatting (not propagating -First / -Skip if over 1mb) --- Commands/Get-OpenPackage.ps1 | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 8e60cb7..2df7e5e 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -3,6 +3,12 @@ function Get-OpenPackage <# .SYNOPSIS Gets Open Packages + .LINK + https://en.wikipedia.org/wiki/Open_Packaging_Conventions + .INPUTS + Almost anything + .OUTPUTS + Open Packages .DESCRIPTION Gets Open Packages from a path, uri, or repository and runs related commands. @@ -75,7 +81,7 @@ function Get-OpenPackage # Get the most recent 50 posts $atLast50 = op at://mrpowershell.com/app.bsky.feed.post/ -First 50 .EXAMPLE - # Get all standard.site.documents + # Get all standard.site.documents for a user $standardSiteDocuments = op at://mrpowershell.com/site.standard.document/ #> [CmdletBinding(PositionalBinding=$false,DefaultParameterSetName='Any',SupportsPaging)] @@ -367,10 +373,10 @@ function Get-OpenPackage process { $namedParameters = [Ordered]@{} + $PSBoundParameters - if ($PSCmdlet.PagingParameters.First) { + if ($PSCmdlet.PagingParameters.First -lt 1mb) { $namedParameters['First'] = $psCmdlet.PagingParameters.First } - if ($PSCmdlet.PagingParameters.Skip) { + if ($PSCmdlet.PagingParameters.Skip -lt 1mb) { $namedParameters['Skip'] = $psCmdlet.PagingParameters.Skip } $messageData = [Ordered]@{} + $PSBoundParameters From 50c0affaf57b1e590d2ac5d0d3e3c61befcf6b16 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 11 Mar 2026 12:39:11 -0700 Subject: [PATCH 268/724] fix: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding content types --- Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 index 873788d..5c4f969 100644 --- a/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 +++ b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 @@ -27,6 +27,7 @@ '.html' = 'text/html' '.ico' = 'image/vnd.microsoft.icon' '.ics' = 'text/calendar' + '.ini' = 'text/plain' '.jar' = 'application/java-archive' '.jpeg' = 'image/jpeg' '.jpg' = 'image/jpeg' @@ -68,14 +69,16 @@ '.tiff' = 'image/tiff' '.ts' = 'application/x-typescript' '.ttf' = 'font/ttf' + '.ttl' = 'text/turtle' '.txt' = 'text/plain' + '.url' = 'text/plain' '.vbproj' = 'application/xml' - '.vsd' = 'application/vnd.visio' + '.vsd' = 'application/vnd.visio' '.wav' = 'audio/wav' '.weba' = 'audio/webm' '.webm' = 'video/webm' '.webp' = 'image/webp' - '.webmanifest' = 'application/manifest+json' + '.webmanifest' = 'application/manifest+json' '.woff' = 'font/woff' '.woff2' = 'font/woff2' '.xhtml' = 'application/xhtml+xml' @@ -85,7 +88,7 @@ '.xsd' = 'text/xsd' '.xsl' = 'text/xsl' '.xul' = 'application/vnd.mozilla.xul+xml' - '.zip' = 'application/zip' + '.zip' = 'application/zip' '.3gp' = 'video/3gpp' '.3g2' = 'video/3gpp2' '.7z' = 'application/x-7z-compressed' From 3b88157a49d04fc0c2c958350e007175de835a58 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 11 Mar 2026 19:39:29 +0000 Subject: [PATCH 269/724] fix: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding content types --- OP.types.ps1xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0ecca20..c6e23c1 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -9509,6 +9509,7 @@ FileList '.html' = 'text/html' '.ico' = 'image/vnd.microsoft.icon' '.ics' = 'text/calendar' + '.ini' = 'text/plain' '.jar' = 'application/java-archive' '.jpeg' = 'image/jpeg' '.jpg' = 'image/jpeg' @@ -9550,14 +9551,16 @@ FileList '.tiff' = 'image/tiff' '.ts' = 'application/x-typescript' '.ttf' = 'font/ttf' + '.ttl' = 'text/turtle' '.txt' = 'text/plain' + '.url' = 'text/plain' '.vbproj' = 'application/xml' - '.vsd' = 'application/vnd.visio' + '.vsd' = 'application/vnd.visio' '.wav' = 'audio/wav' '.weba' = 'audio/webm' '.webm' = 'video/webm' '.webp' = 'image/webp' - '.webmanifest' = 'application/manifest+json' + '.webmanifest' = 'application/manifest+json' '.woff' = 'font/woff' '.woff2' = 'font/woff2' '.xhtml' = 'application/xhtml+xml' @@ -9567,7 +9570,7 @@ FileList '.xsd' = 'text/xsd' '.xsl' = 'text/xsl' '.xul' = 'application/vnd.mozilla.xul+xml' - '.zip' = 'application/zip' + '.zip' = 'application/zip' '.3gp' = 'video/3gpp' '.3g2' = 'video/3gpp2' '.7z' = 'application/x-7z-compressed' From 036b8b8b731f433c1c993d9b5c7c6ffa0a0a0034 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 11 Mar 2026 15:31:09 -0700 Subject: [PATCH 270/724] feat: `OpenPackage.get_Cache` ( Fixes #126 ) --- Types/OpenPackage/get_Cache.ps1 | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Types/OpenPackage/get_Cache.ps1 diff --git a/Types/OpenPackage/get_Cache.ps1 b/Types/OpenPackage/get_Cache.ps1 new file mode 100644 index 0000000..d6354ef --- /dev/null +++ b/Types/OpenPackage/get_Cache.ps1 @@ -0,0 +1,17 @@ +<# +.SYNOPSIS + Gets cache +.DESCRIPTION + Gets an open package's cache. + + This is an ordered dictionary of data attached to the object, but not saved to disk. +#> +param() + +if (-not $this) { return } + +if (-not $this.'#Cache') { + Add-Member -InputObject $this NoteProperty '#Cache' ([Ordered]@{}) -Force +} + +return $this.'#Cache' \ No newline at end of file From fc4b9562aec7d9a6ac5f7b8b85a8d4a81af7114c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 11 Mar 2026 22:31:26 +0000 Subject: [PATCH 271/724] feat: `OpenPackage.get_Cache` ( Fixes #126 ) --- OP.types.ps1xml | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index c6e23c1..a2210e8 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1932,6 +1932,28 @@ param() $this.GetContent($this.FileList -match '\.astro$')
    + + Cache + + <# +.SYNOPSIS + Gets cache +.DESCRIPTION + Gets an open package's cache. + + This is an ordered dictionary of data attached to the object, but not saved to disk. +#> +param() + +if (-not $this) { return } + +if (-not $this.'#Cache') { + Add-Member -InputObject $this NoteProperty '#Cache' ([Ordered]@{}) -Force +} + +return $this.'#Cache' + + Category @@ -5090,6 +5112,28 @@ param() $this.GetContent($this.FileList -match '\.astro$') + + Cache + + <# +.SYNOPSIS + Gets cache +.DESCRIPTION + Gets an open package's cache. + + This is an ordered dictionary of data attached to the object, but not saved to disk. +#> +param() + +if (-not $this) { return } + +if (-not $this.'#Cache') { + Add-Member -InputObject $this NoteProperty '#Cache' ([Ordered]@{}) -Force +} + +return $this.'#Cache' + + Category @@ -8248,6 +8292,28 @@ param() $this.GetContent($this.FileList -match '\.astro$') + + Cache + + <# +.SYNOPSIS + Gets cache +.DESCRIPTION + Gets an open package's cache. + + This is an ordered dictionary of data attached to the object, but not saved to disk. +#> +param() + +if (-not $this) { return } + +if (-not $this.'#Cache') { + Add-Member -InputObject $this NoteProperty '#Cache' ([Ordered]@{}) -Force +} + +return $this.'#Cache' + + Category From 4d68be95f2bacc70e72282f6eb62cd3cc093dbb6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 12 Mar 2026 13:24:45 -0700 Subject: [PATCH 272/724] feat: `OpenPackage.GetUrl` ( Fixes #119 ) Returning package and allowing Get-OpenPackage to pass multiple urls --- Commands/Get-OpenPackage.ps1 | 46 ++++++++++++++++++++++++++---------- Types/OpenPackage/GetUrl.ps1 | 2 +- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 2df7e5e..e8f4a9d 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -350,7 +350,7 @@ function Get-OpenPackage # then write the file to the part $newStream = $newPart.GetStream() $newStream.Write($fileBytes, 0, $fileBytes.Length) - $newStream.Close() + $newStream.Close() # and set the package properties based off of the resolved info. $currentPackage.PackageProperties.Created = $resolvedItem.CreationTime @@ -369,6 +369,7 @@ function Get-OpenPackage } $myNoun = $MyInvocation.MyCommand.Name -replace '^.+?-' + $generateEvent = [Runspace]::DefaultRunspace.Events.GenerateEvent } process { @@ -379,12 +380,33 @@ function Get-OpenPackage if ($PSCmdlet.PagingParameters.Skip -lt 1mb) { $namedParameters['Skip'] = $psCmdlet.PagingParameters.Skip } - $messageData = [Ordered]@{} + $PSBoundParameters - $getOpenPackageEvent = - New-Event -SourceIdentifier Get-OpenPackage -MessageData ( - $messageData - ) -Sender $MyInvocation.MyCommand -EventArguments $MyInvocation, $messageData - + $messageData = [Ordered]@{} + $namedParameters + # Generate an event + $getOpenPackageEvent = $generateEvent.Invoke( + 'Get-OpenPackage', # for Get-OpenPackage + $MyInvocation.MyCommand, # sent by this command + @( + # containing MyInvocation and MessageData + $MyInvocation, $messageData + ), + # And sending the message data dictionary along + $messageData, + # process in the current thread + $true, + # and wait for completion. + $true + ) + + # If the event was processed, and they said any form of "no" + if ($getOpenPackageEvent.MessageData.Rejected -or + $getOpenPackageEvent.MessageData.Reject -or + $getOpenPackageEvent.MessageData.No -or + $getOpenPackageEvent.MessageData.Deny + ) { + Write-Warning "Will not $($MyInvocation.Line)" + return + } + if ($InputObject) { $namedParameters.Remove('InputObject') switch ($InputObject) { @@ -442,19 +464,19 @@ function Get-OpenPackage $InputObject.PartExists($slashKey) ) { # read the contents of the part and emit them to output - & $myCommandName -Uri $slashKey @namedParameters -InputObject $InputObject + Get-OpenPackage -Uri $slashKey @namedParameters -InputObject $InputObject # and continue to the next argument. continue nextArgument } if (Test-Path $arg -ErrorAction Ignore) { # and if it does, turn it into a package - $packages += & $myCommandName -FilePath $arg @namedParameters + $packages += Get-OpenPackage -FilePath $arg @namedParameters continue nextArgument } # If the argument started with at:// if ($arg -match '^at://') { # treat it as an at uri and turn it into a package. - $packages += & $myCommandName -AtUri $arg @namedParameters + $packages += Get-OpenPackage -AtUri $arg @namedParameters continue nextArgument } # If the argument could be a URI @@ -462,14 +484,14 @@ function Get-OpenPackage # and that URI is absolute if ($argUri.IsAbsoluteUri) { # get that uri as a package - $packages += & $myCommandName -Uri $argUri @namedParameters + $packages += Get-OpenPackage -Uri $argUri @namedParameters continue nextArgument } # The the argument is a string and the name of a loaded module if ($arg -is [string] -and $loadedModules.Name -contains $arg) { - $packages += & $myCommandName -Module $arg @namedParameters + $packages += Get-OpenPackage -Module $arg @namedParameters continue nextArgument } diff --git a/Types/OpenPackage/GetUrl.ps1 b/Types/OpenPackage/GetUrl.ps1 index a1043c8..b9b2280 100644 --- a/Types/OpenPackage/GetUrl.ps1 +++ b/Types/OpenPackage/GetUrl.ps1 @@ -198,7 +198,7 @@ foreach ($uri in $url) { $newStream.Close() $newStream.Dispose() - $currentPackage + $package } } From eb31d65abb4d0fedbb44c3e77104cad4de5f66ec Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 12 Mar 2026 20:25:04 +0000 Subject: [PATCH 273/724] feat: `OpenPackage.GetUrl` ( Fixes #119 ) Returning package and allowing Get-OpenPackage to pass multiple urls --- OP.types.ps1xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index a2210e8..e946e0f 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1561,7 +1561,7 @@ foreach ($uri in $url) { $newStream.Close() $newStream.Dispose() - $currentPackage + $package } } @@ -4741,7 +4741,7 @@ foreach ($uri in $url) { $newStream.Close() $newStream.Dispose() - $currentPackage + $package } } @@ -7921,7 +7921,7 @@ foreach ($uri in $url) { $newStream.Close() $newStream.Dispose() - $currentPackage + $package } } From 6f37ba27e90ab633cb47ac825d2ff8ed0af2e87e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 13 Mar 2026 13:56:31 -0700 Subject: [PATCH 274/724] feat: `OpenPackage.GetUrl` ( Fixes #119 ) Improving multiple url support --- Commands/Get-OpenPackage.ps1 | 43 ++++++++++++++++++++++++++++-------- Types/OpenPackage/GetUrl.ps1 | 11 +++++---- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index e8f4a9d..45171d7 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -108,7 +108,7 @@ function Get-OpenPackage # If this URI is a nuget package url or powershell gallery url, will download the package. [Parameter(Mandatory,ParameterSetName='Uri',ValueFromPipelineByPropertyName)] [Alias('Url')] - [uri] + [uri[]] $Uri, # A Repository to package. @@ -187,6 +187,7 @@ function Get-OpenPackage # One or more input objects. [Parameter(ValueFromPipeline)] + [Alias('Package')] [PSObject] $InputObject, @@ -438,10 +439,23 @@ function Get-OpenPackage # Walk over each argument :nextArgument for ($argNumber = 0; $argNumber -lt $ArgumentList.Length; $argNumber++) { + # If we have already output a package + if ($packages -and + # but it was not explicitly mapped + -not $namedParameters.'package' -and + $packages[0] -is [IO.Packaging.Package] + ) { + # map the package to the named parameters. + $namedParameters.'package' = $packages[0] + } + $arg = $ArgumentList[$argNumber] # If it is a string, path info, or uri if ($arg -is [Management.Automation.PSModuleInfo]) { - $packages += & $myCommandName -Module $arg @namedParameters + $modulePackage = Get-OpenPackage -Module $arg @namedParameters + if ($packages -notcontains $modulePackage) { + $packages += $modulePackage + } # and continue to the next argument. continue nextArgument } @@ -468,15 +482,23 @@ function Get-OpenPackage # and continue to the next argument. continue nextArgument } + # If the argument is a path if (Test-Path $arg -ErrorAction Ignore) { - # and if it does, turn it into a package - $packages += Get-OpenPackage -FilePath $arg @namedParameters + # turn it into a package. + $filePackage = Get-OpenPackage -FilePath $arg @namedParameters + if ($packages -notcontains $filePackage) { + $packages += $filePackage + } + continue nextArgument } # If the argument started with at:// if ($arg -match '^at://') { # treat it as an at uri and turn it into a package. - $packages += Get-OpenPackage -AtUri $arg @namedParameters + $atPackage = Get-OpenPackage -AtUri $arg @namedParameters + if ($packages -notcontains $atPackage) { + $packages += $atPackage + } continue nextArgument } # If the argument could be a URI @@ -484,7 +506,10 @@ function Get-OpenPackage # and that URI is absolute if ($argUri.IsAbsoluteUri) { # get that uri as a package - $packages += Get-OpenPackage -Uri $argUri @namedParameters + $uriPackage = Get-OpenPackage -Uri $argUri @namedParameters + if ($packages -notcontains $uriPackage) { + $packages += $uriPackage + } continue nextArgument } @@ -574,8 +599,8 @@ function Get-OpenPackage # Pipe those packages into the verb with any arguments we have mapped $packages | & $verbExists @unnamedArguments @namedParameters # and continue to the next argument to this command. - continue nextArgument - } + continue nextArgument + } } # If we did not run any additional commands, @@ -644,7 +669,7 @@ function Get-OpenPackage #region Open Package from Nuget if ($Nuget) { # Try to call our GetNuget method, and pass the `$Nuget` - InvokeOpMethod 'GetNuget' $PSBoundParameters + InvokeOpMethod 'GetNuget' $PSBoundParameters return } #endregion Open Package from Nuget diff --git a/Types/OpenPackage/GetUrl.ps1 b/Types/OpenPackage/GetUrl.ps1 index b9b2280..35d381f 100644 --- a/Types/OpenPackage/GetUrl.ps1 +++ b/Types/OpenPackage/GetUrl.ps1 @@ -196,10 +196,13 @@ foreach ($uri in $url) { } $newStream.Close() - $newStream.Dispose() - - $package - } + $newStream.Dispose() + } } +# If there is a current package +if ($package) { + $package # output it at the end + # (this avoids returning the same package multiple times) +} From 3493f855521cb701d64ae5b7285ac627534242ec Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 13 Mar 2026 20:56:52 +0000 Subject: [PATCH 275/724] feat: `OpenPackage.GetUrl` ( Fixes #119 ) Improving multiple url support --- OP.types.ps1xml | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index e946e0f..22ef41e 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1559,12 +1559,15 @@ foreach ($uri in $url) { } $newStream.Close() - $newStream.Dispose() - - $package - } + $newStream.Dispose() + } } +# If there is a current package +if ($package) { + $package # output it at the end + # (this avoids returning the same package multiple times) +} @@ -4739,12 +4742,15 @@ foreach ($uri in $url) { } $newStream.Close() - $newStream.Dispose() - - $package - } + $newStream.Dispose() + } } +# If there is a current package +if ($package) { + $package # output it at the end + # (this avoids returning the same package multiple times) +} @@ -7919,12 +7925,15 @@ foreach ($uri in $url) { } $newStream.Close() - $newStream.Dispose() - - $package - } + $newStream.Dispose() + } } +# If there is a current package +if ($package) { + $package # output it at the end + # (this avoids returning the same package multiple times) +} From b194142dd6098cf9e5dac32fc7601e1a690e3d61 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 13 Mar 2026 16:46:28 -0700 Subject: [PATCH 276/724] feat: `OpenPackage.get_Lexicon` ( Fixes #64 ) Improving fault tolerance --- Types/OpenPackage/get_Lexicon.ps1 | 45 ++++++++++++++----------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/Types/OpenPackage/get_Lexicon.ps1 b/Types/OpenPackage/get_Lexicon.ps1 index c94bbf1..33cd07e 100644 --- a/Types/OpenPackage/get_Lexicon.ps1 +++ b/Types/OpenPackage/get_Lexicon.ps1 @@ -10,7 +10,7 @@ * `lexicon` * `defs - The output will be a table mapping ids to defs. + The output will be a table mapping ids to contents. #> [OutputType([Ordered])] param() @@ -21,30 +21,25 @@ $allLexicons = [Ordered]@{} foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '\.json$') { continue } - - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $fromJson = $readStream | ConvertFrom-Json - if ($fromJson.lexicon -and - $fromJson.id -and - $fromJson.defs - ) { - $allLexicons[$fromJson.id] = $fromJson.defs - } - + if ($part.Uri -match 'package-lock') { continue } + + if (-not $part.Reader) { continue } + + try { + $partData = $part.Read() + + if ($partData -is [Object[]]) { continue } + + if ($partData.lexicon -and + $partData.id -and + $partData.defs + ) { + $allLexicons[$partData.id] = $partData + } + } catch { + Write-Warning "$($part.Uri) read error $($_)" + continue + } } return $allLexicons # We are done. \ No newline at end of file From 9f9102df397ef1d395fbc1b19f5db161f8fa9aee Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 13 Mar 2026 23:46:50 +0000 Subject: [PATCH 277/724] feat: `OpenPackage.get_Lexicon` ( Fixes #64 ) Improving fault tolerance --- OP.types.ps1xml | 117 +++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 66 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 22ef41e..0244076 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2628,7 +2628,7 @@ $this.PackageProperties.LastPrinted = $LastPrinted * `lexicon` * `defs - The output will be a table mapping ids to defs. + The output will be a table mapping ids to contents. #> [OutputType([Ordered])] param() @@ -2639,30 +2639,25 @@ $allLexicons = [Ordered]@{} foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '\.json$') { continue } + if ($part.Uri -match 'package-lock') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() + if (-not $part.Reader) { continue } - # Convert the part from json. - $fromJson = $readStream | ConvertFrom-Json - if ($fromJson.lexicon -and - $fromJson.id -and - $fromJson.defs - ) { - $allLexicons[$fromJson.id] = $fromJson.defs - } + try { + $partData = $part.Read() + + if ($partData -is [Object[]]) { continue } + if ($partData.lexicon -and + $partData.id -and + $partData.defs + ) { + $allLexicons[$partData.id] = $partData + } + } catch { + Write-Warning "$($part.Uri) read error $($_)" + continue + } } return $allLexicons # We are done. @@ -5811,7 +5806,7 @@ $this.PackageProperties.LastPrinted = $LastPrinted * `lexicon` * `defs - The output will be a table mapping ids to defs. + The output will be a table mapping ids to contents. #> [OutputType([Ordered])] param() @@ -5822,30 +5817,25 @@ $allLexicons = [Ordered]@{} foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '\.json$') { continue } + if ($part.Uri -match 'package-lock') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) + if (-not $part.Reader) { continue } - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $fromJson = $readStream | ConvertFrom-Json - if ($fromJson.lexicon -and - $fromJson.id -and - $fromJson.defs - ) { - $allLexicons[$fromJson.id] = $fromJson.defs - } + try { + $partData = $part.Read() + + if ($partData -is [Object[]]) { continue } + if ($partData.lexicon -and + $partData.id -and + $partData.defs + ) { + $allLexicons[$partData.id] = $partData + } + } catch { + Write-Warning "$($part.Uri) read error $($_)" + continue + } } return $allLexicons # We are done. @@ -8994,7 +8984,7 @@ $this.PackageProperties.LastPrinted = $LastPrinted * `lexicon` * `defs - The output will be a table mapping ids to defs. + The output will be a table mapping ids to contents. #> [OutputType([Ordered])] param() @@ -9005,30 +8995,25 @@ $allLexicons = [Ordered]@{} foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '\.json$') { continue } + if ($part.Uri -match 'package-lock') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() + if (-not $part.Reader) { continue } - # Convert the part from json. - $fromJson = $readStream | ConvertFrom-Json - if ($fromJson.lexicon -and - $fromJson.id -and - $fromJson.defs - ) { - $allLexicons[$fromJson.id] = $fromJson.defs - } + try { + $partData = $part.Read() + + if ($partData -is [Object[]]) { continue } + if ($partData.lexicon -and + $partData.id -and + $partData.defs + ) { + $allLexicons[$partData.id] = $partData + } + } catch { + Write-Warning "$($part.Uri) read error $($_)" + continue + } } return $allLexicons # We are done. From 0c0ea3bf0549a68cc0e4cc253037784d800ac139 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 13 Mar 2026 17:25:01 -0700 Subject: [PATCH 278/724] feat: `OpenPackage.get_XRPC` ( Fixes #127 ) --- Types/OpenPackage/get_XRPC.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Types/OpenPackage/get_XRPC.ps1 diff --git a/Types/OpenPackage/get_XRPC.ps1 b/Types/OpenPackage/get_XRPC.ps1 new file mode 100644 index 0000000..bac0b3a --- /dev/null +++ b/Types/OpenPackage/get_XRPC.ps1 @@ -0,0 +1,16 @@ +<# +.SYNOPSIS + Gets package xrpc +.DESCRIPTION + Gets any xrpc parts within the package. +#> +if (-not $this.GetParts) { return } +foreach ($part in $this.GetParts()) { + if ( + $part.Uri -notmatch '/xrpc' -or + $part.Uri -notlike '*.*.*' + ) { + continue + } + $part +} \ No newline at end of file From ee7c8945f5aedf6810113ed68777df5b620b7301 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 14 Mar 2026 00:26:53 +0000 Subject: [PATCH 279/724] feat: `OpenPackage.get_XRPC` ( Fixes #127 ) --- OP.types.ps1xml | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0244076..a0a265f 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3171,6 +3171,27 @@ $this.GetContent(@( + + XRPC + + <# +.SYNOPSIS + Gets package xrpc +.DESCRIPTION + Gets any xrpc parts within the package. +#> +if (-not $this.GetParts) { return } +foreach ($part in $this.GetParts()) { + if ( + $part.Uri -notmatch '/xrpc' -or + $part.Uri -notlike '*.*.*' + ) { + continue + } + $part +} + + DefaultDisplay Identifier @@ -6349,6 +6370,27 @@ $this.GetContent(@(
    + + XRPC + + <# +.SYNOPSIS + Gets package xrpc +.DESCRIPTION + Gets any xrpc parts within the package. +#> +if (-not $this.GetParts) { return } +foreach ($part in $this.GetParts()) { + if ( + $part.Uri -notmatch '/xrpc' -or + $part.Uri -notlike '*.*.*' + ) { + continue + } + $part +} + + DefaultDisplay Identifier @@ -9527,6 +9569,27 @@ $this.GetContent(@(
    + + XRPC + + <# +.SYNOPSIS + Gets package xrpc +.DESCRIPTION + Gets any xrpc parts within the package. +#> +if (-not $this.GetParts) { return } +foreach ($part in $this.GetParts()) { + if ( + $part.Uri -notmatch '/xrpc' -or + $part.Uri -notlike '*.*.*' + ) { + continue + } + $part +} + + DefaultDisplay Identifier From 3b8f0e18e3736ebfbad6d033fceadba088d44ea1 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 13 Mar 2026 17:35:01 -0700 Subject: [PATCH 280/724] feat: `OpenPackage.get_Underscore` ( Fixes #91 ) --- Types/OpenPackage/Alias.psd1 | 3 +++ Types/OpenPackage/get_Underscore.ps1 | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 Types/OpenPackage/get_Underscore.ps1 diff --git a/Types/OpenPackage/Alias.psd1 b/Types/OpenPackage/Alias.psd1 index 69adc8e..7535330 100644 --- a/Types/OpenPackage/Alias.psd1 +++ b/Types/OpenPackage/Alias.psd1 @@ -1,5 +1,8 @@ @{ RemovePart = "DeletePart" Lexicons = "Lexicon" + DestinationPath = 'InstallPath' '11ty' = "Eleventy" + 'Underbar' = 'Underscore' + '_' = 'Underscore' } \ No newline at end of file diff --git a/Types/OpenPackage/get_Underscore.ps1 b/Types/OpenPackage/get_Underscore.ps1 new file mode 100644 index 0000000..f888de9 --- /dev/null +++ b/Types/OpenPackage/get_Underscore.ps1 @@ -0,0 +1,14 @@ +<# +.SYNOPSIS + Gets package underscore files +.DESCRIPTION + Gets underscore files defined in a package. + + These are any files that contain an underscore `_` in their path. + + This returns a series of dictionaries containing the contents of the package +#> +param() + + +return $this.GetTree("/_") From 424ef812c0dffe22700d9963a8aaeaff6de1a77e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 14 Mar 2026 00:35:16 +0000 Subject: [PATCH 281/724] feat: `OpenPackage.get_Underscore` ( Fixes #91 ) --- OP.types.ps1xml | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index a0a265f..f222e42 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -15,10 +15,18 @@ + + _ + Underscore + 11ty Eleventy + + DestinationPath + InstallPath + Lexicons Lexicon @@ -27,6 +35,10 @@ RemovePart DeletePart + + Underbar + Underscore + GetAtBlob + Read + + Hash + + $this.GetHash() + + Reader @@ -10405,6 +10438,33 @@ Reader + + GetHash + + Read + + Hash + + $this.GetHash() + + Reader @@ -10947,6 +11013,33 @@ Reader + + GetHash + + Read + + Hash + + $this.GetHash() + + Reader From 0ef8f37ad4da7654b9f472aba6d58e9ee50058fc Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 13 Mar 2026 20:40:37 -0700 Subject: [PATCH 290/724] feat: `OpenPackage.Part.GetHash` ( Fixes #128 ) Adding Path --- Types/OpenPackage.Part/GetHash.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/GetHash.ps1 b/Types/OpenPackage.Part/GetHash.ps1 index dd6a723..1113ea3 100644 --- a/Types/OpenPackage.Part/GetHash.ps1 +++ b/Types/OpenPackage.Part/GetHash.ps1 @@ -16,7 +16,9 @@ if (-not $this.GetStream) { $readStream = $this.GetStream('Open', 'Read') -Get-FileHash -InputStream $readStream -Algorithm $Algorithm +$fileHash = Get-FileHash -InputStream $readStream -Algorithm $Algorithm +$fileHash.Path = $this.Uri +$fileHash $readStream.Close() $null = $readStream.DisposeAsync() \ No newline at end of file From e7aaf26e756ab49bf5dc37fc3f9fc6bfba542b52 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 13 Mar 2026 20:42:10 -0700 Subject: [PATCH 291/724] feat: `OpenPackage.Part.GetHash` ( Fixes #128 ) Adding Path, returing only .Hash in property form --- Types/OpenPackage.Part/get_Hash.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/get_Hash.ps1 b/Types/OpenPackage.Part/get_Hash.ps1 index 1fbe7de..13e9169 100644 --- a/Types/OpenPackage.Part/get_Hash.ps1 +++ b/Types/OpenPackage.Part/get_Hash.ps1 @@ -1 +1,7 @@ -$this.GetHash() \ No newline at end of file +<# +.SYNOPSIS + Gets the part hash +.DESCRIPTION + Gets the part hash as a string +#> +$this.GetHash().Hash \ No newline at end of file From a1f36943a885c1c8ee69f8145f79e97fa7129771 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 14 Mar 2026 03:42:29 +0000 Subject: [PATCH 292/724] feat: `OpenPackage.Part.GetHash` ( Fixes #128 ) Adding Path, returing only .Hash in property form --- OP.types.ps1xml | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 4a2b0f3..f6e159d 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -9884,7 +9884,9 @@ if (-not $this.GetStream) { $readStream = $this.GetStream('Open', 'Read') -Get-FileHash -InputStream $readStream -Algorithm $Algorithm +$fileHash = Get-FileHash -InputStream $readStream -Algorithm $Algorithm +$fileHash.Path = $this.Uri +$fileHash $readStream.Close() $null = $readStream.DisposeAsync() @@ -10352,7 +10354,13 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp Hash - $this.GetHash() + <# +.SYNOPSIS + Gets the part hash +.DESCRIPTION + Gets the part hash as a string +#> +$this.GetHash().Hash @@ -10459,7 +10467,9 @@ if (-not $this.GetStream) { $readStream = $this.GetStream('Open', 'Read') -Get-FileHash -InputStream $readStream -Algorithm $Algorithm +$fileHash = Get-FileHash -InputStream $readStream -Algorithm $Algorithm +$fileHash.Path = $this.Uri +$fileHash $readStream.Close() $null = $readStream.DisposeAsync() @@ -10927,7 +10937,13 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp Hash - $this.GetHash() + <# +.SYNOPSIS + Gets the part hash +.DESCRIPTION + Gets the part hash as a string +#> +$this.GetHash().Hash @@ -11034,7 +11050,9 @@ if (-not $this.GetStream) { $readStream = $this.GetStream('Open', 'Read') -Get-FileHash -InputStream $readStream -Algorithm $Algorithm +$fileHash = Get-FileHash -InputStream $readStream -Algorithm $Algorithm +$fileHash.Path = $this.Uri +$fileHash $readStream.Close() $null = $readStream.DisposeAsync() @@ -11502,7 +11520,13 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp Hash - $this.GetHash() + <# +.SYNOPSIS + Gets the part hash +.DESCRIPTION + Gets the part hash as a string +#> +$this.GetHash().Hash From 1d543dce86c594e008b3888799ea04daf3e253a8 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 14 Mar 2026 17:36:33 -0700 Subject: [PATCH 293/724] feat: `OpenPackage.ShowTreeText` ( Fixes #129 ) --- Types/OpenPackage/ShowTreeText.ps1 | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Types/OpenPackage/ShowTreeText.ps1 diff --git a/Types/OpenPackage/ShowTreeText.ps1 b/Types/OpenPackage/ShowTreeText.ps1 new file mode 100644 index 0000000..7bd06db --- /dev/null +++ b/Types/OpenPackage/ShowTreeText.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Shows a text package tree +.DESCRIPTION + Shows a package tree as text. +#> +param( +# The file pattern. All files that match this pattern will be urn. +[string] +$FilePattern = '.', + +# The symbol used for roots of the tree +[string] +$RootSymbol = '╮', + +# The symbol used for branches of the tree +[string] +$BranchSymbol = "─", + +# The number of characters a branch should take +[int] +$BranchSize = 2, + +# The symbol used for a leaf of the tree. +[string] +$LeafSymbol = '├' +) + +$fileTree = $this.GetTree($FilePattern) + +$depth = 0 +filter toIndex { + $in = $_ + + if ($in -is [Collections.IDictionary]) { + $in.GetEnumerator() | . toIndex + } elseif ($in.Key -is [string]) { + $depth++ + if ($in.Value -is [Collections.IDictionary]) { + + '' + ( + (' ' * ($BranchSize + $LeafSymbol.Length + $RootSymbol.Length - 1)) * ( + $depth - 1 + ) + ) + $LeafSymbol + ( + $BranchSymbol * $BranchSize + ) + $RootSymbol + ' ' + $in.Key + + $in.Value | . toIndex + } else { + ( + ' ' * ($BranchSize + $LeafSymbol.Length + $RootSymbol.Length - 1) + ) * ($depth - 1) + $LeafSymbol + $in.Key + } + $depth-- + } + +} + +return @( + $fileTree | toIndex +) -join [Environment]::NewLine + From 462dfe7b09d5587c3cffb147ac5efbff705bbf5f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 15 Mar 2026 00:36:51 +0000 Subject: [PATCH 294/724] feat: `OpenPackage.ShowTreeText` ( Fixes #129 ) --- OP.types.ps1xml | 207 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f6e159d..1ce423d 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1930,6 +1930,75 @@ return @( $fileTree | toIndex ) -join [Environment]::NewLine + + + + ShowTreeText + @@ -5161,6 +5230,75 @@ return @( $fileTree | toIndex ) -join [Environment]::NewLine + + + + ShowTreeText + @@ -8392,6 +8530,75 @@ return @( $fileTree | toIndex ) -join [Environment]::NewLine + + + + ShowTreeText + From 4ae9d61570061ece8ee0e8af59ba26440c2a3f3a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 16 Mar 2026 12:58:57 -0700 Subject: [PATCH 295/724] feat: Start-OpenPackage ( Fixes #5 ) Supporting more HTTP verbs. Generating main runspace event. Supporting direct markdown rendering. --- Commands/Start-OpenPackage.ps1 | 162 +++++++++++++++++++++-------- Types/OpenPackage/ShowTreeHtml.ps1 | 4 +- 2 files changed, 122 insertions(+), 44 deletions(-) diff --git a/Commands/Start-OpenPackage.ps1 b/Commands/Start-OpenPackage.ps1 index 148d7ed..f2bc6dc 100644 --- a/Commands/Start-OpenPackage.ps1 +++ b/Commands/Start-OpenPackage.ps1 @@ -67,11 +67,10 @@ function Start-OpenPackage { return } $InitializationScript = { - filter serverError { + filter serverStatus { $errorCode = $_ $response.StatusCode = $errorCode - foreach ($pack in $package) { - # and serve any /405.html we find. + foreach ($pack in $package) { if ($pack.PartExists("/$errorCode.html")) { "/$errorCode.html" | servePart continue nextRequest @@ -156,9 +155,31 @@ function Start-OpenPackage { $response.ContentType = $packagePart.ContentType } + $acceptableTypes = @($request.Headers['Accept'] -split ',') + Write-Host "Accepts $($request.Headers['Accept'])" -ForegroundColor Cyan + $partStream = $packagePart.GetStream('Open', 'Read') + if ($packagePart.ContentType -eq 'text/markdown' -and + $acceptableTypes[0] -ne 'text/markdown') { + + $streamReader = [IO.StreamReader]::new($partStream, [Text.Encoding]::UTF8) + + $markdownHtml = (ConvertFrom-Markdown -InputObject ( + $streamReader.ReadToEnd() + )).Html + + $response.ContentType = 'text/html' + $response.Close([Text.Encoding]::UTF8.GetBytes("$markdownHtml"), $false) + $streamReader.Close() + $streamReader.Dispose() + $partStream.Close() + $partStream.Dispose() + return + } + + Write-Host "Now Serving $($request.HttpMethod) $uriPart as $($response.ContentType)" -ForegroundColor Cyan - $partStream = $packagePart.GetStream('Open', 'Read') + if ($partStream.Length -lt $BufferSize) { $partStream.CopyTo($response.OutputStream) $partStream.Close() @@ -246,49 +267,106 @@ function Start-OpenPackage { } # Get our listener context - $context = $getContextAsync.Result + $context = $getContextAsync.Result + # and break that into a result and response - $request, $response = $context.Request, $context.Response + $request, $response = $context.Request, $context.Response + + $MessageData = [Ordered]@{ + Url = $request.Url + Context = $context + Request = $request + Response = $response + Package = $package + Handled = $false + } + if ($parentRunspace) { + $requestEvent = $parentRunspace.Events.GenerateEvent( + $request.Url.Scheme, + $httpListener, + @($request, $response, $context), + $MessageData, + $false, + $true + ) + } + + # If the request has no output stream or it was handled by an event + if (-not $request.OutputStream -or $MessageData.Handled) { + # continue to the next request + continue nextRequest + } - $requestTime = [DateTime]::Now + $requestTime = $requestEvent.TimeGenerated Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] $($request.HttpMethod) $($request.Url)" # If they asked for an inappropriate method if ($request.HttpMethod -notin $Allow) { # use the appropriate status code Write-Host -ForegroundColor Red "[$($requestTime.ToString('o'))] 405 $($request.HttpMethod) $($request.Url)" - 405 | serverError + 405 | serverStatus # and continue to the next request continue nextRequest - } - + } - if ($request.HttpMethod -in 'put', 'post' -and - $request.InputStream.CanRead - ) { - $requestDataPath = - $request.Url.LocalPath,"/$($request.HttpMethod)/$($request.RequestTraceIdentifier)" -join - '' -replace '//', '/' - $memoryStream = [IO.MemoryStream]::new() - $request.InputStream.CopyTo($memoryStream) + # If we're allowing additional methods, we can easily do CRUD operations + switch -regex ($request.HttpMethod) { + # put or post changes file content + 'put|post' { + $anythingChanged = $false - foreach ($pack in $package) { - if (-not $pack.PartExists($requestDataPath)) { - $newPart = $pack.CreatePart($requestDataPath, $request.ContentType, 'Normal') - $newStream = $newPart.GetStream() - $memoryStream.CopyTo($newStream) - $newStream.Close() - $newStream.Dispose() + $memoryStream = [IO.MemoryStream]::new() + + if ($request.InputStream.CanRead) { + $request.InputStream.CopyTo($memoryStream) + } + + foreach ($pack in $package) { + if ($pack.FileOpenAccess -ne 'ReadWrite') { + continue + } + + $partStream = + if ($pack.PartExists($request.Url.LocalPath)) { + $pack.GetStream() + } else { + $newPart = $pack.CreatePart($request.Url.LocalPath, $request.ContentType, 'Superfast') + $newPart.GetStream() + } + + $null = $memoryStream.Seek(0, 'begin') + $partStream.SetLength($memoryStream.Length) + $memoryStream.CopyTo($partStream) + $partStream.Close() + $partStream.Dispose() + $anythingChanged = $true break } + + if ($anythingChanged -and + $request.HttpMethod -eq 'put') { + 201 | serverStatus + continue nextRequest + } } - $response.Close() - $memoryStream.Close() - $memoryStream.Dispose() - - continue nextRequest - } + 'delete' { + $anythingDeleted = $false + foreach ($pack in $package) { + if ($pack.FileOpenAccess -eq 'ReadWrite' -and $pack.PartExists( + $request.Url.LocalPath + )) { + $pack.DeletePart($request.Url.LocalPath) + $anythingDeleted = $true + break + } + } + if ($anythingDeleted) { + 204 | serverStatus + continue nextRequest + } + } + } $foundPart = . findPart @@ -300,25 +378,23 @@ function Start-OpenPackage { $uriPart = ($request.Url.LocalPath -replace '/$') + '/' if ($uriPart -match '/$') { - Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] $($request.HttpMethod) $($request.Url) Missing index, generating" - if ($nestedParts) { - $listing = $pack.ShowTreeHtml([Regex]::Escape($request.Url.LocalPath)) - if ($listing -is [string]) { - $response.ContentType = 'text/html' - $response.Close($OutputEncoding.GetBytes($listing), $false) - } else { - $response.Close() - } - - continue nextRequest + Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] $($request.HttpMethod) $($request.Url) Missing index, generating" + $listing = $pack.ShowTreeHtml([Regex]::Escape($request.Url.LocalPath)) + if ($listing -is [string]) { + $response.ContentType = 'text/html' + $response.Close($OutputEncoding.GetBytes($listing), $false) + } else { + $response.Close() } + + continue nextRequest } else { Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] Marco $($request.HttpMethod) $($request.Url)" } # If we did not find a part, set the appropriate status code - 404 | serverError + 404 | serverStatus } } $generateEvent = [Runspace]::DefaultRunspace.Events.GenerateEvent diff --git a/Types/OpenPackage/ShowTreeHtml.ps1 b/Types/OpenPackage/ShowTreeHtml.ps1 index 1cc7480..655bc34 100644 --- a/Types/OpenPackage/ShowTreeHtml.ps1 +++ b/Types/OpenPackage/ShowTreeHtml.ps1 @@ -34,7 +34,9 @@ filter toIndex { "" } else { "
  • " - "" + "" [Web.HttpUtility]::HtmlEncode($in.Key) "" "
  • " From f1a74dacaf7e36f99a5c5a063ddd3cb38c1dd3c5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 16 Mar 2026 20:02:01 +0000 Subject: [PATCH 296/724] feat: Start-OpenPackage ( Fixes #5 ) Supporting more HTTP verbs. Generating main runspace event. Supporting direct markdown rendering. --- OP.types.ps1xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 1ce423d..b4b3993 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1914,7 +1914,9 @@ filter toIndex { "</details>" } else { "<li>" - "<a href='$($in.Value.Uri -replace '\+', '%20')'>" + "<a href='$([Web.HttpUtility]::HtmlAttributeEncode(( + $in.Value.Uri -replace '\+', '%20' + )))'>" [Web.HttpUtility]::HtmlEncode($in.Key) "</a>" "</li>" @@ -5214,7 +5216,9 @@ filter toIndex { "</details>" } else { "<li>" - "<a href='$($in.Value.Uri -replace '\+', '%20')'>" + "<a href='$([Web.HttpUtility]::HtmlAttributeEncode(( + $in.Value.Uri -replace '\+', '%20' + )))'>" [Web.HttpUtility]::HtmlEncode($in.Key) "</a>" "</li>" @@ -8514,7 +8518,9 @@ filter toIndex { "</details>" } else { "<li>" - "<a href='$($in.Value.Uri -replace '\+', '%20')'>" + "<a href='$([Web.HttpUtility]::HtmlAttributeEncode(( + $in.Value.Uri -replace '\+', '%20' + )))'>" [Web.HttpUtility]::HtmlEncode($in.Key) "</a>" "</li>" From 065c04a0d627ce80a0150aeb4ab213eecb478bf8 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 16 Mar 2026 18:35:40 -0700 Subject: [PATCH 297/724] feat: `Get-OpenPackage` ( Fixes #2 ) No longer looking for related commands (simply getting packages should be relatively safe) --- Commands/Get-OpenPackage.ps1 | 84 ++---------------------------------- 1 file changed, 3 insertions(+), 81 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 45171d7..dc3b5ec 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -10,7 +10,7 @@ function Get-OpenPackage .OUTPUTS Open Packages .DESCRIPTION - Gets Open Packages from a path, uri, or repository and runs related commands. + Gets Open Packages from almost anything. Anything can be a package. @@ -23,6 +23,7 @@ function Get-OpenPackage * Any `*.zip` file * Any `*.tar.gz` file * Any url + * Any public nuget package * Any git repository * Any public at protocol URI * Any dictionary (including nested dictionaries) @@ -519,86 +520,7 @@ function Get-OpenPackage $packages += Get-OpenPackage -Module $arg @namedParameters continue nextArgument } - - # If we have reached this point, we want to look for a related command. - # For the sake of sanity, this will be done by looking for any function with the same noun but a different verb - $verbExists = $ExecutionContext.SessionState.InvokeCommand.GetCommand("$arg-$myNoun", "Function") - - if (-not $verbExists) { - Write-Warning "Unable to map argument '$arg'" - continue nextArgument - } - - # Create a pair of collections for named and unnamed parameters - $namedParameters = [Ordered]@{} - $unnamedArguments = @() - - # We want to look for parameters to the verb. - # These can occur before the next verb, or go to the end of the arguments. - # So we want to walk thru a new loop, and update our argument index. - :findParamaeters for ($nextArgNumber = $argNumber + 1; $nextArgNumber -lt $ArgumentList.Length; $nextArgNumber++) { - # get the next argument - $nextArg = $ArgumentList[$nextArgNumber] - # If it is a string - if ($nextArg -is [string]) { - # check for the next verb - $nextVerb = $ExecutionContext.SessionState.InvokeCommand.GetCommand("$nextArg-$myNoun", "Function") - # If we found one - if ($nextVerb) { - # decrement our index (we will want to parse this in the next pass) - $nextArgNumber-- - # break out of this loop so we can run the command - break findParamaeters - } else { - # If the next argument was not a verb, - # add it to our unnamed arguments - $unnamedArguments += $nextArg - } - } - # If the next argument is a dictionary - elseif ($nextArg -is [Collections.IDictionary]) { - # try to treat it as a splat - $mappedAnything = $false - - # Find all of the potential parameter aliases - $aliases = $verbExists.Parameters.Aliases - # and go over each key - $mappedAnything = $false - foreach ($key in $nextArg.Keys) { - # If the key is a parameter or an aliased parameter in the next verb - if ($verbExists.Parameters[$key] -or $aliases -contains $key) { - # put it into the named parameters table - # (if it already existed, make it as list) - if ($null -ne $namedParameters[$key]) { - $namedParameters[$key] = @($namedParameters[$key]) + $nextArg[$key] - } else { - $namedParameters[$key] = $nextArg[$key] - } - # and make sure to mark that we've mapped anything - $mappedAnything = $true - continue - } - } - - # If we didn't map any part of the dictionary to a splat - if (-not $mappedAnything) { - # pass it positionally - $unnamedArguments += $nextArg - } - } - else { - # If the arg was not a string or a dictionary, pass it positionally - $unnamedArguments += $nextArg - } - } - - # Set our main loop's argument number so we skip all we have parsed - $argNumber = $nextArgNumber - # and do not output packages since were are calling some other command with the packages we have. - $outputPackages = $false - # Pipe those packages into the verb with any arguments we have mapped - $packages | & $verbExists @unnamedArguments @namedParameters - # and continue to the next argument to this command. + continue nextArgument } } From c59f592073e4c02335c4a563ff9bcea51bf3b6d0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 16 Mar 2026 18:45:05 -0700 Subject: [PATCH 298/724] feat: `Start-OpenPackage -Invokable` ( Fixes #130 ) --- Commands/Start-OpenPackage.ps1 | 156 ++++++++++++++++++++++++++++++--- 1 file changed, 142 insertions(+), 14 deletions(-) diff --git a/Commands/Start-OpenPackage.ps1 b/Commands/Start-OpenPackage.ps1 index f2bc6dc..4f170e5 100644 --- a/Commands/Start-OpenPackage.ps1 +++ b/Commands/Start-OpenPackage.ps1 @@ -49,6 +49,12 @@ function Start-OpenPackage { $TypeMap = $( ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap ), + + # If set, the scripts in the package will be invokable. + # This turns allows every PowerShell script in the package into server side code. + # This should be used catiously, and only with known packages. + [Alias('CanInvoke','Invocable')] + [switch]$Invokable, # The throttle limit. # This is the number of concurrent jobs that can be running at once. @@ -100,35 +106,49 @@ function Start-OpenPackage { if ($request.url.localPath -ne '/') { [IO.Packaging.PackUriHelper]::CreatePartUri($request.url.localPath) - } - $noTrailingSlash = ($request.url.LocalPath -replace '/$') + } + if ($Invokable) { + [IO.Packaging.PackUriHelper]::CreatePartUri("$($request.url.LocalPath).ps1") + } + $noTrailingSlash = ($request.url.LocalPath -replace '/$') if ($noTrailingSlash) { + if ($Invokable) { + [IO.Packaging.PackUriHelper]::CreatePartUri("$($noTrailingSlash).ps1") + } try { [IO.Packaging.PackUriHelper]::CreatePartUri($noTrailingSlash) } catch { Write-Warning "$_ - $($request.Url) - $($request.Url.LocalPath)" } } + if ($Invokable) { + $noTrailingSlash + '/index.ps1' + } $noTrailingSlash + '/index.html' $noTrailingSlash + '/README.html' $noTrailingSlash + '/README.md' $noTrailingSlash + '.html' $noTrailingSlash + '/index.json' - $noTrailingSlash + '/index.xml' + $noTrailingSlash + '/index.xml' } elseif ($request.url.LocalPath) { + if ($Invokable) { + [IO.Packaging.PackUriHelper]::CreatePartUri("$($request.url.LocalPath).ps1") + } try { [IO.Packaging.PackUriHelper]::CreatePartUri($request.url.LocalPath) } catch { Write-Warning "$_ - $($request.Url) - $($request.Url.LocalPath)" - } - + } } else { @() } :nextPotential foreach ($potentialUri in $potentialUris) { :nextPack foreach ($pack in $package) { - :nextPart foreach ($part in $pack.GetParts()) { + if ($pack.PartExists($potentialUri)) { + return $potentialUri + } + :nextPart foreach ($part in $pack.GetParts()) { if ($part.Uri -eq $potentialUri) { return $part.Uri } @@ -155,6 +175,103 @@ function Start-OpenPackage { $response.ContentType = $packagePart.ContentType } + # If we are invokable and are dealing with a script file + if ($Invokable -and + $packagePart.Uri -match '\.ps1$' -and + $packagePart.Reader + ) { + if ($packagePart.Uri -match '\.ps1$') { + + $packageScript = $packagePart.Read() + $packageParameterNames = [Ordered]@{} + :nextParameter foreach ($packageScriptParameter in $packageScript.Ast.ParamBlock.Parameters) { + if ($packageScriptParameter.Attributes.typename -match 'hidden') { + continue nextParameter + } + $parameterName = "$($packageScriptParameter.Name.VariablePath.UserPath)" + foreach ($attr in $packageScriptParameter.Attributes) { + if ($attr.typename -ne 'alias') { continue } + foreach ($parameterAlias in $attr.PositionalArguments.Value) { + $packageParameterNames[$parameterAlias] = $parameterName + } + } + $packageParameterNames[$parameterName] = $parameterName + } + + $packageScriptParameters = [Ordered]@{} + if ($request.Url.Query) { + $parsedQuery = [Web.HttpUtility]::ParseQueryString($request.Url.Query) + foreach ($queryKey in $parsedQuery.Keys) { + if (-not $packageParameterNames[$queryKey]) { + continue + } + $parameterName = $packageParameterNames[$queryKey] + if ($null -eq $packageScriptParameters[$parameterName]) { + $packageScriptParameters[$parameterName] = $parsedQuery[$queryKey] + } else { + $packageScriptParameters[$parameterName] = @( + $packageScriptParameters[$parameterName] + ) + $parsedQuery[$queryKey] + } + } + } + + Write-Warning "Invoking $($packagePart.Uri) from $($request.Url)" + + $streamOutput = { + param($reply) + + begin { + if (-not $reply.OutputStream) { throw "no output stream" ; return } + $reply.ProtocolVersion = '1.1' + $reply.SendChunked = $true + } + + process { + $in = $_ + + if ($in.OuterXml) { + $buffer = $OutputEncoding.GetBytes("$($in.OuterXml)") + $reply.OutputStream.Write($buffer, 0, $buffer.Length) + $reply.OutputStream.Flush() + } + elseif ($in.html) { + $buffer = $OutputEncoding.GetBytes("$($in.html)") + $reply.OutputStream.Write($buffer, 0, $buffer.Length) + $reply.OutputStream.Flush() + } + else { + # or the stringification of the result. + $buffer = $OutputEncoding.GetBytes("$in") + $reply.OutputStream.Write($buffer, 0, $buffer.Length) + $reply.OutputStream.Flush() + } + } + + end { + if ($reply.Close) { + $reply.Close() + } + } + } + + try { + & $packageScript @packageScriptParameters | + . $streamOutput $response + } catch { + $response.StatusCode = 500 + if ($response.OutputStream.CanWrite) { + $response.Close( + [Text.Encoding]::UTF8.GetBytes("$_"), $false + ) + } else { + $response.Close() + } + } + } + continue nextRequest + } + $acceptableTypes = @($request.Headers['Accept'] -split ',') Write-Host "Accepts $($request.Headers['Accept'])" -ForegroundColor Cyan $partStream = $packagePart.GetStream('Open', 'Read') @@ -272,6 +389,12 @@ function Start-OpenPackage { # and break that into a result and response $request, $response = $context.Request, $context.Response + if ($request.Url.LocalPath -eq '/favicon.ico') { + $response.StatusCode = 404 + $response.Close() + continue nextRequest + } + $MessageData = [Ordered]@{ Url = $request.Url Context = $context @@ -288,11 +411,11 @@ function Start-OpenPackage { $MessageData, $false, $true - ) + ) } # If the request has no output stream or it was handled by an event - if (-not $request.OutputStream -or $MessageData.Handled) { + if (-not $response.OutputStream -or $MessageData.Handled) { # continue to the next request continue nextRequest } @@ -322,7 +445,7 @@ function Start-OpenPackage { $request.InputStream.CopyTo($memoryStream) } - foreach ($pack in $package) { + :packageWrite foreach ($pack in $package) { if ($pack.FileOpenAccess -ne 'ReadWrite') { continue } @@ -341,7 +464,7 @@ function Start-OpenPackage { $partStream.Close() $partStream.Dispose() $anythingChanged = $true - break + break packageWrite } if ($anythingChanged -and @@ -352,13 +475,13 @@ function Start-OpenPackage { } 'delete' { $anythingDeleted = $false - foreach ($pack in $package) { + :packageDelete foreach ($pack in $package) { if ($pack.FileOpenAccess -eq 'ReadWrite' -and $pack.PartExists( $request.Url.LocalPath )) { $pack.DeletePart($request.Url.LocalPath) $anythingDeleted = $true - break + break packageDelete } } if ($anythingDeleted) { @@ -420,7 +543,7 @@ function Start-OpenPackage { $IO = [Ordered]@{ HttpListener = $httpListener Package = $package - ParentRunspace = [Runspace]::DefaultRunspace + ParentRunspace = [Runspace]::DefaultRunspace } + $PSBoundParameters if (-not $io.TypeMap) { @@ -464,7 +587,11 @@ function Start-OpenPackage { if ($?) { Write-Warning "Listening on $rootUrl" - } + } + + if ($Invokable) { + Write-Warning "! ! ! Invokable on $rootUrl ! ! !" + } if (-not $package.PackageProperties.Identifier) { Write-Warning "No Package Identifier" @@ -483,6 +610,7 @@ function Start-OpenPackage { } ) + $RootUrl = $RootUrl -replace '/$' $JobParameters = [Ordered]@{ ScriptBlock=$JobDefinition ArgumentList=$IO From 5787854c362046442c013998f69b9c3a6a3683f1 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 16 Mar 2026 18:47:03 -0700 Subject: [PATCH 299/724] feat: `Lock-OpenPackage` ( Fixes #93 ) Attaching stream to locked package (or else the locked package is not usable) --- Commands/Lock-OpenPackage.ps1 | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Commands/Lock-OpenPackage.ps1 b/Commands/Lock-OpenPackage.ps1 index d00aa53..eb0c1d7 100644 --- a/Commands/Lock-OpenPackage.ps1 +++ b/Commands/Lock-OpenPackage.ps1 @@ -58,10 +58,7 @@ function Lock-OpenPackage # Create a new package from our new stream, as a read only package. $newPackage = [IO.Packaging.Package]::Open($newStream, 'Open', 'Read') - - # Close and dispose of our new stream - $newStream.Close() - $newStream.Dispose() + $newPackage | Add-Member NoteProperty MemoryStream $newStream -Force $newPackage } From ed3f6ae41c554dde15227486363925684414642a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 16 Mar 2026 18:50:35 -0700 Subject: [PATCH 300/724] feat: `Remove-OpenPackage` ( Fixes #40 ) Adding docs and improving pipeline binding --- Commands/Remove-OpenPackage.ps1 | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Commands/Remove-OpenPackage.ps1 b/Commands/Remove-OpenPackage.ps1 index 3b9dad5..2a37f2a 100644 --- a/Commands/Remove-OpenPackage.ps1 +++ b/Commands/Remove-OpenPackage.ps1 @@ -3,7 +3,23 @@ function Remove-OpenPackage { .SYNOPSIS Removes parts from an open package .DESCRIPTION - Removes content parts from an open package. + Removes content parts from an open package. + .LINK + Get-OpenPackage + .LINK + Select-OpenPackage + .EXAMPLE + Get-OpenPackage @{ + "a.html" = "

    a html file

    " + } | + Remove-OpenPackage -Uri '/a.html' + .EXAMPLE + Get-OpenPackage @{ + "a.html" = "

    a html file

    " + "a.css" = "body { max-width: 100vw; height: 100vh}" + } | + Select-OpenPackage -Include *.html | + Remove-OpenPackage #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] [Alias('Remove-OP','rop','rOpenPackage')] @@ -15,7 +31,7 @@ function Remove-OpenPackage { # The input object. # If this is not a package, the input will be passed thru and nothing will be removed. - [Parameter(ValueFromPipeline)] + [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)] [Alias('Package')] [PSObject] $InputObject @@ -23,7 +39,11 @@ function Remove-OpenPackage { process { if ($InputObject -isnot [IO.Packaging.Package]) { - return $InputObject + if ($inputObject.Package -is [IO.Packaging.Package]) { + $inputObject = $inputObject.Package + } else { + return $InputObject + } } foreach ($part in @($InputObject.GetParts())) { @@ -33,5 +53,7 @@ function Remove-OpenPackage { $InputObject.DeletePart($part.Uri) } } + + $InputObject } } \ No newline at end of file From 134c9305bd0588c3398f243342d15272e275fc13 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 16 Mar 2026 19:49:47 -0700 Subject: [PATCH 301/724] feat: `OpenPackage.get_FileHash` ( Fixes #131 ) --- Types/OpenPackage/get_FileHash.ps1 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Types/OpenPackage/get_FileHash.ps1 diff --git a/Types/OpenPackage/get_FileHash.ps1 b/Types/OpenPackage/get_FileHash.ps1 new file mode 100644 index 0000000..2328dbb --- /dev/null +++ b/Types/OpenPackage/get_FileHash.ps1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS + Gets the file hashes +.DESCRIPTION + Gets the file hashes of each part using any supported algorithm (default SHA256) +.NOTES + Supports any algorithm from Get-FileHash +.LINK + Get-FileHash +#> +param([string]$Algorithm = 'SHA256') + +foreach ($part in $this.GetParts()) { + $part.GetHash($Algorithm) +} From f667e29f5b1f41403b4a5eb45adac2ec82e9f96b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 02:50:11 +0000 Subject: [PATCH 302/724] feat: `OpenPackage.get_FileHash` ( Fixes #131 ) --- OP.types.ps1xml | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index b4b3993..41cbc8a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2381,6 +2381,27 @@ $fileContentTypes
    + + FileHash + + <# +.SYNOPSIS + Gets the file hashes +.DESCRIPTION + Gets the file hashes of each part using any supported algorithm (default SHA256) +.NOTES + Supports any algorithm from Get-FileHash +.LINK + Get-FileHash +#> +param([string]$Algorithm = 'SHA256') + +foreach ($part in $this.GetParts()) { + $part.GetHash($Algorithm) +} + + + FileList @@ -5683,6 +5704,27 @@ $fileContentTypes + + FileHash + + <# +.SYNOPSIS + Gets the file hashes +.DESCRIPTION + Gets the file hashes of each part using any supported algorithm (default SHA256) +.NOTES + Supports any algorithm from Get-FileHash +.LINK + Get-FileHash +#> +param([string]$Algorithm = 'SHA256') + +foreach ($part in $this.GetParts()) { + $part.GetHash($Algorithm) +} + + + FileList @@ -8985,6 +9027,27 @@ $fileContentTypes + + FileHash + + <# +.SYNOPSIS + Gets the file hashes +.DESCRIPTION + Gets the file hashes of each part using any supported algorithm (default SHA256) +.NOTES + Supports any algorithm from Get-FileHash +.LINK + Get-FileHash +#> +param([string]$Algorithm = 'SHA256') + +foreach ($part in $this.GetParts()) { + $part.GetHash($Algorithm) +} + + + FileList From 02a31fe0da45db843282f1b7429cdfea36b804bd Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 16 Mar 2026 20:02:42 -0700 Subject: [PATCH 303/724] feat: `OpenPackage.get_PowerShellParameterAst` ( Fixes #124 ) --- Types/OpenPackage/get_PowerShellParameterAst.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Types/OpenPackage/get_PowerShellParameterAst.ps1 diff --git a/Types/OpenPackage/get_PowerShellParameterAst.ps1 b/Types/OpenPackage/get_PowerShellParameterAst.ps1 new file mode 100644 index 0000000..b89b764 --- /dev/null +++ b/Types/OpenPackage/get_PowerShellParameterAst.ps1 @@ -0,0 +1,16 @@ +<# +.SYNOPSIS + Gets PowerShell Parameter Definitions +.DESCRIPTION + Gets all PowerShell ParameterAst references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.ParameterAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} \ No newline at end of file From 00e7e0ca1a9e29c8bcbdf0686a8ce1fcb0ea3446 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 03:02:57 +0000 Subject: [PATCH 304/724] feat: `OpenPackage.get_PowerShellParameterAst` ( Fixes #124 ) --- OP.types.ps1xml | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 41cbc8a..372bd02 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2976,6 +2976,27 @@ foreach ($part in $this.GetParts()) { } + + PowerShellParameterAst + + <# +.SYNOPSIS + Gets PowerShell Parameter Definitions +.DESCRIPTION + Gets all PowerShell ParameterAst references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.ParameterAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} + + PowerShellTypeAst @@ -6299,6 +6320,27 @@ foreach ($part in $this.GetParts()) { } + + PowerShellParameterAst + + <# +.SYNOPSIS + Gets PowerShell Parameter Definitions +.DESCRIPTION + Gets all PowerShell ParameterAst references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.ParameterAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} + + PowerShellTypeAst @@ -9622,6 +9664,27 @@ foreach ($part in $this.GetParts()) { } + + PowerShellParameterAst + + <# +.SYNOPSIS + Gets PowerShell Parameter Definitions +.DESCRIPTION + Gets all PowerShell ParameterAst references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.ParameterAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} + + PowerShellTypeAst From 7f17398a19982055f6605d13e85252bafb4b887c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 13:59:34 -0700 Subject: [PATCH 305/724] feat: `OpenPackage.get_CHANGELOG.md` ( Fixes #29 ) Renaming property for more clear use with file extensions --- Types/OpenPackage/get_CHANGELOG.md.ps1 | 22 +++++++++++++++ Types/OpenPackage/get_CHANGELOG.ps1 | 39 -------------------------- Types/OpenPackage/set_CHANGELOG.ps1 | 25 ----------------- 3 files changed, 22 insertions(+), 64 deletions(-) create mode 100644 Types/OpenPackage/get_CHANGELOG.md.ps1 delete mode 100644 Types/OpenPackage/get_CHANGELOG.ps1 delete mode 100644 Types/OpenPackage/set_CHANGELOG.ps1 diff --git a/Types/OpenPackage/get_CHANGELOG.md.ps1 b/Types/OpenPackage/get_CHANGELOG.md.ps1 new file mode 100644 index 0000000..a900612 --- /dev/null +++ b/Types/OpenPackage/get_CHANGELOG.md.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Gets a package's changelog +.DESCRIPTION + Gets the content of any parts in the package named CHANGELOG.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. \ No newline at end of file diff --git a/Types/OpenPackage/get_CHANGELOG.ps1 b/Types/OpenPackage/get_CHANGELOG.ps1 deleted file mode 100644 index 2a122da..0000000 --- a/Types/OpenPackage/get_CHANGELOG.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -<# -.SYNOPSIS - Gets a package's changelog -.DESCRIPTION - Gets the content of any parts in the package named CHANGELOG.md -#> -[OutputType("text/markdown")] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG - if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } - - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Make the markdown a PSObject - $markdownObject = [PSObject]::new($readStream) - # add the URI - $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force - # and decorate it as `text/markdown` - $markdownObject.pstypenames.add('text/markdown') - - $markdownObject - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() -} - -# We are done. \ No newline at end of file diff --git a/Types/OpenPackage/set_CHANGELOG.ps1 b/Types/OpenPackage/set_CHANGELOG.ps1 deleted file mode 100644 index b807d1a..0000000 --- a/Types/OpenPackage/set_CHANGELOG.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -param() - -$newChangelog = ($args -join [Environment]::NewLine) + [Environment]::NewLine - -$changelogParts = -@(foreach ($part in $($this.GetParts())) { - if ($part.Uri -match '/CHANGELOG\.md$') { $part} -}) - - -$stream = -if (-not $changelogParts) { - $newPart = $this.CreatePart('/CHANGELOG.md', 'text/markdown', 'Normal') - $newPart.GetStream() -} -else { - $firstChangelog = $changelogParts[0] - $changelogPart = $this.GetPart($firstChangelog.Uri) - $changelogPart.GetStream() -} - -$buffer = $OutputEncoding.GetBytes("$newChangelog") -$stream.Write($buffer,0, $buffer.Length) -$stream.Close() -$stream.Dispose() From 10527a1a928e0b1be11993e2fccaa41c6e78a34c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 21:00:40 +0000 Subject: [PATCH 306/724] feat: `OpenPackage.get_CHANGELOG.md` ( Fixes #29 ) Renaming property for more clear use with file extensions --- OP.types.ps1xml | 171 +++++------------------------------------------- 1 file changed, 18 insertions(+), 153 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 372bd02..91db51a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2070,7 +2070,7 @@ $this.PackageProperties.Category = $Category - CHANGELOG + CHANGELOG.md <# .SYNOPSIS @@ -2086,60 +2086,15 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Make the markdown a PSObject - $markdownObject = [PSObject]::new($readStream) - # add the URI - $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force - # and decorate it as `text/markdown` - $markdownObject.pstypenames.add('text/markdown') - - $markdownObject - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. - - param() - -$newChangelog = ($args -join [Environment]::NewLine) + [Environment]::NewLine - -$changelogParts = -@(foreach ($part in $($this.GetParts())) { - if ($part.Uri -match '/CHANGELOG\.md$') { $part} -}) - - -$stream = -if (-not $changelogParts) { - $newPart = $this.CreatePart('/CHANGELOG.md', 'text/markdown', 'Normal') - $newPart.GetStream() -} -else { - $firstChangelog = $changelogParts[0] - $changelogPart = $this.GetPart($firstChangelog.Uri) - $changelogPart.GetStream() -} - -$buffer = $OutputEncoding.GetBytes("$newChangelog") -$stream.Write($buffer,0, $buffer.Length) -$stream.Close() -$stream.Dispose() - - ChocolateyInstall @@ -5414,7 +5369,7 @@ $this.PackageProperties.Category = $Category - CHANGELOG + CHANGELOG.md <# .SYNOPSIS @@ -5430,60 +5385,15 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Make the markdown a PSObject - $markdownObject = [PSObject]::new($readStream) - # add the URI - $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force - # and decorate it as `text/markdown` - $markdownObject.pstypenames.add('text/markdown') - - $markdownObject - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. - - param() - -$newChangelog = ($args -join [Environment]::NewLine) + [Environment]::NewLine - -$changelogParts = -@(foreach ($part in $($this.GetParts())) { - if ($part.Uri -match '/CHANGELOG\.md$') { $part} -}) - - -$stream = -if (-not $changelogParts) { - $newPart = $this.CreatePart('/CHANGELOG.md', 'text/markdown', 'Normal') - $newPart.GetStream() -} -else { - $firstChangelog = $changelogParts[0] - $changelogPart = $this.GetPart($firstChangelog.Uri) - $changelogPart.GetStream() -} - -$buffer = $OutputEncoding.GetBytes("$newChangelog") -$stream.Write($buffer,0, $buffer.Length) -$stream.Close() -$stream.Dispose() - - ChocolateyInstall @@ -8758,7 +8668,7 @@ $this.PackageProperties.Category = $Category - CHANGELOG + CHANGELOG.md <# .SYNOPSIS @@ -8774,60 +8684,15 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Make the markdown a PSObject - $markdownObject = [PSObject]::new($readStream) - # add the URI - $markdownObject | Add-Member NoteProperty Uri $part.Uri -Force - # and decorate it as `text/markdown` - $markdownObject.pstypenames.add('text/markdown') - - $markdownObject - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. - - param() - -$newChangelog = ($args -join [Environment]::NewLine) + [Environment]::NewLine - -$changelogParts = -@(foreach ($part in $($this.GetParts())) { - if ($part.Uri -match '/CHANGELOG\.md$') { $part} -}) - - -$stream = -if (-not $changelogParts) { - $newPart = $this.CreatePart('/CHANGELOG.md', 'text/markdown', 'Normal') - $newPart.GetStream() -} -else { - $firstChangelog = $changelogParts[0] - $changelogPart = $this.GetPart($firstChangelog.Uri) - $changelogPart.GetStream() -} - -$buffer = $OutputEncoding.GetBytes("$newChangelog") -$stream.Write($buffer,0, $buffer.Length) -$stream.Close() -$stream.Dispose() - - ChocolateyInstall From 45289e7034c37081c67a23b778e2e4c6a11ff56d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 14:28:51 -0700 Subject: [PATCH 307/724] feat: `OpenPackage.get_ServiceWorker.js` ( Fixes #86 ) Renaming certain properties for extension clarity --- Types/OpenPackage/get_ServiceWorker.js.ps1 | 23 +++++++++++++++++++ Types/OpenPackage/get_ServiceWorker.ps1 | 26 ---------------------- 2 files changed, 23 insertions(+), 26 deletions(-) create mode 100644 Types/OpenPackage/get_ServiceWorker.js.ps1 delete mode 100644 Types/OpenPackage/get_ServiceWorker.ps1 diff --git a/Types/OpenPackage/get_ServiceWorker.js.ps1 b/Types/OpenPackage/get_ServiceWorker.js.ps1 new file mode 100644 index 0000000..b3ee1fa --- /dev/null +++ b/Types/OpenPackage/get_ServiceWorker.js.ps1 @@ -0,0 +1,23 @@ +<# +.SYNOPSIS + Gets any service workers in a package +.DESCRIPTION + Gets any clearly named service workers in a package. + + Will find any files named `sw.js` or `ServiceWorker.js` +#> +param() + +$pattern = '/(?>sw|ServiceWorker).js$' + +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $pattern) { + if ($part.Reader) { + $part.Read() + } else { + $part + } + } +} + +# We are done. \ No newline at end of file diff --git a/Types/OpenPackage/get_ServiceWorker.ps1 b/Types/OpenPackage/get_ServiceWorker.ps1 deleted file mode 100644 index bd32fa8..0000000 --- a/Types/OpenPackage/get_ServiceWorker.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -<# -.SYNOPSIS - Gets any service workers in a package -.DESCRIPTION - Gets any clearly named service workers in a package. - - Will find any files named `sw.js` or `ServiceWorker.js` - - -#> -[OutputType("text/javascript")] -param() - -foreach ($content in $this.GetContent( - $this.FileList -match '/(?>sw|ServiceWorker).js$' -)) { - if ($content.pstypenames -notcontains 'text/javascript') { - $content.pstypenames.insert(0, 'text/javascript') - } - $content -} - -return - - -# We are done. \ No newline at end of file From c7e5dd03ee3e90a61dca45148068311aa7adaa9f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 21:29:18 +0000 Subject: [PATCH 308/724] feat: `OpenPackage.get_ServiceWorker.js` ( Fixes #86 ) Renaming certain properties for extension clarity --- OP.types.ps1xml | 75 ++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 91db51a..3102f58 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3047,7 +3047,7 @@ $this.PackageProperties.Revision = $Revision - ServiceWorker + ServiceWorker.js <# .SYNOPSIS @@ -3056,23 +3056,20 @@ $this.PackageProperties.Revision = $Revision Gets any clearly named service workers in a package. Will find any files named `sw.js` or `ServiceWorker.js` - - #> -[OutputType("text/javascript")] param() -foreach ($content in $this.GetContent( - $this.FileList -match '/(?>sw|ServiceWorker).js$' -)) { - if ($content.pstypenames -notcontains 'text/javascript') { - $content.pstypenames.insert(0, 'text/javascript') - } - $content -} - -return +$pattern = '/(?>sw|ServiceWorker).js$' +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $pattern) { + if ($part.Reader) { + $part.Read() + } else { + $part + } + } +} # We are done. @@ -6346,7 +6343,7 @@ $this.PackageProperties.Revision = $Revision - ServiceWorker + ServiceWorker.js <# .SYNOPSIS @@ -6355,23 +6352,20 @@ $this.PackageProperties.Revision = $Revision Gets any clearly named service workers in a package. Will find any files named `sw.js` or `ServiceWorker.js` - - #> -[OutputType("text/javascript")] param() -foreach ($content in $this.GetContent( - $this.FileList -match '/(?>sw|ServiceWorker).js$' -)) { - if ($content.pstypenames -notcontains 'text/javascript') { - $content.pstypenames.insert(0, 'text/javascript') - } - $content -} - -return +$pattern = '/(?>sw|ServiceWorker).js$' +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $pattern) { + if ($part.Reader) { + $part.Read() + } else { + $part + } + } +} # We are done. @@ -9645,7 +9639,7 @@ $this.PackageProperties.Revision = $Revision - ServiceWorker + ServiceWorker.js <# .SYNOPSIS @@ -9654,23 +9648,20 @@ $this.PackageProperties.Revision = $Revision Gets any clearly named service workers in a package. Will find any files named `sw.js` or `ServiceWorker.js` - - #> -[OutputType("text/javascript")] param() -foreach ($content in $this.GetContent( - $this.FileList -match '/(?>sw|ServiceWorker).js$' -)) { - if ($content.pstypenames -notcontains 'text/javascript') { - $content.pstypenames.insert(0, 'text/javascript') - } - $content -} - -return +$pattern = '/(?>sw|ServiceWorker).js$' +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $pattern) { + if ($part.Reader) { + $part.Read() + } else { + $part + } + } +} # We are done. From a1e1260c0a148ea11fafdf5d2af9ba8b42c45467 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 14:40:08 -0700 Subject: [PATCH 309/724] feat: `OpenPackage.get_TypeScriptConfig.json` ( Fixes #49 ) Renaming certain properties for extension clarity --- .../OpenPackage/get_TypeScriptConfig.json.ps1 | 22 ++++++++++++ Types/OpenPackage/get_TypeScriptConfig.ps1 | 34 ------------------- 2 files changed, 22 insertions(+), 34 deletions(-) create mode 100644 Types/OpenPackage/get_TypeScriptConfig.json.ps1 delete mode 100644 Types/OpenPackage/get_TypeScriptConfig.ps1 diff --git a/Types/OpenPackage/get_TypeScriptConfig.json.ps1 b/Types/OpenPackage/get_TypeScriptConfig.json.ps1 new file mode 100644 index 0000000..61ffd14 --- /dev/null +++ b/Types/OpenPackage/get_TypeScriptConfig.json.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Gets a package's `tsconfig.json` +.DESCRIPTION + Gets the content of any TypeScript configuration `tsconfig.json` files in the package +#> +[OutputType([psobject])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/tsconfig\.json$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. \ No newline at end of file diff --git a/Types/OpenPackage/get_TypeScriptConfig.ps1 b/Types/OpenPackage/get_TypeScriptConfig.ps1 deleted file mode 100644 index 08f6dd9..0000000 --- a/Types/OpenPackage/get_TypeScriptConfig.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -<# -.SYNOPSIS - Gets a package's `tsconfig.json` -.DESCRIPTION - Gets the content of any TypeScript configuration `tsconfig.json` files in the package -#> -[OutputType([psobject])] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named manifest.json - if ($part.Uri -notmatch '/tsconfig\.json$') { continue } - - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru -} - -# We are done. \ No newline at end of file From 238e6c38249ccd2f84317382524c701f080e0368 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 21:40:27 +0000 Subject: [PATCH 310/724] feat: `OpenPackage.get_TypeScriptConfig.json` ( Fixes #49 ) Renaming certain properties for extension clarity --- OP.types.ps1xml | 72 +++++++++++++------------------------------------ 1 file changed, 18 insertions(+), 54 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 3102f58..366fee7 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3148,7 +3148,7 @@ $this.PackageProperties.Title = $Title - TypeScriptConfig + TypeScriptConfig.json <# .SYNOPSIS @@ -3164,23 +3164,11 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named manifest.json if ($part.Uri -notmatch '/tsconfig\.json$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. @@ -6444,7 +6432,7 @@ $this.PackageProperties.Title = $Title - TypeScriptConfig + TypeScriptConfig.json <# .SYNOPSIS @@ -6460,23 +6448,11 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named manifest.json if ($part.Uri -notmatch '/tsconfig\.json$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. @@ -9740,7 +9716,7 @@ $this.PackageProperties.Title = $Title - TypeScriptConfig + TypeScriptConfig.json <# .SYNOPSIS @@ -9756,23 +9732,11 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named manifest.json if ($part.Uri -notmatch '/tsconfig\.json$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. From 3acac02c1430b9fe03a9e8b3e9978d5d60c7a853 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 14:45:26 -0700 Subject: [PATCH 311/724] feat: `OpenPackage.get_Manifest.json` ( Fixes #46 ) Renaming certain properties for extension clarity --- Types/OpenPackage/get_Manifest.json.ps1 | 21 +++++++++++++++ Types/OpenPackage/get_ManifestJson.ps1 | 34 ------------------------- 2 files changed, 21 insertions(+), 34 deletions(-) create mode 100644 Types/OpenPackage/get_Manifest.json.ps1 delete mode 100644 Types/OpenPackage/get_ManifestJson.ps1 diff --git a/Types/OpenPackage/get_Manifest.json.ps1 b/Types/OpenPackage/get_Manifest.json.ps1 new file mode 100644 index 0000000..86a07a2 --- /dev/null +++ b/Types/OpenPackage/get_Manifest.json.ps1 @@ -0,0 +1,21 @@ +<# +.SYNOPSIS + Gets a package's `manifest.json` +.DESCRIPTION + Gets the content of any `manifest.json` files in the package +#> +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/manifest\.json$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. \ No newline at end of file diff --git a/Types/OpenPackage/get_ManifestJson.ps1 b/Types/OpenPackage/get_ManifestJson.ps1 deleted file mode 100644 index 74c45fd..0000000 --- a/Types/OpenPackage/get_ManifestJson.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -<# -.SYNOPSIS - Gets a package's `manifest.json` -.DESCRIPTION - Gets the content of any `manifest.json` files in the package -#> -[OutputType([xml])] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named manifest.json - if ($part.Uri -notmatch '/manifest\.json$') { continue } - - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru -} - -# We are done. \ No newline at end of file From ea3ca7e35a25a87eff1bec44a500b33f6e534fc0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 21:45:58 +0000 Subject: [PATCH 312/724] feat: `OpenPackage.get_Manifest.json` ( Fixes #46 ) Renaming certain properties for extension clarity --- OP.types.ps1xml | 75 ++++++++++++------------------------------------- 1 file changed, 18 insertions(+), 57 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 366fee7..09be4b8 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2723,7 +2723,7 @@ return $allLexicons - ManifestJson + Manifest.json <# .SYNOPSIS @@ -2731,7 +2731,6 @@ return $allLexicons .DESCRIPTION Gets the content of any `manifest.json` files in the package #> -[OutputType([xml])] param() # Get every part @@ -2739,23 +2738,11 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named manifest.json if ($part.Uri -notmatch '/manifest\.json$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. @@ -6007,7 +5994,7 @@ return $allLexicons - ManifestJson + Manifest.json <# .SYNOPSIS @@ -6015,7 +6002,6 @@ return $allLexicons .DESCRIPTION Gets the content of any `manifest.json` files in the package #> -[OutputType([xml])] param() # Get every part @@ -6023,23 +6009,11 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named manifest.json if ($part.Uri -notmatch '/manifest\.json$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. @@ -9291,7 +9265,7 @@ return $allLexicons - ManifestJson + Manifest.json <# .SYNOPSIS @@ -9299,7 +9273,6 @@ return $allLexicons .DESCRIPTION Gets the content of any `manifest.json` files in the package #> -[OutputType([xml])] param() # Get every part @@ -9307,23 +9280,11 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named manifest.json if ($part.Uri -notmatch '/manifest\.json$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json | - Add-Member NoteProperty Uri $part.Uri -Force -PassThru + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. From 2c79d917bebc5aca6164fffb7012979221f23a49 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 14:47:46 -0700 Subject: [PATCH 313/724] feat: `OpenPackage.get_Template.json` ( Fixes #45 ) Renaming certain properties for extension clarity --- Types/OpenPackage/get_Template.json.ps1 | 18 ++++++++++++++++++ Types/OpenPackage/get_TemplateJson.ps1 | 10 ---------- 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 Types/OpenPackage/get_Template.json.ps1 delete mode 100644 Types/OpenPackage/get_TemplateJson.ps1 diff --git a/Types/OpenPackage/get_Template.json.ps1 b/Types/OpenPackage/get_Template.json.ps1 new file mode 100644 index 0000000..a6fa2f3 --- /dev/null +++ b/Types/OpenPackage/get_Template.json.ps1 @@ -0,0 +1,18 @@ +<# +.SYNOPSIS + Gets a package's `template.json` +.DESCRIPTION + Gets the content of any `template.json` files in the package +#> +[OutputType([PSObject])] +param() + +foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch '/template\.json$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} \ No newline at end of file diff --git a/Types/OpenPackage/get_TemplateJson.ps1 b/Types/OpenPackage/get_TemplateJson.ps1 deleted file mode 100644 index 5f5a65a..0000000 --- a/Types/OpenPackage/get_TemplateJson.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -<# -.SYNOPSIS - Gets a package's `template.json` -.DESCRIPTION - Gets the content of any `template.json` files in the package -#> -[OutputType([PSObject])] -param() - -$this.GetContent($this.FileList -match '/template\.json$') \ No newline at end of file From c9047603f001ba6cd23f0ab0775ff345c3953a40 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 21:48:05 +0000 Subject: [PATCH 314/724] feat: `OpenPackage.get_Template.json` ( Fixes #45 ) Renaming certain properties for extension clarity --- OP.types.ps1xml | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 09be4b8..71c7dbe 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3091,7 +3091,7 @@ $this.PackageProperties.Subject = $Subject - TemplateJson + Template.json <# .SYNOPSIS @@ -3102,7 +3102,15 @@ $this.PackageProperties.Subject = $Subject [OutputType([PSObject])] param() -$this.GetContent($this.FileList -match '/template\.json$') +foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch '/template\.json$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} @@ -6362,7 +6370,7 @@ $this.PackageProperties.Subject = $Subject - TemplateJson + Template.json <# .SYNOPSIS @@ -6373,7 +6381,15 @@ $this.PackageProperties.Subject = $Subject [OutputType([PSObject])] param() -$this.GetContent($this.FileList -match '/template\.json$') +foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch '/template\.json$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} @@ -9633,7 +9649,7 @@ $this.PackageProperties.Subject = $Subject - TemplateJson + Template.json <# .SYNOPSIS @@ -9644,7 +9660,15 @@ $this.PackageProperties.Subject = $Subject [OutputType([PSObject])] param() -$this.GetContent($this.FileList -match '/template\.json$') +foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch '/template\.json$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} From 79cb672c5c185122e0f7e2ecef03abb3f4d1ea9a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 14:53:37 -0700 Subject: [PATCH 315/724] feat: `OpenPackage.get_Package.json` ( Fixes #32 ) Renaming certain properties for extension clarity --- Types/OpenPackage/get_Package.json.ps1 | 22 +++++++++++++++++ Types/OpenPackage/get_PackageJson.ps1 | 33 -------------------------- 2 files changed, 22 insertions(+), 33 deletions(-) create mode 100644 Types/OpenPackage/get_Package.json.ps1 delete mode 100644 Types/OpenPackage/get_PackageJson.ps1 diff --git a/Types/OpenPackage/get_Package.json.ps1 b/Types/OpenPackage/get_Package.json.ps1 new file mode 100644 index 0000000..ad916fc --- /dev/null +++ b/Types/OpenPackage/get_Package.json.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Gets a package's `package.json` +.DESCRIPTION + Gets the content of any `package.json` files in the package +#> +[OutputType([PSObject])] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CHANGELOG + if ($part.Uri -notmatch '/package\.json$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. \ No newline at end of file diff --git a/Types/OpenPackage/get_PackageJson.ps1 b/Types/OpenPackage/get_PackageJson.ps1 deleted file mode 100644 index 1a86ce2..0000000 --- a/Types/OpenPackage/get_PackageJson.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -<# -.SYNOPSIS - Gets a package's `package.json` -.DESCRIPTION - Gets the content of any `package.json` files in the package -#> -[OutputType([xml])] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG - if ($part.Uri -notmatch '/package\.json$') { continue } - - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json -} - -# We are done. \ No newline at end of file From 44e158dac37b089669c7512c3d42b608c52c2d71 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 21:53:55 +0000 Subject: [PATCH 316/724] feat: `OpenPackage.get_Package.json` ( Fixes #32 ) Renaming certain properties for extension clarity --- OP.types.ps1xml | 75 ++++++++++++++----------------------------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 71c7dbe..e72aaa1 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2835,7 +2835,7 @@ $this.GetContent(
    - PackageJson + Package.json <# .SYNOPSIS @@ -2843,7 +2843,7 @@ $this.GetContent( .DESCRIPTION Gets the content of any `package.json` files in the package #> -[OutputType([xml])] +[OutputType([PSObject])] param() # Get every part @@ -2851,22 +2851,11 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '/package\.json$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. @@ -6114,7 +6103,7 @@ $this.GetContent( - PackageJson + Package.json <# .SYNOPSIS @@ -6122,7 +6111,7 @@ $this.GetContent( .DESCRIPTION Gets the content of any `package.json` files in the package #> -[OutputType([xml])] +[OutputType([PSObject])] param() # Get every part @@ -6130,22 +6119,11 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '/package\.json$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. @@ -9393,7 +9371,7 @@ $this.GetContent( - PackageJson + Package.json <# .SYNOPSIS @@ -9401,7 +9379,7 @@ $this.GetContent( .DESCRIPTION Gets the content of any `package.json` files in the package #> -[OutputType([xml])] +[OutputType([PSObject])] param() # Get every part @@ -9409,22 +9387,11 @@ foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG if ($part.Uri -notmatch '/package\.json$') { continue } - # Read the stream - $partStream = $part.GetStream() - $streamReader = [IO.StreamReader]::new($partStream) - - $readStream = $streamReader.ReadToEnd() - - # Close the reader - $streamReader.Close() - $streamReader.Dispose() - - # and the stream. - $partStream.Close() - $partStream.Dispose() - - # Convert the part from json. - $readStream | ConvertFrom-Json + if ($part.Reader) { + $part.Read() + } else { + $part + } } # We are done. From 2e816c199fa82a4a362f99d23e8d6493b49a980b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 14:57:17 -0700 Subject: [PATCH 317/724] feat: `OpenPackage.get_Version` ( Fixes #17 ) Improving auto detection --- Types/OpenPackage/get_Version.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Types/OpenPackage/get_Version.ps1 b/Types/OpenPackage/get_Version.ps1 index be9cbbe..40fce54 100644 --- a/Types/OpenPackage/get_Version.ps1 +++ b/Types/OpenPackage/get_Version.ps1 @@ -20,7 +20,7 @@ if ($moduleManifest) { return $this.PackageProperties.Version } -$packageJson = $this.PackageJson +$packageJson = $this.'Package.json' if ($packageJson -and $packageJson.version) { $this.PackageProperties.Version = $packageJson.version return $this.PackageProperties.Version From 5d46968d0de0295a0c3142774bcd00e01cf5e650 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 21:57:42 +0000 Subject: [PATCH 318/724] feat: `OpenPackage.get_Version` ( Fixes #17 ) Improving auto detection --- OP.types.ps1xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index e72aaa1..4ae3e6b 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3203,7 +3203,7 @@ if ($moduleManifest) { return $this.PackageProperties.Version } -$packageJson = $this.PackageJson +$packageJson = $this.'Package.json' if ($packageJson -and $packageJson.version) { $this.PackageProperties.Version = $packageJson.version return $this.PackageProperties.Version @@ -6471,7 +6471,7 @@ if ($moduleManifest) { return $this.PackageProperties.Version } -$packageJson = $this.PackageJson +$packageJson = $this.'Package.json' if ($packageJson -and $packageJson.version) { $this.PackageProperties.Version = $packageJson.version return $this.PackageProperties.Version @@ -9739,7 +9739,7 @@ if ($moduleManifest) { return $this.PackageProperties.Version } -$packageJson = $this.PackageJson +$packageJson = $this.'Package.json' if ($packageJson -and $packageJson.version) { $this.PackageProperties.Version = $packageJson.version return $this.PackageProperties.Version From da6bd66afb5df10f0cdd3baac3e7867c7cae89c4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 15:34:14 -0700 Subject: [PATCH 319/724] feat: `Remove-OpenPackage` ( Fixes #40 ) Adding inner docs and -WhatIf support --- Commands/Remove-OpenPackage.ps1 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Commands/Remove-OpenPackage.ps1 b/Commands/Remove-OpenPackage.ps1 index 2a37f2a..e22d13a 100644 --- a/Commands/Remove-OpenPackage.ps1 +++ b/Commands/Remove-OpenPackage.ps1 @@ -38,18 +38,31 @@ function Remove-OpenPackage { ) process { + # If the input object is not a package, if ($InputObject -isnot [IO.Packaging.Package]) { + # look for a property named package if ($inputObject.Package -is [IO.Packaging.Package]) { + # and use that as our input. $inputObject = $inputObject.Package } else { + # Otherwise, pass thru the input. return $InputObject } - } + } foreach ($part in @($InputObject.GetParts())) { + # If -WhatIf was passed, return the part + if ($whatIfPreference) { + $part + continue + } + + # If the part uri is in the list of uris if ($part.Uri -in $Uri -and + # and we confirmed our intention to delete it $PSCmdlet.ShouldProcess("Remove $Uri") ) { + # delete the part. $InputObject.DeletePart($part.Uri) } } From dc03670a2a13221ae672e3840592211ddfb8d72b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 17:47:51 -0700 Subject: [PATCH 320/724] feat: `OpenPackage.get_Config.json` ( Fixes #65 ) Renaming certain properties for extension clarity --- Types/OpenPackage/get_Config.json.ps1 | 20 ++++++++++++++++++++ Types/OpenPackage/get_Config.ps1 | 16 ---------------- 2 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 Types/OpenPackage/get_Config.json.ps1 delete mode 100644 Types/OpenPackage/get_Config.ps1 diff --git a/Types/OpenPackage/get_Config.json.ps1 b/Types/OpenPackage/get_Config.json.ps1 new file mode 100644 index 0000000..a9dff89 --- /dev/null +++ b/Types/OpenPackage/get_Config.json.ps1 @@ -0,0 +1,20 @@ +<# +.SYNOPSIS + Gets a package's config json +.DESCRIPTION + Gets the objects stored in any config.json files in the package. +.NOTES + config.json files are used by several static site generators. +#> +[OutputType([PSObject])] +param() + +$partPattern = '[/\+]config\.json$' + +foreach ($part in $this.GetParts()) { + if ($part.Reader) { + $part.Read() + } else { + $part + } +} diff --git a/Types/OpenPackage/get_Config.ps1 b/Types/OpenPackage/get_Config.ps1 deleted file mode 100644 index 6474de8..0000000 --- a/Types/OpenPackage/get_Config.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -<# -.SYNOPSIS - Gets a package's config files -.DESCRIPTION - Gets the content of any known config files in the package: - - This includes any file named `config.*` or `_config.*` -#> -[OutputType([xml])] -param() - -$partPattern = '[/\.]_?config\.[^\.]+?$' - -$this.GetContent(@( - $this.FileList -match $partPattern -)) From ed8ff0cce4c9cb682569e6e7622e2f2ac2eb4cbe Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 00:48:12 +0000 Subject: [PATCH 321/724] feat: `OpenPackage.get_Config.json` ( Fixes #65 ) Renaming certain properties for extension clarity --- OP.types.ps1xml | 72 ++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 4ae3e6b..205d9ab 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2112,24 +2112,28 @@ $this.GetContent("/tools/chocolateyInstall.ps1") - Config + Config.json <# .SYNOPSIS - Gets a package's config files + Gets a package's config json .DESCRIPTION - Gets the content of any known config files in the package: - - This includes any file named `config.*` or `_config.*` + Gets the objects stored in any config.json files in the package. +.NOTES + config.json files are used by several static site generators. #> -[OutputType([xml])] +[OutputType([PSObject])] param() -$partPattern = '[/\.]_?config\.[^\.]+?$' +$partPattern = '[/\+]config\.json$' -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Reader) { + $part.Read() + } else { + $part + } +} @@ -5380,24 +5384,28 @@ $this.GetContent("/tools/chocolateyInstall.ps1") - Config + Config.json <# .SYNOPSIS - Gets a package's config files + Gets a package's config json .DESCRIPTION - Gets the content of any known config files in the package: - - This includes any file named `config.*` or `_config.*` + Gets the objects stored in any config.json files in the package. +.NOTES + config.json files are used by several static site generators. #> -[OutputType([xml])] +[OutputType([PSObject])] param() -$partPattern = '[/\.]_?config\.[^\.]+?$' +$partPattern = '[/\+]config\.json$' -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Reader) { + $part.Read() + } else { + $part + } +} @@ -8648,24 +8656,28 @@ $this.GetContent("/tools/chocolateyInstall.ps1") - Config + Config.json <# .SYNOPSIS - Gets a package's config files + Gets a package's config json .DESCRIPTION - Gets the content of any known config files in the package: - - This includes any file named `config.*` or `_config.*` + Gets the objects stored in any config.json files in the package. +.NOTES + config.json files are used by several static site generators. #> -[OutputType([xml])] +[OutputType([PSObject])] param() -$partPattern = '[/\.]_?config\.[^\.]+?$' +$partPattern = '[/\+]config\.json$' -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Reader) { + $part.Read() + } else { + $part + } +} From eb0227febea7c7c5fcc23fa043233630143151ff Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 18:01:49 -0700 Subject: [PATCH 322/724] feat: `OpenPackage.get_Config.yaml.ps1` ( Fixes #132 ) --- Types/OpenPackage/get_Config.yaml.ps1 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Types/OpenPackage/get_Config.yaml.ps1 diff --git a/Types/OpenPackage/get_Config.yaml.ps1 b/Types/OpenPackage/get_Config.yaml.ps1 new file mode 100644 index 0000000..ca0c21a --- /dev/null +++ b/Types/OpenPackage/get_Config.yaml.ps1 @@ -0,0 +1,20 @@ +<# +.SYNOPSIS + Gets a package's config yaml +.DESCRIPTION + Gets the objects stored in any config.yaml files in the package. +.NOTES + config.yaml files are used by several static site generators. +#> +[OutputType([PSObject])] +param() + +$partPattern = '[/\+]config\.ya?ml$' + +foreach ($part in $this.GetParts()) { + if ($part.Reader) { + $part.Read() + } else { + $part + } +} From c9f1dfd81f6c36b8138de12e58e55cdd5355c39b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 01:02:11 +0000 Subject: [PATCH 323/724] feat: `OpenPackage.get_Config.yaml.ps1` ( Fixes #132 ) --- OP.types.ps1xml | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 205d9ab..1d42267 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2127,6 +2127,32 @@ param() $partPattern = '[/\+]config\.json$' +foreach ($part in $this.GetParts()) { + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + + + + + Config.yaml + + <# +.SYNOPSIS + Gets a package's config yaml +.DESCRIPTION + Gets the objects stored in any config.yaml files in the package. +.NOTES + config.yaml files are used by several static site generators. +#> +[OutputType([PSObject])] +param() + +$partPattern = '[/\+]config\.ya?ml$' + foreach ($part in $this.GetParts()) { if ($part.Reader) { $part.Read() @@ -5399,6 +5425,32 @@ param() $partPattern = '[/\+]config\.json$' +foreach ($part in $this.GetParts()) { + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + + + + + Config.yaml + + <# +.SYNOPSIS + Gets a package's config yaml +.DESCRIPTION + Gets the objects stored in any config.yaml files in the package. +.NOTES + config.yaml files are used by several static site generators. +#> +[OutputType([PSObject])] +param() + +$partPattern = '[/\+]config\.ya?ml$' + foreach ($part in $this.GetParts()) { if ($part.Reader) { $part.Read() @@ -8671,6 +8723,32 @@ param() $partPattern = '[/\+]config\.json$' +foreach ($part in $this.GetParts()) { + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + + + + + Config.yaml + + <# +.SYNOPSIS + Gets a package's config yaml +.DESCRIPTION + Gets the objects stored in any config.yaml files in the package. +.NOTES + config.yaml files are used by several static site generators. +#> +[OutputType([PSObject])] +param() + +$partPattern = '[/\+]config\.ya?ml$' + foreach ($part in $this.GetParts()) { if ($part.Reader) { $part.Read() From 253c51f14e85468fef86ee455b00899170c554ed Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 18:23:16 -0700 Subject: [PATCH 324/724] feat: `OpenPackage.get_Config.json/yaml.ps1` ( Fixes #65, Fixes #132 ) Enforcing pattern --- Types/OpenPackage/get_Config.json.ps1 | 1 + Types/OpenPackage/get_Config.yaml.ps1 | 1 + 2 files changed, 2 insertions(+) diff --git a/Types/OpenPackage/get_Config.json.ps1 b/Types/OpenPackage/get_Config.json.ps1 index a9dff89..cbc79e1 100644 --- a/Types/OpenPackage/get_Config.json.ps1 +++ b/Types/OpenPackage/get_Config.json.ps1 @@ -12,6 +12,7 @@ param() $partPattern = '[/\+]config\.json$' foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch $partPattern) { continue } if ($part.Reader) { $part.Read() } else { diff --git a/Types/OpenPackage/get_Config.yaml.ps1 b/Types/OpenPackage/get_Config.yaml.ps1 index ca0c21a..29c56d9 100644 --- a/Types/OpenPackage/get_Config.yaml.ps1 +++ b/Types/OpenPackage/get_Config.yaml.ps1 @@ -12,6 +12,7 @@ param() $partPattern = '[/\+]config\.ya?ml$' foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch $partPattern) { continue } if ($part.Reader) { $part.Read() } else { From a29aa853859c9adbd8e4f98c3183b0046ec49b9f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 01:23:40 +0000 Subject: [PATCH 325/724] feat: `OpenPackage.get_Config.json/yaml.ps1` ( Fixes #65, Fixes #132 ) Enforcing pattern --- OP.types.ps1xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 1d42267..92f3056 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2128,6 +2128,7 @@ param() $partPattern = '[/\+]config\.json$' foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch $partPattern) { continue } if ($part.Reader) { $part.Read() } else { @@ -2154,6 +2155,7 @@ param() $partPattern = '[/\+]config\.ya?ml$' foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch $partPattern) { continue } if ($part.Reader) { $part.Read() } else { @@ -5426,6 +5428,7 @@ param() $partPattern = '[/\+]config\.json$' foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch $partPattern) { continue } if ($part.Reader) { $part.Read() } else { @@ -5452,6 +5455,7 @@ param() $partPattern = '[/\+]config\.ya?ml$' foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch $partPattern) { continue } if ($part.Reader) { $part.Read() } else { @@ -8724,6 +8728,7 @@ param() $partPattern = '[/\+]config\.json$' foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch $partPattern) { continue } if ($part.Reader) { $part.Read() } else { @@ -8750,6 +8755,7 @@ param() $partPattern = '[/\+]config\.ya?ml$' foreach ($part in $this.GetParts()) { + if ($part.Uri -notmatch $partPattern) { continue } if ($part.Reader) { $part.Read() } else { From ae843505e956a986c8e8fce3110234cffc0a8640 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 17 Mar 2026 18:29:52 -0700 Subject: [PATCH 326/724] feat: `OpenPackage.get_Config.json/yaml.ps1` ( Fixes #65, Fixes #132 ) Improving pattern --- Types/OpenPackage/get_Config.json.ps1 | 2 +- Types/OpenPackage/get_Config.yaml.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage/get_Config.json.ps1 b/Types/OpenPackage/get_Config.json.ps1 index cbc79e1..7747444 100644 --- a/Types/OpenPackage/get_Config.json.ps1 +++ b/Types/OpenPackage/get_Config.json.ps1 @@ -9,7 +9,7 @@ [OutputType([PSObject])] param() -$partPattern = '[/\+]config\.json$' +$partPattern = '[/\+_]config\.json$' foreach ($part in $this.GetParts()) { if ($part.Uri -notmatch $partPattern) { continue } diff --git a/Types/OpenPackage/get_Config.yaml.ps1 b/Types/OpenPackage/get_Config.yaml.ps1 index 29c56d9..b511b3e 100644 --- a/Types/OpenPackage/get_Config.yaml.ps1 +++ b/Types/OpenPackage/get_Config.yaml.ps1 @@ -9,7 +9,7 @@ [OutputType([PSObject])] param() -$partPattern = '[/\+]config\.ya?ml$' +$partPattern = '[/\+_]config\.ya?ml$' foreach ($part in $this.GetParts()) { if ($part.Uri -notmatch $partPattern) { continue } From 9018ebc6c8f2864d01347483fa4b10ca1d31aec5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 01:30:22 +0000 Subject: [PATCH 327/724] feat: `OpenPackage.get_Config.json/yaml.ps1` ( Fixes #65, Fixes #132 ) Improving pattern --- OP.types.ps1xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 92f3056..3e0c848 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2125,7 +2125,7 @@ $this.GetContent("/tools/chocolateyInstall.ps1") [OutputType([PSObject])] param() -$partPattern = '[/\+]config\.json$' +$partPattern = '[/\+_]config\.json$' foreach ($part in $this.GetParts()) { if ($part.Uri -notmatch $partPattern) { continue } @@ -2152,7 +2152,7 @@ foreach ($part in $this.GetParts()) { [OutputType([PSObject])] param() -$partPattern = '[/\+]config\.ya?ml$' +$partPattern = '[/\+_]config\.ya?ml$' foreach ($part in $this.GetParts()) { if ($part.Uri -notmatch $partPattern) { continue } @@ -5425,7 +5425,7 @@ $this.GetContent("/tools/chocolateyInstall.ps1") [OutputType([PSObject])] param() -$partPattern = '[/\+]config\.json$' +$partPattern = '[/\+_]config\.json$' foreach ($part in $this.GetParts()) { if ($part.Uri -notmatch $partPattern) { continue } @@ -5452,7 +5452,7 @@ foreach ($part in $this.GetParts()) { [OutputType([PSObject])] param() -$partPattern = '[/\+]config\.ya?ml$' +$partPattern = '[/\+_]config\.ya?ml$' foreach ($part in $this.GetParts()) { if ($part.Uri -notmatch $partPattern) { continue } @@ -8725,7 +8725,7 @@ $this.GetContent("/tools/chocolateyInstall.ps1") [OutputType([PSObject])] param() -$partPattern = '[/\+]config\.json$' +$partPattern = '[/\+_]config\.json$' foreach ($part in $this.GetParts()) { if ($part.Uri -notmatch $partPattern) { continue } @@ -8752,7 +8752,7 @@ foreach ($part in $this.GetParts()) { [OutputType([PSObject])] param() -$partPattern = '[/\+]config\.ya?ml$' +$partPattern = '[/\+_]config\.ya?ml$' foreach ($part in $this.GetParts()) { if ($part.Uri -notmatch $partPattern) { continue } From 457b05eb98f12208725675a6c7c7c979b0e24d24 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 11:59:19 -0700 Subject: [PATCH 328/724] feat: `OpenPackage.Part.ReadText` ( Fixes #97 ) Opening up Read signature --- Types/OpenPackage.Part/ReadText.ps1 | 42 +++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/Types/OpenPackage.Part/ReadText.ps1 b/Types/OpenPackage.Part/ReadText.ps1 index 948a336..d1cfebf 100644 --- a/Types/OpenPackage.Part/ReadText.ps1 +++ b/Types/OpenPackage.Part/ReadText.ps1 @@ -19,21 +19,41 @@ 'Order', 100 )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -# If there is no part, return -if (-not $part) { return } +if (-not $InputObject -and + ($this -is [IO.Packaging.PackagePart]) +) { + $Stream = $this.GetStream('Open','Read') -$partStream = $part.GetStream('Open','Read') + $streamReader = [IO.StreamReader]::new($Stream, $true) -$streamReader = [IO.StreamReader]::new($partStream, $true) + $partText = $streamReader.ReadToEnd() -$partText = $streamReader.ReadToEnd() + $Stream.Close() + $Stream.Dispose() -$partStream.Close() -$partStream.Dispose() + $streamReader.Close() + $streamReader.Dispose() -$streamReader.Close() -$streamReader.Dispose() + $partText +} elseif ($InputObject -is [IO.Stream]) { -$partText \ No newline at end of file + $streamReader = [IO.StreamReader]::new($InputObject, $true) + $partText = $streamReader.ReadToEnd() + $streamReader.Close() + $streamReader.Dispose() + $partText +} elseif ($inputObject -is [string]) { + $InputObject +} From cdb0f700f2afb8600682299f83251c6a77362b6c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:00:23 -0700 Subject: [PATCH 329/724] feat: `OpenPackage.Part.ReadJson` ( Fixes #99 ) Opening up Read signature --- Types/OpenPackage.Part/ReadJson.ps1 | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Types/OpenPackage.Part/ReadJson.ps1 b/Types/OpenPackage.Part/ReadJson.ps1 index d39e722..53dba58 100644 --- a/Types/OpenPackage.Part/ReadJson.ps1 +++ b/Types/OpenPackage.Part/ReadJson.ps1 @@ -6,11 +6,19 @@ #> [Reflection.AssemblyMetadata('FilePattern', '\.jsonc?$')] [Reflection.AssemblyMetadata('ContentTypePattern', '[/\+]jsonc?$')] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } -$partText = $part.ReadText($part) - +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) ConvertFrom-Json -InputObject $partText From 4ec6ab55ee48bceeafe3c477657df28903845fc1 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:01:23 -0700 Subject: [PATCH 330/724] feat: `OpenPackage.Part.ReadXml` ( Fixes #98 ) Opening up Read signature --- Types/OpenPackage.Part/ReadXML.ps1 | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage.Part/ReadXML.ps1 b/Types/OpenPackage.Part/ReadXML.ps1 index 0c060e8..bd2f828 100644 --- a/Types/OpenPackage.Part/ReadXML.ps1 +++ b/Types/OpenPackage.Part/ReadXML.ps1 @@ -10,10 +10,20 @@ [Reflection.AssemblyMetadata( 'ContentTypePattern', '[/\+]xml?$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -if (-not $part) { return } -$partText = $part.ReadText($part) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) return [xml]$partText From 8a666a7354d4576bcd7b724b7043ad27a1172d8d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:02:03 -0700 Subject: [PATCH 331/724] feat: `OpenPackage.Part.ReadClixml` ( Fixes #102 ) Opening up Read signature --- Types/OpenPackage.Part/ReadCliXml.ps1 | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage.Part/ReadCliXml.ps1 b/Types/OpenPackage.Part/ReadCliXml.ps1 index 8b27862..d88d83a 100644 --- a/Types/OpenPackage.Part/ReadCliXml.ps1 +++ b/Types/OpenPackage.Part/ReadCliXml.ps1 @@ -5,10 +5,20 @@ Reads Open Package Part Content as Clixml #> [Reflection.AssemblyMetadata('FilePattern', '\.(?>clixml|clix)?$')] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } -if (-not $part) { return } -$partText = $part.ReadText($part) +$partText = $this.ReadText($InputObject, $option) return [Management.Automation.PSSerializer]::Deserialize($partText) From 08c91c4ba93a826d444151fe5de8a6884b584845 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:02:39 -0700 Subject: [PATCH 332/724] feat: `OpenPackage.Part.ReadCSharp` ( Fixes #103 ) Opening up Read signature --- Types/OpenPackage.Part/ReadCSharp.ps1 | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Part/ReadCSharp.ps1 b/Types/OpenPackage.Part/ReadCSharp.ps1 index 002d27a..a9f5d7e 100644 --- a/Types/OpenPackage.Part/ReadCSharp.ps1 +++ b/Types/OpenPackage.Part/ReadCSharp.ps1 @@ -9,8 +9,21 @@ 'FilePattern', '\.cs$' )] -param() -$partString = $Part.ReadText($part) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + +$partString = $this.ReadText($inputObject, $option) + $null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { @@ -18,4 +31,6 @@ if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { Add-Member NoteProperty SyntaxTree ( [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) ) -Force -PassThru +} else { + $partString } \ No newline at end of file From a8b2c3e496982a0d0ff2836566078dba3fc31b4e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:03:54 -0700 Subject: [PATCH 333/724] feat: `OpenPackage.Part.ReadFormData` ( Fixes #108 ) Opening up Read signature --- Types/OpenPackage.Part/ReadFormData.ps1 | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage.Part/ReadFormData.ps1 b/Types/OpenPackage.Part/ReadFormData.ps1 index 1c50587..38db2bc 100644 --- a/Types/OpenPackage.Part/ReadFormData.ps1 +++ b/Types/OpenPackage.Part/ReadFormData.ps1 @@ -8,10 +8,21 @@ 'ContentTypePattern', '[/\+]x-www-form-urlencoded' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } -if (-not $part) { return } -$partText = $part.ReadText($part) + +$partText = $this.ReadText($InputObject, $Option) $formData = [Web.HttpUtility]::ParseQueryString($partText) $formDataObject = [Ordered]@{} From 7d582b3ffdeb3d11a186fbf06307a51a7753fd66 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:04:37 -0700 Subject: [PATCH 334/724] feat: `OpenPackage.Part.ReadJsonL` ( Fixes #104 ) Opening up Read signature --- Types/OpenPackage.Part/ReadJsonL.ps1 | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Types/OpenPackage.Part/ReadJsonL.ps1 b/Types/OpenPackage.Part/ReadJsonL.ps1 index a7e2726..a15022a 100644 --- a/Types/OpenPackage.Part/ReadJsonL.ps1 +++ b/Types/OpenPackage.Part/ReadJsonL.ps1 @@ -8,10 +8,19 @@ 'FilePattern', '\.(?>cast|jsonl)?$' )] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } -$partText = $part.ReadText($part) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) $partText -split '(?>\r\n|\n)' | ConvertFrom-Json From 19b7781b1bd95ff86cc6efc2b318f53d58975f5d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:05:33 -0700 Subject: [PATCH 335/724] feat: `OpenPackage.Part.ReadMarkdown` ( Fixes #105 ) Opening up Read signature --- Types/OpenPackage.Part/ReadMarkdown.ps1 | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage.Part/ReadMarkdown.ps1 b/Types/OpenPackage.Part/ReadMarkdown.ps1 index b48ddda..05db5bf 100644 --- a/Types/OpenPackage.Part/ReadMarkdown.ps1 +++ b/Types/OpenPackage.Part/ReadMarkdown.ps1 @@ -14,13 +14,25 @@ 'ContentTypePattern', '[/\+]markdown' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + $convertFromMarkdownCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Markdown', 'Cmdlet,Function') -$partString = $Part.ReadText() +$partString = $this.ReadText($InputObject, $Option) if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parameters.InputObject) { Write-Warning "ConvertFrom-Markdown not found" $partString = [PSObject]::new($partString) - $partString.pstypenames.insert(0, 'text/markdown') + $partString.pstypenames.add('text/markdown') $partString } else { try { From a3f63a9bddbb75c18ec4ec46e5053436f8327825 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:06:04 -0700 Subject: [PATCH 336/724] feat: `OpenPackage.Part.ReadPowerShell` ( Fixes #100 ) Opening up Read signature --- Types/OpenPackage.Part/ReadPowerShell.ps1 | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Part/ReadPowerShell.ps1 b/Types/OpenPackage.Part/ReadPowerShell.ps1 index 479a762..a63a4b6 100644 --- a/Types/OpenPackage.Part/ReadPowerShell.ps1 +++ b/Types/OpenPackage.Part/ReadPowerShell.ps1 @@ -9,8 +9,19 @@ 'FilePattern', '\.psm?1$' )] -param([IO.Packaging.PackagePart]$part = $this ) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -[ScriptBlock]::Create($part.ReadText($part)) +if (-not $this.ReadText) { return } +[ScriptBlock]::Create($this.ReadText($InputObject, $Option)) From fa8263789d8fb3e805fc9dacde0c42fb2bd12e97 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:06:37 -0700 Subject: [PATCH 337/724] feat: `OpenPackage.Part.ReadPowerShellData` ( Fixes #101 ) Opening up Read signature --- Types/OpenPackage.Part/ReadPowerShellData.ps1 | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Part/ReadPowerShellData.ps1 b/Types/OpenPackage.Part/ReadPowerShellData.ps1 index 17079e8..c36152d 100644 --- a/Types/OpenPackage.Part/ReadPowerShellData.ps1 +++ b/Types/OpenPackage.Part/ReadPowerShellData.ps1 @@ -9,9 +9,21 @@ 'FilePattern', '\.psd1$' )] -param([IO.Packaging.PackagePart]$part = $this ) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -$datablock = [ScriptBlock]::Create("data {$($part.ReadText($part)) }") +if (-not $this.ReadText) { return } + +$datablock = [ScriptBlock]::Create("data {$($this.ReadText($InputObject, $Option)) }") if ($datablock.Ast.EndBlock.Statements.Count -gt 1) { return } if ($datablock.Ast.EndBlock.Statements[0] -isnot [Management.Automation.Language.DataStatementAst] From a0f7a3750f1d83463bcc78d17802712004454e92 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:10:30 -0700 Subject: [PATCH 338/724] feat: `OpenPackage.Part.ReadToml` ( Fixes #106 ) Opening up Read signature --- Types/OpenPackage.Part/ReadToml.ps1 | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Part/ReadToml.ps1 b/Types/OpenPackage.Part/ReadToml.ps1 index 83d13ca..267e3d7 100644 --- a/Types/OpenPackage.Part/ReadToml.ps1 +++ b/Types/OpenPackage.Part/ReadToml.ps1 @@ -13,11 +13,27 @@ 'FilePattern', '\.toml$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + + +if (-not $this.ReadText) { return } + $convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') -$partString = $Part.ReadText($Part) +$partString = $this.ReadText($InputObject, $Option) + if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { Write-Warning "ConvertFrom-Toml not found, please install PSToml" + $partString } else { try { $partString | & $convertFromTomlCommand -ErrorAction Stop From 5d420ab324c88ef99ae053a9e0e9385a1dd84350 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:11:01 -0700 Subject: [PATCH 339/724] feat: `OpenPackage.Part.ReadYaml` ( Fixes #107 ) Opening up Read signature --- Types/OpenPackage.Part/ReadYaml.ps1 | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Part/ReadYaml.ps1 b/Types/OpenPackage.Part/ReadYaml.ps1 index 01199c2..60abe10 100644 --- a/Types/OpenPackage.Part/ReadYaml.ps1 +++ b/Types/OpenPackage.Part/ReadYaml.ps1 @@ -13,11 +13,25 @@ 'FilePattern', '\.ya?ml$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + + +if (-not $this.ReadText) { return } $convertFromYamlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') -$partString = $Part.ReadText($Part) +$partString = $this.ReadText($InputObject, $Option) if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.InputObject) { Write-Warning "Convert-FromYaml not found, please install YaYaml" + $partString } else { try { $partString | & $convertFromYamlCommand -ErrorAction Stop From 8647bab8c6c5bd809797262dca880043e44eeb79 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:12:24 -0700 Subject: [PATCH 340/724] feat: `OpenPackage.Part.ReadXsd` ( Fixes #109 ) Opening up Read signature --- Types/OpenPackage.Part/ReadXsd.ps1 | 64 ++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/Types/OpenPackage.Part/ReadXsd.ps1 b/Types/OpenPackage.Part/ReadXsd.ps1 index 7f54f73..891aaaf 100644 --- a/Types/OpenPackage.Part/ReadXsd.ps1 +++ b/Types/OpenPackage.Part/ReadXsd.ps1 @@ -11,24 +11,54 @@ 'ContentTypePattern', '[/\+]xsd?$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -if (-not $part) { return } +$xmlReaderSettings = [Xml.XmlReaderSettings]::new() +$xmlReaderSettings.DtdProcessing = 'Parse' +foreach ($key in $options.Keys) { + if ($xmlReaderSettings.psobject.Properties[$key].IsSettable) { + $xmlReaderSettings.$key = $option[$key] + } +} + +if ($InputObject -is [IO.Stream]) { + try { + $xmlReader = [Xml.XmlReader]::Create($InputObject, $xmlReaderSettings) + [Xml.Schema.XmlSchema]::Read($xmlReader,{}) + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } + } +} elseif ($this.GetStream) { + try { + $partStream = $this.GetStream('Open','Read') + $xmlReader = [Xml.XmlReader]::Create($partStream, $xmlReaderSettings) + [Xml.Schema.XmlSchema]::Read($xmlReader,{}) + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } -try { - $partStream = $part.GetStream() - $xmlReaderSettings = [Xml.XmlReaderSettings]::new() - $xmlReaderSettings.DtdProcessing = 'Parse' - $xmlReader = [Xml.XmlReader]::Create($partStream, $xmlReaderSettings) - [Xml.Schema.XmlSchema]::Read($xmlReader,{}) -} catch { - $_ -} finally { - if ($xmlReader) { - $xmlReader.Close() - $xmlReader.Dispose() + $partStream.Close() + $partStream.Dispose() } +} + - $partStream.Close() - $partStream.Dispose() -} \ No newline at end of file From 65ceb778a8873ea534bf429456b0d22bc3026233 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:12:59 -0700 Subject: [PATCH 341/724] feat: `OpenPackage.Part.ReadXslt` ( Fixes #110 ) Opening up Read signature --- Types/OpenPackage.Part/ReadXslt.ps1 | 58 ++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/Types/OpenPackage.Part/ReadXslt.ps1 b/Types/OpenPackage.Part/ReadXslt.ps1 index ca4f81e..70c8a67 100644 --- a/Types/OpenPackage.Part/ReadXslt.ps1 +++ b/Types/OpenPackage.Part/ReadXslt.ps1 @@ -11,24 +11,48 @@ 'ContentTypePattern', '[/\+]xslt?$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -if (-not $part) { return } - -try { - $partStream = $part.GetStream() - $xmlReader = [Xml.XmlReader]::Create($partStream) - $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() - $xslTransformer.Load($xmlReader) - $xslTransformer -} catch { - $_ -} finally { - if ($xmlReader) { - $xmlReader.Close() - $xmlReader.Dispose() +if ($InputObject -is [IO.Stream]) { + try { + $xmlReader = [Xml.XmlReader]::Create($InputObject) + $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() + $xslTransformer.Load($xmlReader) + $xslTransformer + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } } +} elseif ($this.GetStream) { + try { + $partStream = $this.GetStream('Open', 'Read') + $xmlReader = [Xml.XmlReader]::Create($partStream) + $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() + $xslTransformer.Load($xmlReader) + $xslTransformer + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } - $partStream.Close() - $partStream.Dispose() + $partStream.Close() + $partStream.Dispose() + } } \ No newline at end of file From 52b0bda159251ed828e629531e137b6ab727a03e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 12:14:57 -0700 Subject: [PATCH 342/724] feat: `OpenPackage.Part.Read` ( Fixes #95 ) Opening up Read signature (allowing reads from arbitrary sources and adding plumbing for options) --- Types/OpenPackage.Part/Read.ps1 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/Read.ps1 b/Types/OpenPackage.Part/Read.ps1 index 3d52e7f..d35020f 100644 --- a/Types/OpenPackage.Part/Read.ps1 +++ b/Types/OpenPackage.Part/Read.ps1 @@ -4,6 +4,18 @@ .DESCRIPTION Reads Open Package Parts, using the `.Reader` associated with this part. #> +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + filter addPackageAndPart { $_ | Add-Member NoteProperty Package $this.Package -Force -PassThru | @@ -18,7 +30,7 @@ if (-not $orderedMethods) { } $method = $orderedMethods[0] return $( - $method.Invoke() | addPackageAndPart + $method.Invoke($InputObject, $Option) | addPackageAndPart ) From 37adb8e5a9cc5a0095afe3b831286c532983f6f4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 19:15:37 +0000 Subject: [PATCH 343/724] feat: `OpenPackage.Part.Read` ( Fixes #95 ) Opening up Read signature (allowing reads from arbitrary sources and adding plumbing for options) --- OP.types.ps1xml | 1104 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 876 insertions(+), 228 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 3e0c848..9e47c07 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -10108,6 +10108,18 @@ $null = $readStream.DisposeAsync() .DESCRIPTION Reads Open Package Parts, using the `.Reader` associated with this part. #> +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + filter addPackageAndPart { $_ | Add-Member NoteProperty Package $this.Package -Force -PassThru | @@ -10122,7 +10134,7 @@ if (-not $orderedMethods) { } $method = $orderedMethods[0] return $( - $method.Invoke() | addPackageAndPart + $method.Invoke($InputObject, $Option) | addPackageAndPart ) @@ -10139,10 +10151,20 @@ return $( Reads Open Package Part Content as Clixml #> [Reflection.AssemblyMetadata('FilePattern', '\.(?>clixml|clix)?$')] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } -if (-not $part) { return } -$partText = $part.ReadText($part) +$partText = $this.ReadText($InputObject, $option) return [Management.Automation.PSSerializer]::Deserialize($partText) @@ -10164,8 +10186,21 @@ return [Management.Automation.PSSerializer]::Deserialize($partText) 'FilePattern', '\.cs$' )] -param() -$partString = $Part.ReadText($part) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + +$partString = $this.ReadText($inputObject, $option) + $null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { @@ -10173,6 +10208,8 @@ if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { Add-Member NoteProperty SyntaxTree ( [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) ) -Force -PassThru +} else { + $partString } @@ -10189,10 +10226,21 @@ if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { 'ContentTypePattern', '[/\+]x-www-form-urlencoded' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + -if (-not $part) { return } -$partText = $part.ReadText($part) +$partText = $this.ReadText($InputObject, $Option) $formData = [Web.HttpUtility]::ParseQueryString($partText) $formDataObject = [Ordered]@{} @@ -10219,11 +10267,19 @@ return $formDataObject #> [Reflection.AssemblyMetadata('FilePattern', '\.jsonc?$')] [Reflection.AssemblyMetadata('ContentTypePattern', '[/\+]jsonc?$')] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } -$partText = $part.ReadText($part) - +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) ConvertFrom-Json -InputObject $partText @@ -10243,10 +10299,19 @@ ConvertFrom-Json -InputObject $partText 'FilePattern', '\.(?>cast|jsonl)?$' )] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } -$partText = $part.ReadText($part) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) $partText -split '(?>\r\n|\n)' | ConvertFrom-Json @@ -10272,13 +10337,25 @@ $partText -split '(?>\r\n|\n)' | ConvertFrom-Json 'ContentTypePattern', '[/\+]markdown' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + $convertFromMarkdownCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Markdown', 'Cmdlet,Function') -$partString = $Part.ReadText() +$partString = $this.ReadText($InputObject, $Option) if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parameters.InputObject) { Write-Warning "ConvertFrom-Markdown not found" $partString = [PSObject]::new($partString) - $partString.pstypenames.insert(0, 'text/markdown') + $partString.pstypenames.add('text/markdown') $partString } else { try { @@ -10305,9 +10382,20 @@ if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parame 'FilePattern', '\.psm?1$' )] -param([IO.Packaging.PackagePart]$part = $this ) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -[ScriptBlock]::Create($part.ReadText($part)) +if (-not $this.ReadText) { return } +[ScriptBlock]::Create($this.ReadText($InputObject, $Option)) @@ -10327,9 +10415,21 @@ param([IO.Packaging.PackagePart]$part = $this ) 'FilePattern', '\.psd1$' )] -param([IO.Packaging.PackagePart]$part = $this ) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + +if (-not $this.ReadText) { return } -$datablock = [ScriptBlock]::Create("data {$($part.ReadText($part)) }") +$datablock = [ScriptBlock]::Create("data {$($this.ReadText($InputObject, $Option)) }") if ($datablock.Ast.EndBlock.Statements.Count -gt 1) { return } if ($datablock.Ast.EndBlock.Statements[0] -isnot [Management.Automation.Language.DataStatementAst] @@ -10370,24 +10470,45 @@ if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { 'Order', 100 )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -# If there is no part, return -if (-not $part) { return } +if (-not $InputObject -and + ($this -is [IO.Packaging.PackagePart]) +) { + $Stream = $this.GetStream('Open','Read') -$partStream = $part.GetStream('Open','Read') + $streamReader = [IO.StreamReader]::new($Stream, $true) -$streamReader = [IO.StreamReader]::new($partStream, $true) + $partText = $streamReader.ReadToEnd() -$partText = $streamReader.ReadToEnd() + $Stream.Close() + $Stream.Dispose() -$partStream.Close() -$partStream.Dispose() + $streamReader.Close() + $streamReader.Dispose() -$streamReader.Close() -$streamReader.Dispose() + $partText +} elseif ($InputObject -is [IO.Stream]) { + + $streamReader = [IO.StreamReader]::new($InputObject, $true) + $partText = $streamReader.ReadToEnd() + $streamReader.Close() + $streamReader.Dispose() + $partText +} elseif ($inputObject -is [string]) { + $InputObject +} -$partText @@ -10408,11 +10529,27 @@ $partText 'FilePattern', '\.toml$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + + +if (-not $this.ReadText) { return } + $convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') -$partString = $Part.ReadText($Part) +$partString = $this.ReadText($InputObject, $Option) + if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { Write-Warning "ConvertFrom-Toml not found, please install PSToml" + $partString } else { try { $partString | & $convertFromTomlCommand -ErrorAction Stop @@ -10437,10 +10574,20 @@ if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.Inp [Reflection.AssemblyMetadata( 'ContentTypePattern', '[/\+]xml?$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -if (-not $part) { return } -$partText = $part.ReadText($part) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) return [xml]$partText @@ -10464,27 +10611,58 @@ return [xml]$partText 'ContentTypePattern', '[/\+]xsd?$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -if (-not $part) { return } +$xmlReaderSettings = [Xml.XmlReaderSettings]::new() +$xmlReaderSettings.DtdProcessing = 'Parse' +foreach ($key in $options.Keys) { + if ($xmlReaderSettings.psobject.Properties[$key].IsSettable) { + $xmlReaderSettings.$key = $option[$key] + } +} -try { - $partStream = $part.GetStream() - $xmlReaderSettings = [Xml.XmlReaderSettings]::new() - $xmlReaderSettings.DtdProcessing = 'Parse' - $xmlReader = [Xml.XmlReader]::Create($partStream, $xmlReaderSettings) - [Xml.Schema.XmlSchema]::Read($xmlReader,{}) -} catch { - $_ -} finally { - if ($xmlReader) { - $xmlReader.Close() - $xmlReader.Dispose() +if ($InputObject -is [IO.Stream]) { + try { + $xmlReader = [Xml.XmlReader]::Create($InputObject, $xmlReaderSettings) + [Xml.Schema.XmlSchema]::Read($xmlReader,{}) + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } } +} elseif ($this.GetStream) { + try { + $partStream = $this.GetStream('Open','Read') + $xmlReader = [Xml.XmlReader]::Create($partStream, $xmlReaderSettings) + [Xml.Schema.XmlSchema]::Read($xmlReader,{}) + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } - $partStream.Close() - $partStream.Dispose() + $partStream.Close() + $partStream.Dispose() + } } + + + @@ -10503,26 +10681,50 @@ try { 'ContentTypePattern', '[/\+]xslt?$' )] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -try { - $partStream = $part.GetStream() - $xmlReader = [Xml.XmlReader]::Create($partStream) - $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() - $xslTransformer.Load($xmlReader) - $xslTransformer -} catch { - $_ -} finally { - if ($xmlReader) { - $xmlReader.Close() - $xmlReader.Dispose() +if ($InputObject -is [IO.Stream]) { + try { + $xmlReader = [Xml.XmlReader]::Create($InputObject) + $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() + $xslTransformer.Load($xmlReader) + $xslTransformer + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } } +} elseif ($this.GetStream) { + try { + $partStream = $this.GetStream('Open', 'Read') + $xmlReader = [Xml.XmlReader]::Create($partStream) + $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() + $xslTransformer.Load($xmlReader) + $xslTransformer + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } - $partStream.Close() - $partStream.Dispose() + $partStream.Close() + $partStream.Dispose() + } } @@ -10544,11 +10746,25 @@ try { 'FilePattern', '\.ya?ml$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + + +if (-not $this.ReadText) { return } $convertFromYamlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') -$partString = $Part.ReadText($Part) +$partString = $this.ReadText($InputObject, $Option) if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.InputObject) { Write-Warning "Convert-FromYaml not found, please install YaYaml" + $partString } else { try { $partString | & $convertFromYamlCommand -ErrorAction Stop @@ -10691,6 +10907,18 @@ $null = $readStream.DisposeAsync() .DESCRIPTION Reads Open Package Parts, using the `.Reader` associated with this part. #> +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + filter addPackageAndPart { $_ | Add-Member NoteProperty Package $this.Package -Force -PassThru | @@ -10705,7 +10933,7 @@ if (-not $orderedMethods) { } $method = $orderedMethods[0] return $( - $method.Invoke() | addPackageAndPart + $method.Invoke($InputObject, $Option) | addPackageAndPart ) @@ -10722,10 +10950,20 @@ return $( Reads Open Package Part Content as Clixml #> [Reflection.AssemblyMetadata('FilePattern', '\.(?>clixml|clix)?$')] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } -if (-not $part) { return } -$partText = $part.ReadText($part) +$partText = $this.ReadText($InputObject, $option) return [Management.Automation.PSSerializer]::Deserialize($partText) @@ -10747,8 +10985,21 @@ return [Management.Automation.PSSerializer]::Deserialize($partText) 'FilePattern', '\.cs$' )] -param() -$partString = $Part.ReadText($part) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + +$partString = $this.ReadText($inputObject, $option) + $null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { @@ -10756,6 +11007,8 @@ if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { Add-Member NoteProperty SyntaxTree ( [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) ) -Force -PassThru +} else { + $partString } @@ -10772,10 +11025,21 @@ if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { 'ContentTypePattern', '[/\+]x-www-form-urlencoded' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } -if (-not $part) { return } -$partText = $part.ReadText($part) + +$partText = $this.ReadText($InputObject, $Option) $formData = [Web.HttpUtility]::ParseQueryString($partText) $formDataObject = [Ordered]@{} @@ -10802,11 +11066,19 @@ return $formDataObject #> [Reflection.AssemblyMetadata('FilePattern', '\.jsonc?$')] [Reflection.AssemblyMetadata('ContentTypePattern', '[/\+]jsonc?$')] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } -$partText = $part.ReadText($part) - +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) ConvertFrom-Json -InputObject $partText @@ -10826,10 +11098,19 @@ ConvertFrom-Json -InputObject $partText 'FilePattern', '\.(?>cast|jsonl)?$' )] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } -$partText = $part.ReadText($part) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) $partText -split '(?>\r\n|\n)' | ConvertFrom-Json @@ -10855,13 +11136,25 @@ $partText -split '(?>\r\n|\n)' | ConvertFrom-Json 'ContentTypePattern', '[/\+]markdown' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + $convertFromMarkdownCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Markdown', 'Cmdlet,Function') -$partString = $Part.ReadText() +$partString = $this.ReadText($InputObject, $Option) if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parameters.InputObject) { Write-Warning "ConvertFrom-Markdown not found" $partString = [PSObject]::new($partString) - $partString.pstypenames.insert(0, 'text/markdown') + $partString.pstypenames.add('text/markdown') $partString } else { try { @@ -10888,9 +11181,20 @@ if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parame 'FilePattern', '\.psm?1$' )] -param([IO.Packaging.PackagePart]$part = $this ) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -[ScriptBlock]::Create($part.ReadText($part)) +if (-not $this.ReadText) { return } +[ScriptBlock]::Create($this.ReadText($InputObject, $Option)) @@ -10910,9 +11214,21 @@ param([IO.Packaging.PackagePart]$part = $this ) 'FilePattern', '\.psd1$' )] -param([IO.Packaging.PackagePart]$part = $this ) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -$datablock = [ScriptBlock]::Create("data {$($part.ReadText($part)) }") +if (-not $this.ReadText) { return } + +$datablock = [ScriptBlock]::Create("data {$($this.ReadText($InputObject, $Option)) }") if ($datablock.Ast.EndBlock.Statements.Count -gt 1) { return } if ($datablock.Ast.EndBlock.Statements[0] -isnot [Management.Automation.Language.DataStatementAst] @@ -10953,24 +11269,45 @@ if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { 'Order', 100 )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -# If there is no part, return -if (-not $part) { return } +if (-not $InputObject -and + ($this -is [IO.Packaging.PackagePart]) +) { + $Stream = $this.GetStream('Open','Read') -$partStream = $part.GetStream('Open','Read') + $streamReader = [IO.StreamReader]::new($Stream, $true) -$streamReader = [IO.StreamReader]::new($partStream, $true) + $partText = $streamReader.ReadToEnd() -$partText = $streamReader.ReadToEnd() + $Stream.Close() + $Stream.Dispose() -$partStream.Close() -$partStream.Dispose() + $streamReader.Close() + $streamReader.Dispose() -$streamReader.Close() -$streamReader.Dispose() + $partText +} elseif ($InputObject -is [IO.Stream]) { + + $streamReader = [IO.StreamReader]::new($InputObject, $true) + $partText = $streamReader.ReadToEnd() + $streamReader.Close() + $streamReader.Dispose() + $partText +} elseif ($inputObject -is [string]) { + $InputObject +} -$partText @@ -10991,11 +11328,27 @@ $partText 'FilePattern', '\.toml$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + + +if (-not $this.ReadText) { return } + $convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') -$partString = $Part.ReadText($Part) +$partString = $this.ReadText($InputObject, $Option) + if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { Write-Warning "ConvertFrom-Toml not found, please install PSToml" + $partString } else { try { $partString | & $convertFromTomlCommand -ErrorAction Stop @@ -11020,10 +11373,20 @@ if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.Inp [Reflection.AssemblyMetadata( 'ContentTypePattern', '[/\+]xml?$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -if (-not $part) { return } -$partText = $part.ReadText($part) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) return [xml]$partText @@ -11047,27 +11410,58 @@ return [xml]$partText 'ContentTypePattern', '[/\+]xsd?$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -if (-not $part) { return } +$xmlReaderSettings = [Xml.XmlReaderSettings]::new() +$xmlReaderSettings.DtdProcessing = 'Parse' +foreach ($key in $options.Keys) { + if ($xmlReaderSettings.psobject.Properties[$key].IsSettable) { + $xmlReaderSettings.$key = $option[$key] + } +} -try { - $partStream = $part.GetStream() - $xmlReaderSettings = [Xml.XmlReaderSettings]::new() - $xmlReaderSettings.DtdProcessing = 'Parse' - $xmlReader = [Xml.XmlReader]::Create($partStream, $xmlReaderSettings) - [Xml.Schema.XmlSchema]::Read($xmlReader,{}) -} catch { - $_ -} finally { - if ($xmlReader) { - $xmlReader.Close() - $xmlReader.Dispose() +if ($InputObject -is [IO.Stream]) { + try { + $xmlReader = [Xml.XmlReader]::Create($InputObject, $xmlReaderSettings) + [Xml.Schema.XmlSchema]::Read($xmlReader,{}) + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } } +} elseif ($this.GetStream) { + try { + $partStream = $this.GetStream('Open','Read') + $xmlReader = [Xml.XmlReader]::Create($partStream, $xmlReaderSettings) + [Xml.Schema.XmlSchema]::Read($xmlReader,{}) + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } - $partStream.Close() - $partStream.Dispose() + $partStream.Close() + $partStream.Dispose() + } } + + + @@ -11086,26 +11480,50 @@ try { 'ContentTypePattern', '[/\+]xslt?$' )] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -try { - $partStream = $part.GetStream() - $xmlReader = [Xml.XmlReader]::Create($partStream) - $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() - $xslTransformer.Load($xmlReader) - $xslTransformer -} catch { - $_ -} finally { - if ($xmlReader) { - $xmlReader.Close() - $xmlReader.Dispose() +if ($InputObject -is [IO.Stream]) { + try { + $xmlReader = [Xml.XmlReader]::Create($InputObject) + $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() + $xslTransformer.Load($xmlReader) + $xslTransformer + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } } +} elseif ($this.GetStream) { + try { + $partStream = $this.GetStream('Open', 'Read') + $xmlReader = [Xml.XmlReader]::Create($partStream) + $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() + $xslTransformer.Load($xmlReader) + $xslTransformer + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } - $partStream.Close() - $partStream.Dispose() + $partStream.Close() + $partStream.Dispose() + } } @@ -11127,11 +11545,25 @@ try { 'FilePattern', '\.ya?ml$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + + +if (-not $this.ReadText) { return } $convertFromYamlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') -$partString = $Part.ReadText($Part) +$partString = $this.ReadText($InputObject, $Option) if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.InputObject) { Write-Warning "Convert-FromYaml not found, please install YaYaml" + $partString } else { try { $partString | & $convertFromYamlCommand -ErrorAction Stop @@ -11274,6 +11706,18 @@ $null = $readStream.DisposeAsync() .DESCRIPTION Reads Open Package Parts, using the `.Reader` associated with this part. #> +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + filter addPackageAndPart { $_ | Add-Member NoteProperty Package $this.Package -Force -PassThru | @@ -11288,7 +11732,7 @@ if (-not $orderedMethods) { } $method = $orderedMethods[0] return $( - $method.Invoke() | addPackageAndPart + $method.Invoke($InputObject, $Option) | addPackageAndPart ) @@ -11305,10 +11749,20 @@ return $( Reads Open Package Part Content as Clixml #> [Reflection.AssemblyMetadata('FilePattern', '\.(?>clixml|clix)?$')] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } -if (-not $part) { return } -$partText = $part.ReadText($part) +$partText = $this.ReadText($InputObject, $option) return [Management.Automation.PSSerializer]::Deserialize($partText) @@ -11330,8 +11784,21 @@ return [Management.Automation.PSSerializer]::Deserialize($partText) 'FilePattern', '\.cs$' )] -param() -$partString = $Part.ReadText($part) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + +$partString = $this.ReadText($inputObject, $option) + $null = Add-Type -AssemblyName Microsoft.CodeAnalysis.CSharp -PassThru if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { @@ -11339,6 +11806,8 @@ if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { Add-Member NoteProperty SyntaxTree ( [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($partString) ) -Force -PassThru +} else { + $partString } @@ -11355,10 +11824,21 @@ if ('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree' -as [Type]) { 'ContentTypePattern', '[/\+]x-www-form-urlencoded' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + -if (-not $part) { return } -$partText = $part.ReadText($part) +$partText = $this.ReadText($InputObject, $Option) $formData = [Web.HttpUtility]::ParseQueryString($partText) $formDataObject = [Ordered]@{} @@ -11385,11 +11865,19 @@ return $formDataObject #> [Reflection.AssemblyMetadata('FilePattern', '\.jsonc?$')] [Reflection.AssemblyMetadata('ContentTypePattern', '[/\+]jsonc?$')] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } -$partText = $part.ReadText($part) - +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) ConvertFrom-Json -InputObject $partText @@ -11409,10 +11897,19 @@ ConvertFrom-Json -InputObject $partText 'FilePattern', '\.(?>cast|jsonl)?$' )] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } -$partText = $part.ReadText($part) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) $partText -split '(?>\r\n|\n)' | ConvertFrom-Json @@ -11438,13 +11935,25 @@ $partText -split '(?>\r\n|\n)' | ConvertFrom-Json 'ContentTypePattern', '[/\+]markdown' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) +if (-not $this.ReadText) { return } + $convertFromMarkdownCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Markdown', 'Cmdlet,Function') -$partString = $Part.ReadText() +$partString = $this.ReadText($InputObject, $Option) if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parameters.InputObject) { Write-Warning "ConvertFrom-Markdown not found" $partString = [PSObject]::new($partString) - $partString.pstypenames.insert(0, 'text/markdown') + $partString.pstypenames.add('text/markdown') $partString } else { try { @@ -11471,9 +11980,20 @@ if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parame 'FilePattern', '\.psm?1$' )] -param([IO.Packaging.PackagePart]$part = $this ) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -[ScriptBlock]::Create($part.ReadText($part)) +if (-not $this.ReadText) { return } +[ScriptBlock]::Create($this.ReadText($InputObject, $Option)) @@ -11493,9 +12013,21 @@ param([IO.Packaging.PackagePart]$part = $this ) 'FilePattern', '\.psd1$' )] -param([IO.Packaging.PackagePart]$part = $this ) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + +if (-not $this.ReadText) { return } -$datablock = [ScriptBlock]::Create("data {$($part.ReadText($part)) }") +$datablock = [ScriptBlock]::Create("data {$($this.ReadText($InputObject, $Option)) }") if ($datablock.Ast.EndBlock.Statements.Count -gt 1) { return } if ($datablock.Ast.EndBlock.Statements[0] -isnot [Management.Automation.Language.DataStatementAst] @@ -11536,24 +12068,45 @@ if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { 'Order', 100 )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -# If there is no part, return -if (-not $part) { return } +if (-not $InputObject -and + ($this -is [IO.Packaging.PackagePart]) +) { + $Stream = $this.GetStream('Open','Read') -$partStream = $part.GetStream('Open','Read') + $streamReader = [IO.StreamReader]::new($Stream, $true) -$streamReader = [IO.StreamReader]::new($partStream, $true) + $partText = $streamReader.ReadToEnd() -$partText = $streamReader.ReadToEnd() + $Stream.Close() + $Stream.Dispose() -$partStream.Close() -$partStream.Dispose() + $streamReader.Close() + $streamReader.Dispose() -$streamReader.Close() -$streamReader.Dispose() + $partText +} elseif ($InputObject -is [IO.Stream]) { + + $streamReader = [IO.StreamReader]::new($InputObject, $true) + $partText = $streamReader.ReadToEnd() + $streamReader.Close() + $streamReader.Dispose() + $partText +} elseif ($inputObject -is [string]) { + $InputObject +} -$partText @@ -11574,11 +12127,27 @@ $partText 'FilePattern', '\.toml$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + + +if (-not $this.ReadText) { return } + $convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') -$partString = $Part.ReadText($Part) +$partString = $this.ReadText($InputObject, $Option) + if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { Write-Warning "ConvertFrom-Toml not found, please install PSToml" + $partString } else { try { $partString | & $convertFromTomlCommand -ErrorAction Stop @@ -11603,10 +12172,20 @@ if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.Inp [Reflection.AssemblyMetadata( 'ContentTypePattern', '[/\+]xml?$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -if (-not $part) { return } -$partText = $part.ReadText($part) +if (-not $this.ReadText) { return } +$partText = $this.ReadText($InputObject, $Option) return [xml]$partText @@ -11630,27 +12209,58 @@ return [xml]$partText 'ContentTypePattern', '[/\+]xsd?$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -if (-not $part) { return } +$xmlReaderSettings = [Xml.XmlReaderSettings]::new() +$xmlReaderSettings.DtdProcessing = 'Parse' +foreach ($key in $options.Keys) { + if ($xmlReaderSettings.psobject.Properties[$key].IsSettable) { + $xmlReaderSettings.$key = $option[$key] + } +} -try { - $partStream = $part.GetStream() - $xmlReaderSettings = [Xml.XmlReaderSettings]::new() - $xmlReaderSettings.DtdProcessing = 'Parse' - $xmlReader = [Xml.XmlReader]::Create($partStream, $xmlReaderSettings) - [Xml.Schema.XmlSchema]::Read($xmlReader,{}) -} catch { - $_ -} finally { - if ($xmlReader) { - $xmlReader.Close() - $xmlReader.Dispose() +if ($InputObject -is [IO.Stream]) { + try { + $xmlReader = [Xml.XmlReader]::Create($InputObject, $xmlReaderSettings) + [Xml.Schema.XmlSchema]::Read($xmlReader,{}) + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } } +} elseif ($this.GetStream) { + try { + $partStream = $this.GetStream('Open','Read') + $xmlReader = [Xml.XmlReader]::Create($partStream, $xmlReaderSettings) + [Xml.Schema.XmlSchema]::Read($xmlReader,{}) + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } - $partStream.Close() - $partStream.Dispose() + $partStream.Close() + $partStream.Dispose() + } } + + + @@ -11669,26 +12279,50 @@ try { 'ContentTypePattern', '[/\+]xslt?$' )] -param([IO.Packaging.PackagePart]$Part = $this) - -if (-not $part) { return } +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) -try { - $partStream = $part.GetStream() - $xmlReader = [Xml.XmlReader]::Create($partStream) - $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() - $xslTransformer.Load($xmlReader) - $xslTransformer -} catch { - $_ -} finally { - if ($xmlReader) { - $xmlReader.Close() - $xmlReader.Dispose() +if ($InputObject -is [IO.Stream]) { + try { + $xmlReader = [Xml.XmlReader]::Create($InputObject) + $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() + $xslTransformer.Load($xmlReader) + $xslTransformer + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } } +} elseif ($this.GetStream) { + try { + $partStream = $this.GetStream('Open', 'Read') + $xmlReader = [Xml.XmlReader]::Create($partStream) + $xslTransformer = [xml.Xsl.XslCompiledTransform]::new() + $xslTransformer.Load($xmlReader) + $xslTransformer + } catch { + $_ + } finally { + if ($xmlReader) { + $xmlReader.Close() + $xmlReader.Dispose() + } - $partStream.Close() - $partStream.Dispose() + $partStream.Close() + $partStream.Dispose() + } } @@ -11710,11 +12344,25 @@ try { 'FilePattern', '\.ya?ml$' )] -param([IO.Packaging.PackagePart]$Part = $this) +param( + # An optional input object + # If provided, content will be read from this object. + # If not provided, content will be read from this part. + [Alias('Input')] + [PSObject]$InputObject = $null, + + # Any options used to read the data. + [Alias('Options')] + [Collections.IDictionary]$Option = [Ordered]@{} +) + + +if (-not $this.ReadText) { return } $convertFromYamlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') -$partString = $Part.ReadText($Part) +$partString = $this.ReadText($InputObject, $Option) if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.InputObject) { Write-Warning "Convert-FromYaml not found, please install YaYaml" + $partString } else { try { $partString | & $convertFromYamlCommand -ErrorAction Stop From 2d9b6600e7cf1967125b9ff0a1ba67c7458735ba Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 13:02:18 -0700 Subject: [PATCH 344/724] feat: `OpenPackage.get_Security.md` ( Fixes #135 ) --- Types/OpenPackage/get_Security.md.ps1 | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Security.md.ps1 diff --git a/Types/OpenPackage/get_Security.md.ps1 b/Types/OpenPackage/get_Security.md.ps1 new file mode 100644 index 0000000..1df9a1c --- /dev/null +++ b/Types/OpenPackage/get_Security.md.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Gets a package's security notice +.DESCRIPTION + Gets any parts in the package named Security.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named SECURITY + if ($part.Uri -notmatch '/SECURITY\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. \ No newline at end of file From 607feb327f00f58edeaa44383d6b0d7e8da4f850 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 20:02:34 +0000 Subject: [PATCH 345/724] feat: `OpenPackage.get_Security.md` ( Fixes #135 ) --- OP.types.ps1xml | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9e47c07..2cca1cb 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3054,6 +3054,33 @@ param([string]$Revision) $this.PackageProperties.Revision = $Revision + + Security.md + + <# +.SYNOPSIS + Gets a package's security notice +.DESCRIPTION + Gets any parts in the package named Security.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named SECURITY + if ($part.Uri -notmatch '/SECURITY\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. + + ServiceWorker.js @@ -6354,6 +6381,33 @@ param([string]$Revision) $this.PackageProperties.Revision = $Revision + + Security.md + + <# +.SYNOPSIS + Gets a package's security notice +.DESCRIPTION + Gets any parts in the package named Security.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named SECURITY + if ($part.Uri -notmatch '/SECURITY\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. + + ServiceWorker.js @@ -9654,6 +9708,33 @@ param([string]$Revision) $this.PackageProperties.Revision = $Revision + + Security.md + + <# +.SYNOPSIS + Gets a package's security notice +.DESCRIPTION + Gets any parts in the package named Security.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named SECURITY + if ($part.Uri -notmatch '/SECURITY\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. + + ServiceWorker.js From e51049cb6103150f0f5ddbf6888bfdac2ff59b63 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 13:04:11 -0700 Subject: [PATCH 346/724] feat: `OpenPackage.get_Contributing.md` ( Fixes #134 ) --- Types/OpenPackage/get_Contributing.md.ps1 | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_Contributing.md.ps1 diff --git a/Types/OpenPackage/get_Contributing.md.ps1 b/Types/OpenPackage/get_Contributing.md.ps1 new file mode 100644 index 0000000..100bd1d --- /dev/null +++ b/Types/OpenPackage/get_Contributing.md.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Gets a package's contribution guide +.DESCRIPTION + Gets any parts in the package named Contributing.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named Contributing + if ($part.Uri -notmatch '/Contributing\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. \ No newline at end of file From a7fe70b9a69ef176806256a53cf6a0080b78fae6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 20:04:47 +0000 Subject: [PATCH 347/724] feat: `OpenPackage.get_Contributing.md` ( Fixes #134 ) --- OP.types.ps1xml | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 2cca1cb..4906be9 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2223,6 +2223,33 @@ param([string]$ContentType) $this.PackageProperties.ContentType = $ContentType + + Contributing.md + + <# +.SYNOPSIS + Gets a package's contribution guide +.DESCRIPTION + Gets any parts in the package named Contributing.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named Contributing + if ($part.Uri -notmatch '/Contributing\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. + + Count @@ -5550,6 +5577,33 @@ param([string]$ContentType) $this.PackageProperties.ContentType = $ContentType + + Contributing.md + + <# +.SYNOPSIS + Gets a package's contribution guide +.DESCRIPTION + Gets any parts in the package named Contributing.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named Contributing + if ($part.Uri -notmatch '/Contributing\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. + + Count @@ -8877,6 +8931,33 @@ param([string]$ContentType) $this.PackageProperties.ContentType = $ContentType + + Contributing.md + + <# +.SYNOPSIS + Gets a package's contribution guide +.DESCRIPTION + Gets any parts in the package named Contributing.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named Contributing + if ($part.Uri -notmatch '/Contributing\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. + + Count From 1e102b8368843a1f8e64fd24fa0339170626841f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 13:05:02 -0700 Subject: [PATCH 348/724] feat: `OpenPackage.get_CodeOfConduct.md` ( Fixes #133 ) --- Types/OpenPackage/get_CodeOfConduct.md.ps1 | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Types/OpenPackage/get_CodeOfConduct.md.ps1 diff --git a/Types/OpenPackage/get_CodeOfConduct.md.ps1 b/Types/OpenPackage/get_CodeOfConduct.md.ps1 new file mode 100644 index 0000000..8c8ba31 --- /dev/null +++ b/Types/OpenPackage/get_CodeOfConduct.md.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Gets a package's code of conduct +.DESCRIPTION + Gets any parts in the package named Code_Of_Conduct.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CODE_OF_CONDUCT + if ($part.Uri -notmatch '/CODE_OF_CONDUCT\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. \ No newline at end of file From e23b986949f8f417a6e8e5eb303f94091cdcc024 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 20:05:33 +0000 Subject: [PATCH 349/724] feat: `OpenPackage.get_CodeOfConduct.md` ( Fixes #133 ) --- OP.types.ps1xml | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 4906be9..d199748 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2111,6 +2111,33 @@ param() $this.GetContent("/tools/chocolateyInstall.ps1") + + CodeOfConduct.md + + <# +.SYNOPSIS + Gets a package's code of conduct +.DESCRIPTION + Gets any parts in the package named Code_Of_Conduct.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CODE_OF_CONDUCT + if ($part.Uri -notmatch '/CODE_OF_CONDUCT\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. + + Config.json @@ -5465,6 +5492,33 @@ param() $this.GetContent("/tools/chocolateyInstall.ps1") + + CodeOfConduct.md + + <# +.SYNOPSIS + Gets a package's code of conduct +.DESCRIPTION + Gets any parts in the package named Code_Of_Conduct.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CODE_OF_CONDUCT + if ($part.Uri -notmatch '/CODE_OF_CONDUCT\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. + + Config.json @@ -8819,6 +8873,33 @@ param() $this.GetContent("/tools/chocolateyInstall.ps1") + + CodeOfConduct.md + + <# +.SYNOPSIS + Gets a package's code of conduct +.DESCRIPTION + Gets any parts in the package named Code_Of_Conduct.md +#> +[OutputType("text/markdown")] +param() + +# Get every part +foreach ($part in $this.GetParts()) { + # and ignore any part not named CODE_OF_CONDUCT + if ($part.Uri -notmatch '/CODE_OF_CONDUCT\.md$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part + } +} + +# We are done. + + Config.json From 6854f99aff3b1a98078e878cc1b65d41ed4e2e46 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 19:45:57 -0700 Subject: [PATCH 350/724] feat: `OpenPackage.Part.get_Writer` ( Fixes #136 ) --- Types/OpenPackage.Part/get_Writer.ps1 | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Types/OpenPackage.Part/get_Writer.ps1 diff --git a/Types/OpenPackage.Part/get_Writer.ps1 b/Types/OpenPackage.Part/get_Writer.ps1 new file mode 100644 index 0000000..7b391c9 --- /dev/null +++ b/Types/OpenPackage.Part/get_Writer.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + Gets a Part's available writers +.DESCRIPTION + Gets the different methods we can use to Write to a part. +#> +if ($this.'#Writers') { + return $this.'#Writers' +} +$WriteMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { + if ($method.Name -notmatch 'Write.+?') { + continue + } + if (-not $method.Script) { continue } + $script = $method.Script + if (-not $script.Attributes) { continue } + $attributeData = [Ordered]@{} + foreach ($attribute in $script.Attributes) { + if ($attribute.Key -and $attribute.value) { + if (-not $attributeData[$attribute.key]) { + $attributeData[$attribute.key] = $attribute.value + } else { + $attributeData[$attribute.key] = @($attributeData[$attribute.key]) + $attribute.Value + } + } + } + + try { + if ( + ( + $attributeData.FilePattern -and ( + $this.Uri -match $attributeData.FilePattern + ) + ) -or ( + $attributeData.ContentTypePattern -and ( + $this.ContentType -match $attributeData.ContentTypePattern + ) + ) + ) { + $method + } else { + continue nextMethod + } + } catch { + + } +}) + +$WriteMethods = @($WriteMethods | Sort-Object { + $in = $_ + foreach ($attr in $in.Script.Attributes) { + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + return ($attr.Value -as [int]) + } + } + return 0 +}) + +$this | Add-Member NoteProperty '#Writers' $WriteMethods -Force +return $this.'#Writers' \ No newline at end of file From e027dfc5272b6bb83108588ce70c65cd1d6e53f0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 02:46:13 +0000 Subject: [PATCH 351/724] feat: `OpenPackage.Part.get_Writer` ( Fixes #136 ) --- OP.types.ps1xml | 195 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d199748..f5c04eb 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11088,6 +11088,71 @@ $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' + + Writer + + <# +.SYNOPSIS + Gets a Part's available writers +.DESCRIPTION + Gets the different methods we can use to Write to a part. +#> +if ($this.'#Writers') { + return $this.'#Writers' +} +$WriteMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { + if ($method.Name -notmatch 'Write.+?') { + continue + } + if (-not $method.Script) { continue } + $script = $method.Script + if (-not $script.Attributes) { continue } + $attributeData = [Ordered]@{} + foreach ($attribute in $script.Attributes) { + if ($attribute.Key -and $attribute.value) { + if (-not $attributeData[$attribute.key]) { + $attributeData[$attribute.key] = $attribute.value + } else { + $attributeData[$attribute.key] = @($attributeData[$attribute.key]) + $attribute.Value + } + } + } + + try { + if ( + ( + $attributeData.FilePattern -and ( + $this.Uri -match $attributeData.FilePattern + ) + ) -or ( + $attributeData.ContentTypePattern -and ( + $this.ContentType -match $attributeData.ContentTypePattern + ) + ) + ) { + $method + } else { + continue nextMethod + } + } catch { + + } +}) + +$WriteMethods = @($WriteMethods | Sort-Object { + $in = $_ + foreach ($attr in $in.Script.Attributes) { + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + return ($attr.Value -as [int]) + } + } + return 0 +}) + +$this | Add-Member NoteProperty '#Writers' $WriteMethods -Force +return $this.'#Writers' + + DefaultDisplay Uri @@ -11887,6 +11952,71 @@ $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' + + Writer + + <# +.SYNOPSIS + Gets a Part's available writers +.DESCRIPTION + Gets the different methods we can use to Write to a part. +#> +if ($this.'#Writers') { + return $this.'#Writers' +} +$WriteMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { + if ($method.Name -notmatch 'Write.+?') { + continue + } + if (-not $method.Script) { continue } + $script = $method.Script + if (-not $script.Attributes) { continue } + $attributeData = [Ordered]@{} + foreach ($attribute in $script.Attributes) { + if ($attribute.Key -and $attribute.value) { + if (-not $attributeData[$attribute.key]) { + $attributeData[$attribute.key] = $attribute.value + } else { + $attributeData[$attribute.key] = @($attributeData[$attribute.key]) + $attribute.Value + } + } + } + + try { + if ( + ( + $attributeData.FilePattern -and ( + $this.Uri -match $attributeData.FilePattern + ) + ) -or ( + $attributeData.ContentTypePattern -and ( + $this.ContentType -match $attributeData.ContentTypePattern + ) + ) + ) { + $method + } else { + continue nextMethod + } + } catch { + + } +}) + +$WriteMethods = @($WriteMethods | Sort-Object { + $in = $_ + foreach ($attr in $in.Script.Attributes) { + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + return ($attr.Value -as [int]) + } + } + return 0 +}) + +$this | Add-Member NoteProperty '#Writers' $WriteMethods -Force +return $this.'#Writers' + + DefaultDisplay Uri @@ -12686,6 +12816,71 @@ $this | Add-Member NoteProperty '#Readers' $readMethods -Force return $this.'#Readers' + + Writer + + <# +.SYNOPSIS + Gets a Part's available writers +.DESCRIPTION + Gets the different methods we can use to Write to a part. +#> +if ($this.'#Writers') { + return $this.'#Writers' +} +$WriteMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { + if ($method.Name -notmatch 'Write.+?') { + continue + } + if (-not $method.Script) { continue } + $script = $method.Script + if (-not $script.Attributes) { continue } + $attributeData = [Ordered]@{} + foreach ($attribute in $script.Attributes) { + if ($attribute.Key -and $attribute.value) { + if (-not $attributeData[$attribute.key]) { + $attributeData[$attribute.key] = $attribute.value + } else { + $attributeData[$attribute.key] = @($attributeData[$attribute.key]) + $attribute.Value + } + } + } + + try { + if ( + ( + $attributeData.FilePattern -and ( + $this.Uri -match $attributeData.FilePattern + ) + ) -or ( + $attributeData.ContentTypePattern -and ( + $this.ContentType -match $attributeData.ContentTypePattern + ) + ) + ) { + $method + } else { + continue nextMethod + } + } catch { + + } +}) + +$WriteMethods = @($WriteMethods | Sort-Object { + $in = $_ + foreach ($attr in $in.Script.Attributes) { + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + return ($attr.Value -as [int]) + } + } + return 0 +}) + +$this | Add-Member NoteProperty '#Writers' $WriteMethods -Force +return $this.'#Writers' + + DefaultDisplay Uri From f009982d574c2b25c6b6ea3722ef036c0dae557a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 22:35:02 -0700 Subject: [PATCH 352/724] feat: `OpenPackage.Part.WriteText` ( Fixes #137 ) --- Types/OpenPackage.Part/WriteText.ps1 | 85 ++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 Types/OpenPackage.Part/WriteText.ps1 diff --git a/Types/OpenPackage.Part/WriteText.ps1 b/Types/OpenPackage.Part/WriteText.ps1 new file mode 100644 index 0000000..5962253 --- /dev/null +++ b/Types/OpenPackage.Part/WriteText.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + Writes Part Content as Text +.DESCRIPTION + Writes Package Part Content as Text +.NOTES + +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to .txt files, modelfiles, and dockerfiles. + 'FilePattern', + '(?>[/\.]Dockerfile|[/\.]Modelfile|\.txt|\.svg|\.xml)$' +)] +[Reflection.AssemblyMetadata( + # This should automatically apply to any text/ content types + 'ContentTypePattern', + '^text/' +)] +[Reflection.AssemblyMetadata( + # This should automatically apply to any xml content types + 'ContentTypePattern', + '[/\+]xml' +)] +[Reflection.AssemblyMetadata( + # This should automatically apply to any json content types + 'ContentTypePattern', + '[/\+]json' +)] +[Reflection.AssemblyMetadata( + # This has a higher order, indicating it should be run later than most. + 'Order', + 100 +)] +param( +[Alias('Input','Content','Text')] +[PSObject] +$InputObject, + +[Collections.IDictionary] +$Option = [Ordered]@{} +) + +# If there is no part, return +$part = $this +if (-not $part) { return } + +if (-not $option.Encoding) { + $option.Encoding = [Text.Encoding]::UTF8 +} + + +if ( + # If we provide an optional stream + $option.Stream -is [IO.Stream] -and + # and it is writeable + $option.Stream.CanWrite +) { + # Then we will write to the stream. + + # Get the bytes we will write + $bytes = $Option.Encoding.GetBytes("$InputObject") + # and write them to the stream. + $option.Stream.Write($bytes,0, $bytes.Length) + # * The caller gave us the stream + # * The caller should dispose of it sometime. + # We can just return. + return +} + +# If no `-Option @{Stream}` was passed +# Then we are writing to this part (or trying to). +$partStream = $part.GetStream('Open','ReadWrite') +# If we can not write, return now. +if (-not $partStream) { return } + +# Otherwise, zero out the length +$partStream.SetLength(0) + +# Get the bytes we will write +$bytes = $Option.Encoding.GetBytes("$InputObject") + +# Write the bytes to the stream, +$partStream.Write($bytes, 0, $bytes.Length) +$partStream.Close() # close, +$partStream.Dispose() # and dispose. \ No newline at end of file From 17868b01c8d1883153e6d592272832fcd611fb70 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 05:35:20 +0000 Subject: [PATCH 353/724] feat: `OpenPackage.Part.WriteText` ( Fixes #137 ) --- OP.types.ps1xml | 270 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f5c04eb..9d54006 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11017,6 +11017,96 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp } + + WriteText + + Hash @@ -11881,6 +11971,96 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp } + + WriteText + + Hash @@ -12745,6 +12925,96 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp } + + WriteText + + Hash From 6f424c795a51dc6660d6f2dd76270dde7cd870d2 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 22:40:44 -0700 Subject: [PATCH 354/724] feat: `OpenPackage.Part.WriteText` ( Fixes #137 ) Documenting options --- Types/OpenPackage.Part/WriteText.ps1 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Types/OpenPackage.Part/WriteText.ps1 b/Types/OpenPackage.Part/WriteText.ps1 index 5962253..49989de 100644 --- a/Types/OpenPackage.Part/WriteText.ps1 +++ b/Types/OpenPackage.Part/WriteText.ps1 @@ -32,10 +32,23 @@ 100 )] param( +# The object to write. [Alias('Input','Content','Text')] [PSObject] $InputObject, +<# + +Any options used to write the object + +Supported Options: + +|Option|Description| +|-|-| +|Encoding|The text encoding| +|Stream|Optional destination stream| +#> + [Collections.IDictionary] $Option = [Ordered]@{} ) From 4feb4135b62eb7473406c82c38c55af896b1ad41 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 22:55:43 -0700 Subject: [PATCH 355/724] feat: `OpenPackage.Part.WriteJson` ( Fixes #138 ) --- Types/OpenPackage.Part/WriteJson.ps1 | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 Types/OpenPackage.Part/WriteJson.ps1 diff --git a/Types/OpenPackage.Part/WriteJson.ps1 b/Types/OpenPackage.Part/WriteJson.ps1 new file mode 100644 index 0000000..070b56c --- /dev/null +++ b/Types/OpenPackage.Part/WriteJson.ps1 @@ -0,0 +1,55 @@ +<# +.SYNOPSIS + Writes Part Content as Json +.DESCRIPTION + Writes Open Package Part Content as Json +#> +[Reflection.AssemblyMetadata('FilePattern', '\.jsonc?$')] +[Reflection.AssemblyMetadata('ContentTypePattern', '[/\+]jsonc?$')] +param( +# The object to write. +[Alias('Input','Content','Text')] +[PSObject] +$InputObject, + +<# + +Any options used to write the object + +Supported Options: + +|Option|Description| +|-|-| +|Depth|The serialization depth| +|Encoding|The text encoding| +|Stream|Optional destination stream| + +#> +[Collections.IDictionary] +$Option = [Ordered]@{} +) + +# If this object does not have a write text method, return. +if (-not $this.WriteText) { throw 'No `.WriteText()`'; return } + +# If no depth was set, +if (-not $option.Depth) { + # use double the format enumeration limit (by default, 8) + $option.Depth = $FormatEnumerationLimit * 2 +} + +# If we have a .Package and .PartUri property +if ($content.Package -is [IO.Packaging.Package] -and + $content.PartUri -is [uri]) { + # avoid putting them in the object + $text = ConvertTo-Json -InputObject ( + $content | + Select-Object -Property * -ExcludeProperty 'Package', 'PartUri' + ) -Depth $Option.Depth +} else { + # Convert any other objects to json. + $text = ConvertTo-Json -InputObject $content -Depth $Option.Depth +} + +# Then, write the text +$this.WriteText($text, $Option) \ No newline at end of file From 6d442b6d0004ebc730ac55800554d9023cfdc18a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 05:56:04 +0000 Subject: [PATCH 356/724] feat: `OpenPackage.Part.WriteJson` ( Fixes #138 ) --- OP.types.ps1xml | 219 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9d54006..8c2d16d 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11017,6 +11017,66 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp } + + WriteJson + + WriteText + + WriteJson + + WriteText + + WriteJson + + WriteText - ReadXML + ReadXml - ReadXML + ReadXml - ReadXML + ReadXml + + WriteXml + + Hash @@ -12207,6 +12265,64 @@ $partStream.Close() # close, $partStream.Dispose() # and dispose. + + WriteXml + + Hash @@ -13234,6 +13350,64 @@ $partStream.Close() # close, $partStream.Dispose() # and dispose. + + WriteXml + + Hash From c0795825b7ef977a9856c6c90ace1b36f44fb93f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 23:18:05 -0700 Subject: [PATCH 363/724] feat: `OpenPackage.Part.WriteYaml` ( Fixes #140 ) --- Types/OpenPackage.Part/WriteYaml.ps1 | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Types/OpenPackage.Part/WriteYaml.ps1 diff --git a/Types/OpenPackage.Part/WriteYaml.ps1 b/Types/OpenPackage.Part/WriteYaml.ps1 new file mode 100644 index 0000000..5261dd3 --- /dev/null +++ b/Types/OpenPackage.Part/WriteYaml.ps1 @@ -0,0 +1,69 @@ +<# +.SYNOPSIS + Writes Part Content as Yaml +.DESCRIPTION + Writes Open Package Part Content as Yaml +.LINK + https://github.com/jborean93/YaYaml +.LINK + https://www.powershellgallery.com/packages/YaYaml/ +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to .yaml files + 'FilePattern', + '\.ya?ml$' +)] +param( +# The object to write. +[Alias('Input','Content','Text')] +[PSObject] +$InputObject, + +<# + +Any options used to write the object + +Supported Options: + +|Option|Description| +|-|-| +|Depth|The serialization depth| +|Encoding|The text encoding| +|Stream|Optional destination stream| + +#> +[Collections.IDictionary] +$Option = [Ordered]@{} +) + +# If this object does not have a write text method, return. +if (-not $this.WriteText) { throw 'No `.WriteText()`'; return } + +# If no depth was set, +if (-not $option.Depth) { + # use double the format enumeration limit (by default, 8) + $option.Depth = $FormatEnumerationLimit * 2 +} + +$ConvertToYaml = $executionContext.SessionState.InvokeCommand.GetCommand('ConvertTo-Yaml', 'Cmdlet,Function') + +if (-not $ConvertToYaml) { + Write-Error "ConvertTo-Yaml not found, please install YaYaml" + return +} + +# If we have a .Package and .PartUri property +if ($inputObject.Package -is [IO.Packaging.Package] -and + $inputObject.PartUri -is [uri]) { + # avoid putting them in the object + $text = & $ConvertToYaml -InputObject ( + $inputObject | + Select-Object -Property * -ExcludeProperty 'Package', 'PartUri' + ) -Depth $Option.Depth +} else { + # Convert any other objects to json. + $text = & $ConvertToYaml -InputObject $inputObject -Depth $Option.Depth +} + +if (-not $text) { return } +$this.WriteText($text, $Option) \ No newline at end of file From 9e03119644018dd55c4493f523b3fa7bfeb318b3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 06:18:35 +0000 Subject: [PATCH 364/724] feat: `OpenPackage.Part.WriteYaml` ( Fixes #140 ) --- OP.types.ps1xml | 222 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index eeedd5f..ff9ff96 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11238,6 +11238,80 @@ if ($contentAsXml) { } + + WriteYaml + + Hash @@ -12323,6 +12397,80 @@ if ($contentAsXml) { } + + WriteYaml + + Hash @@ -13408,6 +13556,80 @@ if ($contentAsXml) { } + + WriteYaml + + Hash From b8c13e029233e54499d2ec74b34f754218ad13a2 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 23:19:12 -0700 Subject: [PATCH 365/724] feat: `OpenPackage.Part.WriteToml` ( Fixes #141 ) --- Types/OpenPackage.Part/WriteToml.ps1 | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 Types/OpenPackage.Part/WriteToml.ps1 diff --git a/Types/OpenPackage.Part/WriteToml.ps1 b/Types/OpenPackage.Part/WriteToml.ps1 new file mode 100644 index 0000000..10211b0 --- /dev/null +++ b/Types/OpenPackage.Part/WriteToml.ps1 @@ -0,0 +1,71 @@ +<# +.SYNOPSIS + Writes Part Content as Toml +.DESCRIPTION + Writes Open Package Part Content as Tom's Obvious Minimal Language +.LINK + https://toml.io/ +.LINK + https://github.com/jborean93/PSToml +.LINK + https://www.powershellgallery.com/packages/PSToml/ +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to .yaml files + 'FilePattern', + '\.toml$' +)] +param( +# The object to write. +[Alias('Input','Content','Text')] +[PSObject] +$InputObject, + +<# + +Any options used to write the object + +Supported Options: + +|Option|Description| +|-|-| +|Depth|The serialization depth| +|Encoding|The text encoding| +|Stream|Optional destination stream| + +#> +[Collections.IDictionary] +$Option = [Ordered]@{} +) + +# If this object does not have a write text method, return. +if (-not $this.WriteText) { throw 'No `.WriteText()`'; return } + +# If no depth was set, +if (-not $option.Depth) { + # use double the format enumeration limit (by default, 8) + $option.Depth = $FormatEnumerationLimit * 2 +} + +$ConvertToToml = $executionContext.SessionState.InvokeCommand.GetCommand('ConvertTo-Toml', 'Cmdlet,Function') + +if (-not $ConvertToToml) { + Write-Error "ConvertTo-Toml not found, please install PSToml" + return +} + +# If we have a .Package and .PartUri property +if ($inputObject.Package -is [IO.Packaging.Package] -and + $inputObject.PartUri -is [uri]) { + # avoid putting them in the object + $text = & $ConvertToToml -InputObject ( + $inputObject | + Select-Object -Property * -ExcludeProperty 'Package', 'PartUri' + ) -Depth $Option.Depth +} else { + # Convert any other objects to json. + $text = & $ConvertToToml -InputObject $inputObject -Depth $Option.Depth +} + +if (-not $text) { return } +$this.WriteText($text, $Option) \ No newline at end of file From 2fcb84926dd92d78db0bbd40f727f708a726f341 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 06:19:43 +0000 Subject: [PATCH 366/724] feat: `OpenPackage.Part.WriteToml` ( Fixes #141 ) --- OP.types.ps1xml | 228 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index ff9ff96..887bb4e 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11180,6 +11180,82 @@ $partStream.Close() # close, $partStream.Dispose() # and dispose. + + WriteToml + + WriteXml + + WriteToml + + WriteXml + + WriteToml + + WriteXml + + + WriteClixml + @@ -12250,6 +12294,50 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" } } + + + + WriteClixml + @@ -13485,6 +13573,50 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" } } + + + + WriteClixml + From f06690d52f3d963720ab9cc501366d254e42ff8b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 18 Mar 2026 23:27:10 -0700 Subject: [PATCH 369/724] feat: `OpenPackage.Part.Write` ( Fixes #143 ) --- Types/OpenPackage.Part/Write.ps1 | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Types/OpenPackage.Part/Write.ps1 diff --git a/Types/OpenPackage.Part/Write.ps1 b/Types/OpenPackage.Part/Write.ps1 new file mode 100644 index 0000000..0dcd145 --- /dev/null +++ b/Types/OpenPackage.Part/Write.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Writes Open Package Parts +.DESCRIPTION + Writes Open Package Parts, using the `.Writer` associated with this part. +#> +param( +# The object to write. +[Alias('Input','Content','Text')] +[PSObject] +$InputObject, + +<# + +Any options used to write the object + +Commonly Supported Options: + +|Option|Description| +|-|-| +|Depth|The serialization depth| +|Encoding|The text encoding| +|Stream|Optional destination stream| + +#> +[Alias('Options')] +[Collections.IDictionary] +$Option = [Ordered]@{} +) + +$orderedMethods = @($this.Writer) + +if (-not $orderedMethods) { + Write-Warning "No writer found for $($this.Uri)" + return +} + +$method = $orderedMethods[0] +return $method.Invoke($content, $option) \ No newline at end of file From 6cae8c0253dfa114e9bf81b631708493d61aef15 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 06:27:29 +0000 Subject: [PATCH 370/724] feat: `OpenPackage.Part.Write` ( Fixes #143 ) --- OP.types.ps1xml | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9ef87a5..4d8a685 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11017,6 +11017,50 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp } + + Write + + WriteClixml + + Write + + WriteClixml + + Write + + WriteClixml @@ -12381,7 +12384,10 @@ if (-not $orderedMethods) { } $method = $orderedMethods[0] -return $method.Invoke($content, $option) +if ($method) { + return $method.Invoke($InputObject, $option) +} + @@ -13704,7 +13710,10 @@ if (-not $orderedMethods) { } $method = $orderedMethods[0] -return $method.Invoke($content, $option) +if ($method) { + return $method.Invoke($InputObject, $option) +} + From 1c272723c51984fdeac216bc45ac77d21fd068bf Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 14:01:06 -0700 Subject: [PATCH 373/724] feat: `OpenPackage.Part.WritePowerShell` ( Fixes #144 ) --- Types/OpenPackage.Part/WritePowerShell.ps1 | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 Types/OpenPackage.Part/WritePowerShell.ps1 diff --git a/Types/OpenPackage.Part/WritePowerShell.ps1 b/Types/OpenPackage.Part/WritePowerShell.ps1 new file mode 100644 index 0000000..a9247a6 --- /dev/null +++ b/Types/OpenPackage.Part/WritePowerShell.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS + Writes Part Content as PowerShell +.DESCRIPTION + Reads Open Package Part Content as PowerShell +.NOTES + This may attempt to convert the input into a ScriptBlock. + + It will not write content if the conversion fails. + + If a function is provided as input, will write a script block that declares that function. +#> +[Reflection.AssemblyMetadata( + # This should automatically apply to `.ps1` and `.psm1` files + 'FilePattern', + '\.psm?1$' +)] +param( +# The object to write. +[Alias('Input','Content','Text')] +[PSObject] +$InputObject, + +<# + +Any options used to write the object + +Commonly Supported Options: + +|Option|Description| +|-|-| +|Depth|The serialization depth| +|Encoding|The text encoding| +|Stream|Optional destination stream| + +#> +[Alias('Options')] +[Collections.IDictionary] +$Option = [Ordered]@{} +) + +# If this object does not have a write text method, return. +if (-not $this.WriteText) { throw 'No `.WriteText()`'; return } + +# If the input is a scriptblock +if ($inputObject -is [ScriptBlock]) { + # write it and return. + $this.WriteText("$InputObject", $Option) + return +} + +# If the input was an external script +if ($inputObject -is [Management.Automation.ExternalScriptInfo]) { + # write its script block and return + $this.WriteText("$($InputObject.ScriptBlock)", $Option) + return +} + +# If the input is a function, recreate the function +if ($inputObject -is [Management.Automation.FunctionInfo]) { + # First check for exotically named commands. + if ($inputObject.Name -match '[\s\{\}\(\)]') { + # We can use the function: drive to set them (but only functions). + if ($inputObject.CommandType -eq 'function') { + $this.WriteText(@( + "`$executionContext.SessionState.PSVariable.Set(" + " 'function:$($inputObject.Name -replace "'", "''")',{" + $InputObject.ScriptBlock + "})" + ) -join [Environment]::NewLine) + } else { + Write-Error "Can not recreated $($InputObject.Name) as a filter" + } + } else { + # If the command was not exotically named, + # we can just embed it directly + $this.WriteText(@( + "$($inputObject.CommandType) $($inputObject.Name) {" + $InputObject.ScriptBlock + "}" + ) -join [Environment]::NewLine, $Option) + } + + return +} + +# If the input object was not a ScriptBlock, Function, or ExternalScript +# try to cast the input into a script. +# If this fails, it will output an error +$scriptBlock = [ScriptBlock]::Create("$InputObject") + +# If it succeeded, write the text +if ($scriptBlock -is [scriptblock]) { + $this.WriteText("$scriptBlock", $Option) +} +# Either way, we're done. +return \ No newline at end of file From 0ce204f7a06445ba079f03231a3ded270e7ab897 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 21:01:26 +0000 Subject: [PATCH 374/724] feat: `OpenPackage.Part.WritePowerShell` ( Fixes #144 ) --- OP.types.ps1xml | 306 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index af90acc..a2cdf04 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11168,6 +11168,108 @@ if ($InputObject.Package -is [IO.Packaging.Package] -and $this.WriteText($text, $Option) + + WritePowerShell + + WriteText + + WritePowerShell + + WriteText + + WritePowerShell + + WriteText @@ -3829,6 +3832,9 @@ $progress.Status = "Getting records" } } while ($results -and $results.cursor) +$progress.Remove('PercentComplete') +$progress.Completed = $true +Write-Progress @progress @@ -7275,6 +7281,9 @@ $progress.Status = "Getting records" } } while ($results -and $results.cursor) +$progress.Remove('PercentComplete') +$progress.Completed = $true +Write-Progress @progress From 9b3058752f5d9cecc8875325d7040ce766695a2e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 18:18:18 -0700 Subject: [PATCH 393/724] feat: `OpenPackage.GetAtBlob/Proto/Record/Type` ( Fixes #112, Fixes #113, Fixes #114, Fixes #125 ) Docs and pds fault tolerance --- Commands/Get-OpenPackage.ps1 | 7 ++++++- Types/OpenPackage/GetAtBlob.ps1 | 9 ++++++++- Types/OpenPackage/GetAtProto.ps1 | 10 +++++++--- Types/OpenPackage/GetAtRecord.ps1 | 18 ++++++++++++++++-- Types/OpenPackage/GetAtType.ps1 | 21 +++++++++++++++++++-- Types/OpenPackage/GetUrl.ps1 | 4 ++++ 6 files changed, 60 insertions(+), 9 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index dc3b5ec..2eca464 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -135,7 +135,12 @@ function Get-OpenPackage # This can be a single post or a collection of all posts of a type. [Parameter(Mandatory,ParameterSetName='AtUri',ValueFromPipelineByPropertyName)] [string[]] - $AtUri, + $AtUri, + + # The personal data server. This is used in At Protocol requests. + [Parameter(ValueFromPipelineByPropertyName)] + [string] + $PDS, # Adds a dictionary of content to the package [Parameter(Mandatory,ParameterSetName='Dictionary',ValueFromPipelineByPropertyName)] diff --git a/Types/OpenPackage/GetAtBlob.ps1 b/Types/OpenPackage/GetAtBlob.ps1 index 46810ec..65afa1c 100644 --- a/Types/OpenPackage/GetAtBlob.ps1 +++ b/Types/OpenPackage/GetAtBlob.ps1 @@ -21,6 +21,13 @@ $pds = "https://bsky.social/" ) -return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" +return Invoke-WebRequest -Uri "$(# Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' +})/xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" diff --git a/Types/OpenPackage/GetAtProto.ps1 b/Types/OpenPackage/GetAtProto.ps1 index 901ed15..0e4f299 100644 --- a/Types/OpenPackage/GetAtProto.ps1 +++ b/Types/OpenPackage/GetAtProto.ps1 @@ -40,20 +40,24 @@ $TypeMap = $( [Alias('CompressionLevel')] $CompressionOption = 'Superfast', -[uri] +# The personal data server. This is used in At Protocol requests. +[string] $pds = "https://bsky.social", # The current package [IO.Packaging.Package] $Package, +# The batch size [ValidateRange(1,100)] [int] $BatchSize = 100, +# The number of records to get [long] $First, +# If number of records to skip [long] $Skip ) @@ -127,14 +131,14 @@ foreach ($at in $AtUri) { # If the uri is not an at uri if ($atRecord.uri -notmatch $atPattern) { # continue to the next record - continue + continue } packAtProtoRecord } } else { - try { + try { $atBlob = $this.GetAtBlob($matches.did, $matches.type) $atContentType = $atBlob.Headers.'Content-Type' if (-not $atContentType) { diff --git a/Types/OpenPackage/GetAtRecord.ps1 b/Types/OpenPackage/GetAtRecord.ps1 index ddba336..412c655 100644 --- a/Types/OpenPackage/GetAtRecord.ps1 +++ b/Types/OpenPackage/GetAtRecord.ps1 @@ -7,24 +7,38 @@ Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile #> param( +# Who [did](https://atproto.com/specs/did) (decentralized identifier) [Parameter(Mandatory)] +[Alias('DectralizedIdentifier')] [string] $did, +# What collection or type [Parameter(Mandatory)] [string] $collection, +# What is the record key? [Parameter(Mandatory)] [string] $rkey, -[uri] +# Where is the data? +[string] $pds = "https://bsky.social" ) # Construct the XRPC url to that record -$xrpcUrl = "$pds/xrpc/com.atproto.repo.getRecord?repo=$( +$xrpcUrl = "$( + # Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' + } +)/xrpc/com.atproto.repo.getRecord?repo=$( $did )&collection=$( $collection diff --git a/Types/OpenPackage/GetAtType.ps1 b/Types/OpenPackage/GetAtType.ps1 index 84b1ea0..5c50923 100644 --- a/Types/OpenPackage/GetAtType.ps1 +++ b/Types/OpenPackage/GetAtType.ps1 @@ -7,24 +7,32 @@ Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile #> param( +# Who [did](https://atproto.com/specs/did) (decentralized identifier) [Parameter(Mandatory)] +[Alias('DectralizedIdentifier')] [string] $did, +# What collection or type [Parameter(Mandatory)] [string] $collection, +# How many items to get in each batch. +# By default, 100. [ValidateRange(1,100)] [int] $BatchSize = 100, +# If provided, will only get N items. [long] $First, +# If provided, will skip N items. [long] $Skip, +# The PDS (Personal Data Server). [string] $pds = "https://bsky.social/" ) @@ -36,8 +44,17 @@ $skipped = [long]0 $cursor = '' $progress = [Ordered]@{Id = Get-Random} $progress.Status = "Getting records" -:AtSync do { - $xrpcUrl = "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=$( +:AtSync do { + $xrpcUrl = "$( + # Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' + } + )/xrpc/com.atproto.repo.listRecords?repo=$( $did )&collection=$( $collection diff --git a/Types/OpenPackage/GetUrl.ps1 b/Types/OpenPackage/GetUrl.ps1 index 35d381f..ccf1ea8 100644 --- a/Types/OpenPackage/GetUrl.ps1 +++ b/Types/OpenPackage/GetUrl.ps1 @@ -58,6 +58,10 @@ $IncludeGit, [switch] $IncludeNodeModule, +# The personal data server. This is used in At Protocol requests. +[string] +$PDS, + # The current package [IO.Packaging.Package] $Package From 6670fba44fe2967017c30591ae3626f1cf3f5ec4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 20 Mar 2026 01:18:41 +0000 Subject: [PATCH 394/724] feat: `OpenPackage.GetAtBlob/Proto/Record/Type` ( Fixes #112, Fixes #113, Fixes #114, Fixes #125 ) Docs and pds fault tolerance --- OP.types.ps1xml | 186 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 162 insertions(+), 24 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d4deca5..c0e2b07 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -73,7 +73,14 @@ $pds = "https://bsky.social/" ) -return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" +return Invoke-WebRequest -Uri "$(# Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' +})/xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" @@ -124,20 +131,24 @@ $TypeMap = $( [Alias('CompressionLevel')] $CompressionOption = 'Superfast', -[uri] +# The personal data server. This is used in At Protocol requests. +[string] $pds = "https://bsky.social", # The current package [IO.Packaging.Package] $Package, +# The batch size [ValidateRange(1,100)] [int] $BatchSize = 100, +# The number of records to get [long] $First, +# If number of records to skip [long] $Skip ) @@ -211,14 +222,14 @@ foreach ($at in $AtUri) { # If the uri is not an at uri if ($atRecord.uri -notmatch $atPattern) { # continue to the next record - continue + continue } packAtProtoRecord } } else { - try { + try { $atBlob = $this.GetAtBlob($matches.did, $matches.type) $atContentType = $atBlob.Headers.'Content-Type' if (-not $atContentType) { @@ -264,24 +275,38 @@ return $Package Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile #> param( +# Who [did](https://atproto.com/specs/did) (decentralized identifier) [Parameter(Mandatory)] +[Alias('DectralizedIdentifier')] [string] $did, +# What collection or type [Parameter(Mandatory)] [string] $collection, +# What is the record key? [Parameter(Mandatory)] [string] $rkey, -[uri] +# Where is the data? +[string] $pds = "https://bsky.social" ) # Construct the XRPC url to that record -$xrpcUrl = "$pds/xrpc/com.atproto.repo.getRecord?repo=$( +$xrpcUrl = "$( + # Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' + } +)/xrpc/com.atproto.repo.getRecord?repo=$( $did )&collection=$( $collection @@ -307,24 +332,32 @@ Invoke-RestMethod -Uri $xrpcUrl Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile #> param( +# Who [did](https://atproto.com/specs/did) (decentralized identifier) [Parameter(Mandatory)] +[Alias('DectralizedIdentifier')] [string] $did, +# What collection or type [Parameter(Mandatory)] [string] $collection, +# How many items to get in each batch. +# By default, 100. [ValidateRange(1,100)] [int] $BatchSize = 100, +# If provided, will only get N items. [long] $First, +# If provided, will skip N items. [long] $Skip, +# The PDS (Personal Data Server). [string] $pds = "https://bsky.social/" ) @@ -336,8 +369,17 @@ $skipped = [long]0 $cursor = '' $progress = [Ordered]@{Id = Get-Random} $progress.Status = "Getting records" -:AtSync do { - $xrpcUrl = "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=$( +:AtSync do { + $xrpcUrl = "$( + # Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' + } + )/xrpc/com.atproto.repo.listRecords?repo=$( $did )&collection=$( $collection @@ -1444,6 +1486,10 @@ $IncludeGit, [switch] $IncludeNodeModule, +# The personal data server. This is used in At Protocol requests. +[string] +$PDS, + # The current package [IO.Packaging.Package] $Package @@ -3522,7 +3568,14 @@ $pds = "https://bsky.social/" ) -return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" +return Invoke-WebRequest -Uri "$(# Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' +})/xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" @@ -3573,20 +3626,24 @@ $TypeMap = $( [Alias('CompressionLevel')] $CompressionOption = 'Superfast', -[uri] +# The personal data server. This is used in At Protocol requests. +[string] $pds = "https://bsky.social", # The current package [IO.Packaging.Package] $Package, +# The batch size [ValidateRange(1,100)] [int] $BatchSize = 100, +# The number of records to get [long] $First, +# If number of records to skip [long] $Skip ) @@ -3660,14 +3717,14 @@ foreach ($at in $AtUri) { # If the uri is not an at uri if ($atRecord.uri -notmatch $atPattern) { # continue to the next record - continue + continue } packAtProtoRecord } } else { - try { + try { $atBlob = $this.GetAtBlob($matches.did, $matches.type) $atContentType = $atBlob.Headers.'Content-Type' if (-not $atContentType) { @@ -3713,24 +3770,38 @@ return $Package Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile #> param( +# Who [did](https://atproto.com/specs/did) (decentralized identifier) [Parameter(Mandatory)] +[Alias('DectralizedIdentifier')] [string] $did, +# What collection or type [Parameter(Mandatory)] [string] $collection, +# What is the record key? [Parameter(Mandatory)] [string] $rkey, -[uri] +# Where is the data? +[string] $pds = "https://bsky.social" ) # Construct the XRPC url to that record -$xrpcUrl = "$pds/xrpc/com.atproto.repo.getRecord?repo=$( +$xrpcUrl = "$( + # Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' + } +)/xrpc/com.atproto.repo.getRecord?repo=$( $did )&collection=$( $collection @@ -3756,24 +3827,32 @@ Invoke-RestMethod -Uri $xrpcUrl Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile #> param( +# Who [did](https://atproto.com/specs/did) (decentralized identifier) [Parameter(Mandatory)] +[Alias('DectralizedIdentifier')] [string] $did, +# What collection or type [Parameter(Mandatory)] [string] $collection, +# How many items to get in each batch. +# By default, 100. [ValidateRange(1,100)] [int] $BatchSize = 100, +# If provided, will only get N items. [long] $First, +# If provided, will skip N items. [long] $Skip, +# The PDS (Personal Data Server). [string] $pds = "https://bsky.social/" ) @@ -3785,8 +3864,17 @@ $skipped = [long]0 $cursor = '' $progress = [Ordered]@{Id = Get-Random} $progress.Status = "Getting records" -:AtSync do { - $xrpcUrl = "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=$( +:AtSync do { + $xrpcUrl = "$( + # Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' + } + )/xrpc/com.atproto.repo.listRecords?repo=$( $did )&collection=$( $collection @@ -4893,6 +4981,10 @@ $IncludeGit, [switch] $IncludeNodeModule, +# The personal data server. This is used in At Protocol requests. +[string] +$PDS, + # The current package [IO.Packaging.Package] $Package @@ -6971,7 +7063,14 @@ $pds = "https://bsky.social/" ) -return Invoke-WebRequest -Uri "${pds}xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" +return Invoke-WebRequest -Uri "$(# Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' +})/xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" @@ -7022,20 +7121,24 @@ $TypeMap = $( [Alias('CompressionLevel')] $CompressionOption = 'Superfast', -[uri] +# The personal data server. This is used in At Protocol requests. +[string] $pds = "https://bsky.social", # The current package [IO.Packaging.Package] $Package, +# The batch size [ValidateRange(1,100)] [int] $BatchSize = 100, +# The number of records to get [long] $First, +# If number of records to skip [long] $Skip ) @@ -7109,14 +7212,14 @@ foreach ($at in $AtUri) { # If the uri is not an at uri if ($atRecord.uri -notmatch $atPattern) { # continue to the next record - continue + continue } packAtProtoRecord } } else { - try { + try { $atBlob = $this.GetAtBlob($matches.did, $matches.type) $atContentType = $atBlob.Headers.'Content-Type' if (-not $atContentType) { @@ -7162,24 +7265,38 @@ return $Package Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile #> param( +# Who [did](https://atproto.com/specs/did) (decentralized identifier) [Parameter(Mandatory)] +[Alias('DectralizedIdentifier')] [string] $did, +# What collection or type [Parameter(Mandatory)] [string] $collection, +# What is the record key? [Parameter(Mandatory)] [string] $rkey, -[uri] +# Where is the data? +[string] $pds = "https://bsky.social" ) # Construct the XRPC url to that record -$xrpcUrl = "$pds/xrpc/com.atproto.repo.getRecord?repo=$( +$xrpcUrl = "$( + # Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' + } +)/xrpc/com.atproto.repo.getRecord?repo=$( $did )&collection=$( $collection @@ -7205,24 +7322,32 @@ Invoke-RestMethod -Uri $xrpcUrl Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile #> param( +# Who [did](https://atproto.com/specs/did) (decentralized identifier) [Parameter(Mandatory)] +[Alias('DectralizedIdentifier')] [string] $did, +# What collection or type [Parameter(Mandatory)] [string] $collection, +# How many items to get in each batch. +# By default, 100. [ValidateRange(1,100)] [int] $BatchSize = 100, +# If provided, will only get N items. [long] $First, +# If provided, will skip N items. [long] $Skip, +# The PDS (Personal Data Server). [string] $pds = "https://bsky.social/" ) @@ -7234,8 +7359,17 @@ $skipped = [long]0 $cursor = '' $progress = [Ordered]@{Id = Get-Random} $progress.Status = "Getting records" -:AtSync do { - $xrpcUrl = "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=$( +:AtSync do { + $xrpcUrl = "$( + # Be fault tolerant with the pds format + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # and prefix anything else by https:// + "https://$pds" -replace '/$' + } + )/xrpc/com.atproto.repo.listRecords?repo=$( $did )&collection=$( $collection @@ -8342,6 +8476,10 @@ $IncludeGit, [switch] $IncludeNodeModule, +# The personal data server. This is used in At Protocol requests. +[string] +$PDS, + # The current package [IO.Packaging.Package] $Package From 404a8cd64cd45a6aa5f37d6890eca45cb80aa2f7 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 18:50:16 -0700 Subject: [PATCH 395/724] feat: `OpenPackage.Part.ReadXml` ( Fixes #98 ) Adding more xml file patterns --- Types/OpenPackage.Part/ReadXml.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/ReadXml.ps1 b/Types/OpenPackage.Part/ReadXml.ps1 index bd2f828..3d0368d 100644 --- a/Types/OpenPackage.Part/ReadXml.ps1 +++ b/Types/OpenPackage.Part/ReadXml.ps1 @@ -5,7 +5,7 @@ Reads an OpenPackage Part's Content as XML #> [Reflection.AssemblyMetadata( - 'FilePattern', '\.(?>svg|ps1xml|xml|xhtml)?$' + 'FilePattern', '\.(?>svg|ps1xml|xml|xhtml|.+proj|nuspec)$' )] [Reflection.AssemblyMetadata( 'ContentTypePattern', '[/\+]xml?$' From 50657237a14302efa9167cfa166fcdf4a473d756 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 20 Mar 2026 01:50:32 +0000 Subject: [PATCH 396/724] feat: `OpenPackage.Part.ReadXml` ( Fixes #98 ) Adding more xml file patterns --- OP.types.ps1xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index c0e2b07..d75a7d4 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11154,7 +11154,7 @@ if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.Inp Reads an OpenPackage Part's Content as XML #> [Reflection.AssemblyMetadata( - 'FilePattern', '\.(?>svg|ps1xml|xml|xhtml)?$' + 'FilePattern', '\.(?>svg|ps1xml|xml|xhtml|.+proj|nuspec)$' )] [Reflection.AssemblyMetadata( 'ContentTypePattern', '[/\+]xml?$' @@ -12656,7 +12656,7 @@ if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.Inp Reads an OpenPackage Part's Content as XML #> [Reflection.AssemblyMetadata( - 'FilePattern', '\.(?>svg|ps1xml|xml|xhtml)?$' + 'FilePattern', '\.(?>svg|ps1xml|xml|xhtml|.+proj|nuspec)$' )] [Reflection.AssemblyMetadata( 'ContentTypePattern', '[/\+]xml?$' @@ -14158,7 +14158,7 @@ if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.Inp Reads an OpenPackage Part's Content as XML #> [Reflection.AssemblyMetadata( - 'FilePattern', '\.(?>svg|ps1xml|xml|xhtml)?$' + 'FilePattern', '\.(?>svg|ps1xml|xml|xhtml|.+proj|nuspec)$' )] [Reflection.AssemblyMetadata( 'ContentTypePattern', '[/\+]xml?$' From bdbbe517dd3144a5f8ad3a297d71014ed42a5c32 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 19:01:20 -0700 Subject: [PATCH 397/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding content types --- .../DefaultTypeMap.psd1 | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 index 5c4f969..7edef50 100644 --- a/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 +++ b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 @@ -10,10 +10,13 @@ '.bmp' = 'image/bmp' '.bz' = 'application/x-bzip' '.bz2' = 'application/x-bzip2' + '.c' = 'text/plain' '.cda' = 'application/x-cdf' + '.cpp' = 'text/plain' + '.cs' = 'text/plain' '.csh' = 'application/x-csh' '.csproj' = 'application/xml' - '.css' = 'text/css' + '.css' = 'text/css' '.csv' = 'text/csv' '.doc' = 'application/msword' '.docx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' @@ -22,7 +25,9 @@ '.exe' = 'application/executeable' '.fsproj' = 'application/xml' '.gz' = 'application/gzip' + '.go' = 'text/plain' '.gif' = 'image/gif' + '.h' = 'text/plain' '.htm' = 'text/html' '.html' = 'text/html' '.ico' = 'image/vnd.microsoft.icon' @@ -36,6 +41,7 @@ '.json' = 'application/json' '.jsonld' = 'application/ld+json' '.md' = 'text/markdown' + '.mod' = 'text/plain' '.mid' = 'audio/midi' '.midi' = 'audio/midi' '.mjs' = 'text/javascript' @@ -43,6 +49,7 @@ '.mp4' = 'video/mp4' '.mpeg' = 'video/mpeg' '.mpkg' = 'application/vnd.apple.installer+xml' + '.op' = 'application/zip' '.odp' = 'application/vnd.oasis.opendocument.presentation' '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' '.odt' = 'application/vnd.oasis.opendocument.text' @@ -62,6 +69,8 @@ '.pptx' = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' '.rar' = 'application/vnd.rar' '.rtf' = 'application/rtf' + '.rs' = 'text/plain' + '.s' = 'text/plain' '.sh' = 'application/x-sh' '.svg' = 'image/svg+xml' '.tar' = 'application/x-tar' @@ -71,8 +80,10 @@ '.ttf' = 'font/ttf' '.ttl' = 'text/turtle' '.txt' = 'text/plain' - '.url' = 'text/plain' + '.url' = 'text/plain' + '.vb' = 'text/plain' '.vbproj' = 'application/xml' + '.vbs' = 'text/plain' '.vsd' = 'application/vnd.visio' '.wav' = 'audio/wav' '.weba' = 'audio/webm' From 1e1002b2bd5b49fdd3bf7fc5a0e139f449486637 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 20 Mar 2026 02:01:56 +0000 Subject: [PATCH 398/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding content types --- OP.types.ps1xml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d75a7d4..4ba9e9f 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -10503,10 +10503,13 @@ FileList '.bmp' = 'image/bmp' '.bz' = 'application/x-bzip' '.bz2' = 'application/x-bzip2' + '.c' = 'text/plain' '.cda' = 'application/x-cdf' + '.cpp' = 'text/plain' + '.cs' = 'text/plain' '.csh' = 'application/x-csh' '.csproj' = 'application/xml' - '.css' = 'text/css' + '.css' = 'text/css' '.csv' = 'text/csv' '.doc' = 'application/msword' '.docx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' @@ -10515,7 +10518,9 @@ FileList '.exe' = 'application/executeable' '.fsproj' = 'application/xml' '.gz' = 'application/gzip' + '.go' = 'text/plain' '.gif' = 'image/gif' + '.h' = 'text/plain' '.htm' = 'text/html' '.html' = 'text/html' '.ico' = 'image/vnd.microsoft.icon' @@ -10529,6 +10534,7 @@ FileList '.json' = 'application/json' '.jsonld' = 'application/ld+json' '.md' = 'text/markdown' + '.mod' = 'text/plain' '.mid' = 'audio/midi' '.midi' = 'audio/midi' '.mjs' = 'text/javascript' @@ -10536,6 +10542,7 @@ FileList '.mp4' = 'video/mp4' '.mpeg' = 'video/mpeg' '.mpkg' = 'application/vnd.apple.installer+xml' + '.op' = 'application/zip' '.odp' = 'application/vnd.oasis.opendocument.presentation' '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' '.odt' = 'application/vnd.oasis.opendocument.text' @@ -10555,6 +10562,8 @@ FileList '.pptx' = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' '.rar' = 'application/vnd.rar' '.rtf' = 'application/rtf' + '.rs' = 'text/plain' + '.s' = 'text/plain' '.sh' = 'application/x-sh' '.svg' = 'image/svg+xml' '.tar' = 'application/x-tar' @@ -10564,8 +10573,10 @@ FileList '.ttf' = 'font/ttf' '.ttl' = 'text/turtle' '.txt' = 'text/plain' - '.url' = 'text/plain' + '.url' = 'text/plain' + '.vb' = 'text/plain' '.vbproj' = 'application/xml' + '.vbs' = 'text/plain' '.vsd' = 'application/vnd.visio' '.wav' = 'audio/wav' '.weba' = 'audio/webm' From cb31ab562df50f5606692823be4326355c409768 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 19:08:59 -0700 Subject: [PATCH 399/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding default display --- Types/OpenPackage.ContentTypeMap/DefaultDisplay.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 Types/OpenPackage.ContentTypeMap/DefaultDisplay.txt diff --git a/Types/OpenPackage.ContentTypeMap/DefaultDisplay.txt b/Types/OpenPackage.ContentTypeMap/DefaultDisplay.txt new file mode 100644 index 0000000..06810ba --- /dev/null +++ b/Types/OpenPackage.ContentTypeMap/DefaultDisplay.txt @@ -0,0 +1 @@ +TypeMap \ No newline at end of file From 0a98c4a36a7aa3a84c266b5f6008020a852012a9 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 20 Mar 2026 02:09:25 +0000 Subject: [PATCH 400/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding default display --- OP.types.ps1xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 4ba9e9f..af0c237 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -10488,6 +10488,17 @@ FileList OpenPackage.ContentTypeMap + + PSStandardMembers + + + DefaultDisplayPropertySet + + TypeMap + + + + DefaultTypeMap @@ -10648,6 +10659,10 @@ if ($TypeMap.Count -eq 0) { } + + DefaultDisplay + TypeMap + From eaceeba73dfcce89a31e806ef5312da63513dd4c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 19 Mar 2026 19:30:28 -0700 Subject: [PATCH 401/724] feat: `OpenPackage.get_Mcp.json` ( Fixes #146 ) Expanding pattern list --- Types/OpenPackage/get_Mcp.json.ps1 | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage/get_Mcp.json.ps1 b/Types/OpenPackage/get_Mcp.json.ps1 index 9ecb1a2..d72cd62 100644 --- a/Types/OpenPackage/get_Mcp.json.ps1 +++ b/Types/OpenPackage/get_Mcp.json.ps1 @@ -2,12 +2,25 @@ .SYNOPSIS Gets a Package's mcp.json .DESCRIPTION - Gets any `/mcp.json` or `/claude_desktop_config.json` in a package + Gets any mcp definitions in an Open Package + + Definitions can be in parts matching: + * `/mcp.json` + * `/claude_desktop_config.json` + * `/\.?mcp/server.json` #> param() +$pattern = @( + "/mcp\.json" + "/claude_desktop_config\.json" + '/\.?mcp/server.json$' +) + +$pattern = "(?>$($pattern -join '|'))$" + foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch '/(?>mcp|claude_desktop_config)\.json$') { + if ($part.Uri -notmatch $pattern) { continue } From 8d23f19772b8b9c6a309c5456d5898ffcd5d8bd0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 20 Mar 2026 02:30:48 +0000 Subject: [PATCH 402/724] feat: `OpenPackage.get_Mcp.json` ( Fixes #146 ) Expanding pattern list --- OP.types.ps1xml | 51 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index af0c237..0e6c43d 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2922,12 +2922,25 @@ foreach ($part in $this.GetParts()) { .SYNOPSIS Gets a Package's mcp.json .DESCRIPTION - Gets any `/mcp.json` or `/claude_desktop_config.json` in a package + Gets any mcp definitions in an Open Package + + Definitions can be in parts matching: + * `/mcp.json` + * `/claude_desktop_config.json` + * `/\.?mcp/server.json` #> param() +$pattern = @( + "/mcp\.json" + "/claude_desktop_config\.json" + '/\.?mcp/server.json$' +) + +$pattern = "(?>$($pattern -join '|'))$" + foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch '/(?>mcp|claude_desktop_config)\.json$') { + if ($part.Uri -notmatch $pattern) { continue } @@ -6417,12 +6430,25 @@ foreach ($part in $this.GetParts()) { .SYNOPSIS Gets a Package's mcp.json .DESCRIPTION - Gets any `/mcp.json` or `/claude_desktop_config.json` in a package + Gets any mcp definitions in an Open Package + + Definitions can be in parts matching: + * `/mcp.json` + * `/claude_desktop_config.json` + * `/\.?mcp/server.json` #> param() +$pattern = @( + "/mcp\.json" + "/claude_desktop_config\.json" + '/\.?mcp/server.json$' +) + +$pattern = "(?>$($pattern -join '|'))$" + foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch '/(?>mcp|claude_desktop_config)\.json$') { + if ($part.Uri -notmatch $pattern) { continue } @@ -9912,12 +9938,25 @@ foreach ($part in $this.GetParts()) { .SYNOPSIS Gets a Package's mcp.json .DESCRIPTION - Gets any `/mcp.json` or `/claude_desktop_config.json` in a package + Gets any mcp definitions in an Open Package + + Definitions can be in parts matching: + * `/mcp.json` + * `/claude_desktop_config.json` + * `/\.?mcp/server.json` #> param() +$pattern = @( + "/mcp\.json" + "/claude_desktop_config\.json" + '/\.?mcp/server.json$' +) + +$pattern = "(?>$($pattern -join '|'))$" + foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch '/(?>mcp|claude_desktop_config)\.json$') { + if ($part.Uri -notmatch $pattern) { continue } From f5da13de361f28e06deb0c6ccecf92698c9fb6c1 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 20 Mar 2026 11:52:31 -0700 Subject: [PATCH 403/724] feat: `Start-OpenPackage` ( Fixes #5 ) Adding MessageData to event and other minor fixes --- Commands/Start-OpenPackage.ps1 | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Commands/Start-OpenPackage.ps1 b/Commands/Start-OpenPackage.ps1 index 4f170e5..166bba2 100644 --- a/Commands/Start-OpenPackage.ps1 +++ b/Commands/Start-OpenPackage.ps1 @@ -52,7 +52,7 @@ function Start-OpenPackage { # If set, the scripts in the package will be invokable. # This turns allows every PowerShell script in the package into server side code. - # This should be used catiously, and only with known packages. + # This should be used cautiously, and only with known packages. [Alias('CanInvoke','Invocable')] [switch]$Invokable, @@ -435,12 +435,10 @@ function Start-OpenPackage { # If we're allowing additional methods, we can easily do CRUD operations switch -regex ($request.HttpMethod) { - # put or post changes file content + # Put or Post changes file content. 'put|post' { - $anythingChanged = $false - + $anythingChanged = $false $memoryStream = [IO.MemoryStream]::new() - if ($request.InputStream.CanRead) { $request.InputStream.CopyTo($memoryStream) } @@ -452,7 +450,7 @@ function Start-OpenPackage { $partStream = if ($pack.PartExists($request.Url.LocalPath)) { - $pack.GetStream() + $pack.GetPart($request.Url.LocalPath).GetStream() } else { $newPart = $pack.CreatePart($request.Url.LocalPath, $request.ContentType, 'Superfast') $newPart.GetStream() @@ -556,6 +554,14 @@ function Start-OpenPackage { if (-not $io.BufferSize) { $io.BufferSize = $BufferSize } + + $messageData = [Ordered]@{ + RootUrl = $RootUrl + Package = $package + HttpListener = $httpListener + InvocationInfo = $MyInvocation + Command = $MyInvocation.MyCommand + } # Generate an event $StartOpenPackageEvent = $generateEvent.Invoke( @@ -599,8 +605,7 @@ function Start-OpenPackage { if (-not $site) { $site = [Ordered]@{} - } - + } $IO.ImportModule = @( if ($myInvocation.MyCommand.Module) { @@ -616,7 +621,7 @@ function Start-OpenPackage { ArgumentList=$IO Name=$RootUrl ThrottleLimit = $ThrottleLimit - InitializationScript = $InitializationScript + InitializationScript = $InitializationScript } # Start a thread job From e10d5843d1a544deebcc1af55d1010d29d44c1ce Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 20 Mar 2026 12:31:21 -0700 Subject: [PATCH 404/724] docs: `contributing.md` ( Fixes #147 ) --- contributing.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 contributing.md diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..8a1df1f --- /dev/null +++ b/contributing.md @@ -0,0 +1,11 @@ +# OP is Open + +Open Packages are pretty powerful. + +They create an Open Platform for rich local applications and services. + +We are _very_ open to ideas, integrations, and help. + +We want to be able to open anything as a package, and will accept any help to do so. + +Please consider filing an issue, starting a discussion, or contributing code. \ No newline at end of file From 85bf7dfd2ba57acb394bc6891acf8fbb3af8a497 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 20 Mar 2026 12:32:35 -0700 Subject: [PATCH 405/724] docs: `code_of_conduct.md` ( Fixes #148 ) --- code_of_conduct.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 code_of_conduct.md diff --git a/code_of_conduct.md b/code_of_conduct.md new file mode 100644 index 0000000..fdf09f3 --- /dev/null +++ b/code_of_conduct.md @@ -0,0 +1,7 @@ +# OP Code of Conduct + +Open Packages can be pretty overpowered. + +They should be used for good. + +Please use the powers of this module for good, and report any abuse of this module. \ No newline at end of file From 07610a4e85f8456cf507574df37def105cb02ba1 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 20 Mar 2026 12:51:20 -0700 Subject: [PATCH 406/724] docs: `security.md` ( Fixes #149 ) --- security.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 security.md diff --git a/security.md b/security.md new file mode 100644 index 0000000..84d21e8 --- /dev/null +++ b/security.md @@ -0,0 +1,160 @@ +# OP Security + +Many packages are zip files in a trenchcoat, and files can be dangerous. + +OP lets you work with all sorts of packages. This can be helpful, and can be dangerous. + +Lets think of things we can do with a package in terms of levels of risk: + +## Low Risk + +### Reading Packages + +It is generally safe to read files. + +OP helps you with this: you can make anything into a package, and see what's inside. + +OP provides an Open Platform for reading files and many ways to see what is inside and check the contents. + +It also provides ways to inspect any code inside of the package (see mitigations): + +### Showing Packages + +Because Open Packages have both file data and content types, we can easily serve them up and show them in a browser. + +IF the server does does now allow any other verb than `-Get`, and is run locally, this is fairly safe. + +It is more safe if not running as root or admin, and only interacting with well-known content types. + +## Medium Risk + +### Editing Packages + +Things start to get tricky when you can change a package. + +We want to enable package editing, but do so as safely as we can. + +OP can let you change packages with simple local PUT/POST/DELETE operations. + +But only if you `-Allow` it. If we are running a local web server, we might `-Allow` it. + +If we were running a remote web server with this flag on, we might be very foolish. + +To start a local server that can edit a package, we can run: + +~~~PowerShell +$package | + Start-OpenPackage -Allow GET, HEAD, POST, PUT, DELETE +~~~ + +## High Risk + +### Invoking Packages + +Packages can contain code, and code can be run. + +As Open Packages can be an Open Platform for applications, we _can_ invoke from packages. + +We would be very foolish if we did this with packages we do not trust. + +## Mitigations + +In order to mitigate risks, Open Package provides several mitigations. + +### Package Metadata + +Open Packages contain metadata that most archives lack. + +Each Open Package has a [`.PackageProperties`](https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties?wt.mc_id=MVP_321542) collection. + +This collection contains some useful information, such as the Identifier and Version. + +### Package Content Hashes + +It is trivial to get all of the hashes of files within a package. + +If these hashes deviate, the package has been changed. + +For example, if we wanted all of the hashes of a PowerShell gallery module, we can use: + +~~~PowerShell +$turtlePackage = Get-OpenPackage https://powershellgallery.com/Packages/Turtle +$turtlePackage.FileHash +~~~ + +### Package Data Inspection + +In addition to reading hashes, we can also read any file within the package. + +This allows us to scan packages. + +`Select-OpenPackage` can be quite handy. + +It allows us to select parts of a package by name, and allows us to search within package parts using: + +* Regular Expressions +* XPath +* PowerShell AST Conditions + +Additionally, each package has several properties to assist inspection. + +For example, `.package.json` will return any package.json files in the package. + +To assist in inspecting PowerShell packages, we can also look for specific Abstract Syntax Tree types: + +* `.GetPowerShellCommandAst` gets all commands referenced in a package +* `.GetPowerShellParameterAst` gets all parameters referenced in a package +* `.GetPowerShellTypeAst` gets all types referenced in a package + +To see every property an OpenPackage provides, use Get-Member: + +~~~PowerShell +$openPackage = Import-Module OP -PassThru | OP +$openPackage | Get-Member +~~~ + +### Locking Packages + +In order to prevent package alteration, we can `Lock-OpenPackage` to get a read-only version of the package. + +This will prevent alteration of the package, even if we choose the `-Allow` other http verbs in `Start-OpenPackage` + +### Eventing + +In order to prevent the loading of unwelcome packages or starting of unwelcome servers, many commands broadcast an event. + +For example: + +* `Get-OpenPackage` will be sent every time a package might be opened. +* `Start-OpenPackage` will be sent every time a package might be run as a server. + +These events can be used for logging of activity. + +We can also use them to prevent some execution + +### Preventing Execution + +Each event has a .MessageData dictionary. + +To prevent a command from executing, just say `no`. + +For example, we want to prevent Start-OpenPackage from launching any server, we can use: + +~~~PowerShell +Register-EngineEvent -SourceIdentifier Start-OpenPackage -Action { + Write-Host "Won't Start" + + $event.MessageData['No'] = 'way' +} +~~~ + +Adding any number of keys to an event's message will reject the event. + +Those properties are: + +* `No` +* `Deny` +* `Reject` +* `Rejected` + +If any of these properties are found, the command should stop processing. \ No newline at end of file From b548009e208f81e3a062e83b3efcbd17aeb73b0b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 14:22:57 -0700 Subject: [PATCH 407/724] feat: `OpenPackage.GetAtRecord` ( Fixes #113 ) Improving fault tolerance --- Types/OpenPackage/GetAtRecord.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage/GetAtRecord.ps1 b/Types/OpenPackage/GetAtRecord.ps1 index 412c655..761e987 100644 --- a/Types/OpenPackage/GetAtRecord.ps1 +++ b/Types/OpenPackage/GetAtRecord.ps1 @@ -47,5 +47,10 @@ $xrpcUrl = "$( )" # and go fetch. -Invoke-RestMethod -Uri $xrpcUrl +try { + Invoke-RestMethod -Uri $xrpcUrl +} catch { + Write-Verbose "$xrpcUrl - $_" +} + From aa70d63532696c1160dd9ebdeff6ae928b4ba443 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 14:23:25 -0700 Subject: [PATCH 408/724] feat: `OpenPackage.GetAtType` ( Fixes #114 ) Improving fault tolerance --- Types/OpenPackage/GetAtType.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage/GetAtType.ps1 b/Types/OpenPackage/GetAtType.ps1 index 5c50923..e31808d 100644 --- a/Types/OpenPackage/GetAtType.ps1 +++ b/Types/OpenPackage/GetAtType.ps1 @@ -62,7 +62,15 @@ $progress.Status = "Getting records" $progress.Activity = "$total " Write-Progress @progress # Get the page of records - $results = Invoke-RestMethod $xrpcUrl + $results = try { + Invoke-RestMethod $xrpcUrl + } catch { + $_ + } + if ($results -is [Management.Automation.ErrorRecord]) { + Write-Verbose "$xrpcUrl - $results" + continue + } # If we got results and have a cursor to more if ($results -and $results.cursor) { # set it for the next round. From 516c13f4e779cbef2f45a9da53a0e8d9ff12a357 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 14:24:33 -0700 Subject: [PATCH 409/724] feat: `OpenPackage.GetAtProto` ( Fixes #125 ) Improving fault tolerance --- Types/OpenPackage/GetAtProto.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage/GetAtProto.ps1 b/Types/OpenPackage/GetAtProto.ps1 index 0e4f299..b370cea 100644 --- a/Types/OpenPackage/GetAtProto.ps1 +++ b/Types/OpenPackage/GetAtProto.ps1 @@ -167,5 +167,8 @@ foreach ($at in $AtUri) { } } +# Only return a package if it is not empty. +if (@($package.GetParts()).Length) { + return $Package +} -return $Package From d7f06eb9e1a0f94ba0a2c0f4e2c6579e31a03071 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 14:59:07 -0700 Subject: [PATCH 410/724] feat: `OpenPackage.GetAt` ( Fixes #150 ) --- Commands/Get-OpenPackage.ps1 | 42 ++++++++-- Types/OpenPackage/GetAt.ps1 | 149 +++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 Types/OpenPackage/GetAt.ps1 diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 2eca464..0d81960 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -104,6 +104,13 @@ function Get-OpenPackage [string] $FilePath, + # Gets Open Packages with `@` syntax. + # Without a domain, `@` will be presumed to be a github + # With a domain, `@` will look for an https url, and check at protocol + [Parameter(Mandatory,ParameterSetName='At',ValueFromPipelineByPropertyName)] + [string[]] + $At, + # A URI to package. # If this URI is a git repository, will make a package out of the repository # If this URI is a nuget package url or powershell gallery url, will download the package. @@ -375,7 +382,6 @@ function Get-OpenPackage InvokeOpMethod 'GetZip' $NamedParameters } - $myNoun = $MyInvocation.MyCommand.Name -replace '^.+?-' $generateEvent = [Runspace]::DefaultRunspace.Events.GenerateEvent } @@ -498,7 +504,21 @@ function Get-OpenPackage continue nextArgument } - # If the argument started with at:// + # If the argument started with `@` + if ($arg -match '^@') { + # treat it as open-ended at syntax + $atPackages = Get-OpenPackage -At $arg @namedParameters + foreach ($atPackage in $atPackages) { + if ($atPackage -isnot [IO.Packaging.Package]) { + continue + } + if ($packages -notcontains $atPackage) { + $packages += $atPackage + } + } + continue nextArgument + } + # If the argument started with `at://` if ($arg -match '^at://') { # treat it as an at uri and turn it into a package. $atPackage = Get-OpenPackage -AtUri $arg @namedParameters @@ -515,7 +535,7 @@ function Get-OpenPackage $uriPackage = Get-OpenPackage -Uri $argUri @namedParameters if ($packages -notcontains $uriPackage) { $packages += $uriPackage - } + } continue nextArgument } @@ -550,19 +570,27 @@ function Get-OpenPackage InvokeOpMethod 'GetUrl' $NamedParameters return } + + #region Open Package in At Syntax + if ($At) { + $NamedParameters['At'] = $At + InvokeOpMethod 'GetAt' $NamedParameters + return + } + #endregion Open Package in At Syntax + #region Open Package from Repository if ($Repository) { $NamedParameters['Repository'] = $Repository - InvokeOpMethod 'GetRepository' $NamedParameters + InvokeOpMethod 'GetRepository' $NamedParameters return } #endregion Open Package from Repository #region Open Package from At Uri if ($AtUri) { - $NamedParameters['AtUri'] = $AtUri - + $NamedParameters['AtUri'] = $AtUri InvokeOpMethod 'GetAtProto' $NamedParameters return } @@ -634,6 +662,6 @@ function Get-OpenPackage return } - getCurrentPack + getCurrentPack } } \ No newline at end of file diff --git a/Types/OpenPackage/GetAt.ps1 b/Types/OpenPackage/GetAt.ps1 new file mode 100644 index 0000000..8ab6459 --- /dev/null +++ b/Types/OpenPackage/GetAt.ps1 @@ -0,0 +1,149 @@ +<# +.SYNOPSIS + Gets Open Packages at a location +.DESCRIPTION + Gets Open Packages with `@` syntax. + + Without a domain `@` will be presumed to be github.com + With a domain `@` at will check for records using https and at protocol +#> +param( +# One or more locations, in at syntax. +# `@name` will default to github +# `@domain.name` will default to at protocol and https +[string[]] +$At, + +# A list of file wildcards to include. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Include, + +# A list of file wildcards to exclude. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Exclude, + +# The base path within the package. +# Content should be added beneath this base path. +[string] +$BasePath = '/', + +# A content type map. +# This maps extensions and URIs to a content type. +[Collections.IDictionary] +$TypeMap = $( + ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap +), + +# The compression option. +[IO.Packaging.CompressionOption] +[Alias('CompressionLevel')] +$CompressionOption = 'Superfast', + +# If set, will force the redownload of various resources and remove existing files or directories +[switch] +$Force, + +# If set, will include hidden files and folders, except for files beneath `.git` +[Alias('IncludeDotFiles')] +[switch] +$IncludeHidden, + +# If set, will include the `.git` directory contents if found. +[Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] +[switch] +$IncludeGit, + +# If set, will include any content found in `/node_modules`. +[Alias('IncludeNodeModules')] +[switch] +$IncludeNodeModule, + +# The current package +[IO.Packaging.Package] +$Package +) + + +if (-not $package) { + $memoryStream = [IO.MemoryStream]::new() + $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') + $package.pstypenames.insert(0, 'OP') + $package.pstypenames.insert(0, 'OpenPackage') + Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force +} + + +$namedParameters = [Ordered]@{} + +foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { + $var = $ExecutionContext.SessionState.PSVariable.Get($key) + if (-not [string]::IsNullOrEmpty($var.value)) { + $namedParameters[$key] = $var.value + } +} + +$namedParameters.Remove('At') +$namedParameters.Remove('Package') + +$outputPackages = @() + +foreach ($atSyntax in $at) { + $parameterCopy = [Ordered]@{} + $namedParameters + if ($atSyntax -notmatch '^@') { + continue + } + # If there is no domain name qualifier, treat it as github.com/ + if ($atSyntax -notmatch '\.') { + $parameterCopy.InputObject = $Package + $parameterCopy.Repository = $atSyntax -replace '^@', 'https://github.com/' + $atRepoPackage = Get-OpenPackage @parameterCopy + foreach ($repoPackage in $atRepoPackage) { + if ( + $repoPackage -is [IO.Packaging.Package] -and + $outputPackages -notcontains $repoPackage + ) { + $outputPackages += $repoPackage + } + } + } + else { + # Otherwise, it could be either at protocol or https. + # Try https first + $parameterCopy.InputObject = $package + $parameterCopy.Url = $atSyntax -replace '^@', 'https://' + + $httpsPackage = Get-OpenPackage @parameterCopy + + if ($httpsPackage) { + $parameterCopy.InputObject = $httpsPackage + } + $parameterCopy.Remove('Url') + $parameterCopy.AtUri = $atSyntax -replace '^@', 'at://' + $atPackage = Get-OpenPackage @parameterCopy + + $possiblePackages = @( + if ($httpsPackage -and ($atPackage -eq $httpsPackage)) { + $httpsPackage + } else { + if ($atPackage) { + $atPackage + } + if ($httpsPackage) { + $httpsPackage + } + } + ) + foreach ($possibility in $possiblePackages ) { + if ($possibility -is [IO.Packaging.Package] -and + $outputPackages -notcontains $possibility) { + $outputPackages += $possibility + } + } + } +} + +return $outputPackages \ No newline at end of file From 35e08f5ef5ee5757dfe9cacefcc1f1f48ba09b5d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 21:59:27 +0000 Subject: [PATCH 411/724] feat: `OpenPackage.GetAt` ( Fixes #150 ) --- OP.types.ps1xml | 528 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 519 insertions(+), 9 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0e6c43d..90d8ed6 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -47,6 +47,160 @@ Underbar Underscore + + GetAt + + GetAtBlob @@ -315,7 +472,12 @@ $xrpcUrl = "$( )" # and go fetch. -Invoke-RestMethod -Uri $xrpcUrl +try { + Invoke-RestMethod -Uri $xrpcUrl +} catch { + Write-Verbose "$xrpcUrl - $_" +} + @@ -387,7 +549,15 @@ $progress.Status = "Getting records" $progress.Activity = "$total " Write-Progress @progress # Get the page of records - $results = Invoke-RestMethod $xrpcUrl + $results = try { + Invoke-RestMethod $xrpcUrl + } catch { + $_ + } + if ($results -is [Management.Automation.ErrorRecord]) { + Write-Verbose "$xrpcUrl - $results" + continue + } # If we got results and have a cursor to more if ($results -and $results.cursor) { # set it for the next round. @@ -3555,6 +3725,160 @@ FileList Underbar Underscore + + GetAt + + GetAtBlob @@ -3823,7 +4150,12 @@ $xrpcUrl = "$( )" # and go fetch. -Invoke-RestMethod -Uri $xrpcUrl +try { + Invoke-RestMethod -Uri $xrpcUrl +} catch { + Write-Verbose "$xrpcUrl - $_" +} + @@ -3895,7 +4227,15 @@ $progress.Status = "Getting records" $progress.Activity = "$total " Write-Progress @progress # Get the page of records - $results = Invoke-RestMethod $xrpcUrl + $results = try { + Invoke-RestMethod $xrpcUrl + } catch { + $_ + } + if ($results -is [Management.Automation.ErrorRecord]) { + Write-Verbose "$xrpcUrl - $results" + continue + } # If we got results and have a cursor to more if ($results -and $results.cursor) { # set it for the next round. @@ -7063,6 +7403,160 @@ FileList Underbar Underscore + + GetAt + + GetAtBlob @@ -7331,7 +7828,12 @@ $xrpcUrl = "$( )" # and go fetch. -Invoke-RestMethod -Uri $xrpcUrl +try { + Invoke-RestMethod -Uri $xrpcUrl +} catch { + Write-Verbose "$xrpcUrl - $_" +} + @@ -7403,7 +7905,15 @@ $progress.Status = "Getting records" $progress.Activity = "$total " Write-Progress @progress # Get the page of records - $results = Invoke-RestMethod $xrpcUrl + $results = try { + Invoke-RestMethod $xrpcUrl + } catch { + $_ + } + if ($results -is [Management.Automation.ErrorRecord]) { + Write-Verbose "$xrpcUrl - $results" + continue + } # If we got results and have a cursor to more if ($results -and $results.cursor) { # set it for the next round. From 3720954c42acdd3fe92ac106758f7ab8ac89a5cb Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 15:01:04 -0700 Subject: [PATCH 412/724] feat: `OpenPackage.GetUrl` ( Fixes #119, re #150 ) Improving fault tolerance to support @ --- Types/OpenPackage/GetUrl.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage/GetUrl.ps1 b/Types/OpenPackage/GetUrl.ps1 index ccf1ea8..c0cb9f4 100644 --- a/Types/OpenPackage/GetUrl.ps1 +++ b/Types/OpenPackage/GetUrl.ps1 @@ -75,6 +75,7 @@ foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { } } $namedParameters.Remove('url') +$namedParameters.Remove('inputObject') foreach ($uri in $url) { # If the uri is an at:// uri @@ -129,7 +130,15 @@ foreach ($uri in $url) { } # At this point lets just poke at the uri and make a package. - $WebResponse = Invoke-WebRequest -Uri $Uri + $WebResponse = try { + Invoke-WebRequest -Uri $Uri + } catch { + $_ + } + if ($WebResponse -is [Management.Automation.ErrorRecord]) { + Write-Verbose "$uri - $webResponse" + continue + } # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and From f43830d3bd24b2ad4bc53874899e86259545baf0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 22:01:35 +0000 Subject: [PATCH 413/724] feat: `OpenPackage.GetUrl` ( Fixes #119, re #150 ) Improving fault tolerance to support @ --- OP.types.ps1xml | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 90d8ed6..e72d765 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1673,6 +1673,7 @@ foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { } } $namedParameters.Remove('url') +$namedParameters.Remove('inputObject') foreach ($uri in $url) { # If the uri is an at:// uri @@ -1727,7 +1728,15 @@ foreach ($uri in $url) { } # At this point lets just poke at the uri and make a package. - $WebResponse = Invoke-WebRequest -Uri $Uri + $WebResponse = try { + Invoke-WebRequest -Uri $Uri + } catch { + $_ + } + if ($WebResponse -is [Management.Automation.ErrorRecord]) { + Write-Verbose "$uri - $webResponse" + continue + } # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and @@ -5351,6 +5360,7 @@ foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { } } $namedParameters.Remove('url') +$namedParameters.Remove('inputObject') foreach ($uri in $url) { # If the uri is an at:// uri @@ -5405,7 +5415,15 @@ foreach ($uri in $url) { } # At this point lets just poke at the uri and make a package. - $WebResponse = Invoke-WebRequest -Uri $Uri + $WebResponse = try { + Invoke-WebRequest -Uri $Uri + } catch { + $_ + } + if ($WebResponse -is [Management.Automation.ErrorRecord]) { + Write-Verbose "$uri - $webResponse" + continue + } # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and @@ -9029,6 +9047,7 @@ foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { } } $namedParameters.Remove('url') +$namedParameters.Remove('inputObject') foreach ($uri in $url) { # If the uri is an at:// uri @@ -9083,7 +9102,15 @@ foreach ($uri in $url) { } # At this point lets just poke at the uri and make a package. - $WebResponse = Invoke-WebRequest -Uri $Uri + $WebResponse = try { + Invoke-WebRequest -Uri $Uri + } catch { + $_ + } + if ($WebResponse -is [Management.Automation.ErrorRecord]) { + Write-Verbose "$uri - $webResponse" + continue + } # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and From 59a392b824f8d99486b76cd9bf184715393576c6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 15:05:20 -0700 Subject: [PATCH 414/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding content types --- Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 index 7edef50..95e2ce2 100644 --- a/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 +++ b/Types/OpenPackage.ContentTypeMap/DefaultTypeMap.psd1 @@ -49,6 +49,7 @@ '.mp4' = 'video/mp4' '.mpeg' = 'video/mpeg' '.mpkg' = 'application/vnd.apple.installer+xml' + '.nuspec' = 'application/xml' '.op' = 'application/zip' '.odp' = 'application/vnd.oasis.opendocument.presentation' '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' @@ -72,6 +73,7 @@ '.rs' = 'text/plain' '.s' = 'text/plain' '.sh' = 'application/x-sh' + '.srt' = 'application/x-subrip' '.svg' = 'image/svg+xml' '.tar' = 'application/x-tar' '.tif' = 'image/tiff' @@ -84,7 +86,8 @@ '.vb' = 'text/plain' '.vbproj' = 'application/xml' '.vbs' = 'text/plain' - '.vsd' = 'application/vnd.visio' + '.vsd' = 'application/vnd.visio' + '.vtt' = 'text/vtt' '.wav' = 'audio/wav' '.weba' = 'audio/webm' '.webm' = 'video/webm' From bcbb321aee1abcd15bbbb16cd71981cdc3d69dfe Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 22:05:37 +0000 Subject: [PATCH 415/724] feat: `OpenPackage.ContentTypeMap.DefaultTypeMap` ( Fixes #14 ) Adding content types --- OP.types.ps1xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index e72d765..6dd6ea5 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11129,6 +11129,7 @@ FileList '.mp4' = 'video/mp4' '.mpeg' = 'video/mpeg' '.mpkg' = 'application/vnd.apple.installer+xml' + '.nuspec' = 'application/xml' '.op' = 'application/zip' '.odp' = 'application/vnd.oasis.opendocument.presentation' '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' @@ -11152,6 +11153,7 @@ FileList '.rs' = 'text/plain' '.s' = 'text/plain' '.sh' = 'application/x-sh' + '.srt' = 'application/x-subrip' '.svg' = 'image/svg+xml' '.tar' = 'application/x-tar' '.tif' = 'image/tiff' @@ -11164,7 +11166,8 @@ FileList '.vb' = 'text/plain' '.vbproj' = 'application/xml' '.vbs' = 'text/plain' - '.vsd' = 'application/vnd.visio' + '.vsd' = 'application/vnd.visio' + '.vtt' = 'text/vtt' '.wav' = 'audio/wav' '.weba' = 'audio/webm' '.webm' = 'video/webm' From 32d2e2d12afa76d0d957bc6082cd9a8ea1555789 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 15:24:03 -0700 Subject: [PATCH 416/724] feat: `Read-OpenPackage` ( Fixes #67 ) Improving range support and adding inner docs --- Commands/Read-OpenPackage.ps1 | 69 ++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/Commands/Read-OpenPackage.ps1 b/Commands/Read-OpenPackage.ps1 index 500ebb4..a60edaf 100644 --- a/Commands/Read-OpenPackage.ps1 +++ b/Commands/Read-OpenPackage.ps1 @@ -57,49 +57,82 @@ function Read-OpenPackage return $InputObject # pass it thru. } + # Read each part in the open package. foreach ($partUri in $uri) { + # Make sure we have a leading slash $SlashPart = "$partUri" -replace '^/?', '/' + # If the part does not exist if (-not $InputObject.PartExists($SlashPart)) { + # continue to the next part. continue } + # Try to get the part $part = $InputObject.GetPart($SlashPart) + # if that failed, continue. if (-not $part) { continue } + # If we do not have a range start and end if (-not $RangeStart -and -not $RangeEnd) { + # look for a reader if ($part.Reader -and $part.Read) { - $part.Read() - continue + # If we found one, + $part.Read() # read the content + continue # and continue. } + # Otherwise, get our stream $partStream = $part.GetStream('Open', 'Read') + # copy the output to a memory stream $memoryStream = [IO.MemoryStream]::new() - $partStream.CopyTo($memoryStream) + $partStream.CopyTo($memoryStream) } else { + # If a range was provided, get our stream + $partStream = $part.GetStream('Open', 'Read') + # If the range was smaller than the stream length if ($RangeStart -lt $partStream.Length) { + # seek to the start of the range $null = $partStream.Seek($RangeStart, 'Begin') + # and adjust the end if needed if ($rangeEnd -ge $partStream.Length) { - $rangeEnd = $partStream.Length + $rangeEnd = $partStream.Length + } + # If the total range is positive + if ($RangeEnd - $RangeStart -gt 0) { + # get the range + $buffer = [byte[]]::new($RangeEnd - $RangeStart) + # read our byte range + $bytesRead = $partStream.Read($buffer, 0, $buffer.Length) + if ($bytesRead) { + $memoryStream = [IO.MemoryStream]::new() + # and write it to the memory stream + $null = $memoryStream.Write($buffer,0 , $bytesRead) + } } - $buffer = [byte[]]::new($RangeEnd - $RangeStart) - $null = $partStream.Read($buffer, 0, $buffer.Length) - $null = $memoryStream.Write($buffer,0 , $buffer.Length) + + } else { + # copy the output to a memory stream + $memoryStream = [IO.MemoryStream]::new() + $partStream.CopyTo($memoryStream) } } + + $partStream.Close() # Close our part stream + $partStream.Dispose() # and dispose of it. - $partStream.Close() - $partStream.Dispose() - - [byte[]]$partBytes = $memoryStream.ToArray() - - $memoryStream.Close() - $memoryStream.Dispose() - - ,$partBytes - } - + # If we had a memory stream + if ($memoryStream) { + # get it as an array + [byte[]]$partBytes = $memoryStream.ToArray() + + $memoryStream.Close() # then close + $memoryStream.Dispose() # and dispose our stream. + # and write the byte[] as a single object. + $PSCmdlet.WriteObject($partBytes, $false) + } + } } } \ No newline at end of file From 14db341e0495833e22f1fba6d8cf4598eda4763a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 16:11:38 -0700 Subject: [PATCH 417/724] feat: `Start-OpenPackage` layering and lifespan ( Fixes #5 ) Adding inner docs, supporting layering, adding -Lifespan --- Commands/Start-OpenPackage.ps1 | 190 ++++++++++++++++++++++++++------- 1 file changed, 153 insertions(+), 37 deletions(-) diff --git a/Commands/Start-OpenPackage.ps1 b/Commands/Start-OpenPackage.ps1 index 166bba2..e3ee2b9 100644 --- a/Commands/Start-OpenPackage.ps1 +++ b/Commands/Start-OpenPackage.ps1 @@ -23,9 +23,10 @@ function Start-OpenPackage { [CmdletBinding(PositionalBinding=$false)] param( # The path to an Open Package file, or a glob that matches multiple Open Package files. - [Parameter()] - [string] - $FilePath, + [Parameter(ValueFromRemainingArguments)] + [Alias('Arguments','Args','At','Url', 'AtUri', 'FilePath','Repository','Nuget')] + [PSObject[]] + $ArgumentList, # The root url. # By default, this will be automatically to a random local port. @@ -35,16 +36,15 @@ function Start-OpenPackage { # The input object. This can be provided to avoid loading a file from disk. [Parameter(ValueFromPipeline)] - [PSObject] + [Alias('Package')] + [PSObject[]] $InputObject, # The allowed http verbs. - [Parameter(ValueFromPipelineByPropertyName)] [string[]] $Allow = @('get', 'head'), # The content type map - [Parameter(ValueFromPipelineByPropertyName)] [Collections.IDictionary] $TypeMap = $( ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap @@ -64,7 +64,11 @@ function Start-OpenPackage { # If parts are smaller than this size, they will be streamed. # If parts are larger than this size, they will be handled in the background # (and may use a buffer of this size when accepting range requests) - [uint]$BufferSize = 16mb + [uint]$BufferSize = 16mb, + + # The lifespan of the server. + # If provided, will automatically stop the server after it's life is over. + [TimeSpan]$Lifespan ) begin { @@ -76,7 +80,7 @@ function Start-OpenPackage { filter serverStatus { $errorCode = $_ $response.StatusCode = $errorCode - foreach ($pack in $package) { + foreach ($pack in $package) { if ($pack.PartExists("/$errorCode.html")) { "/$errorCode.html" | servePart continue nextRequest @@ -275,8 +279,16 @@ function Start-OpenPackage { $acceptableTypes = @($request.Headers['Accept'] -split ',') Write-Host "Accepts $($request.Headers['Accept'])" -ForegroundColor Cyan $partStream = $packagePart.GetStream('Open', 'Read') - if ($packagePart.ContentType -eq 'text/markdown' -and - $acceptableTypes[0] -ne 'text/markdown') { + if ( + ( + $packagePart.ContentType -eq 'text/markdown' -or + $packagePart.Uri -match '(?>\.md|\.markdown)$' + ) -and + ( + $acceptableTypes[0] -ne 'text/markdown' -and + $request.Headers['Content-Type'] -ne 'text/markdown' + ) + ) { $streamReader = [IO.StreamReader]::new($partStream, [Text.Encoding]::UTF8) @@ -293,9 +305,7 @@ function Start-OpenPackage { return } - - Write-Host "Now Serving $($request.HttpMethod) $uriPart as $($response.ContentType)" -ForegroundColor Cyan - + Write-Host "$($request.HttpMethod) $uriPart $($response.ContentType)" -ForegroundColor Cyan if ($partStream.Length -lt $BufferSize) { $partStream.CopyTo($response.OutputStream) @@ -376,12 +386,19 @@ function Start-OpenPackage { # declare some inner functions to help serve + $ServerStartTime = [DateTime]::Now + # and start listening - :nextRequest while ($httpListener.IsListening) { + :nextRequest while ($httpListener.IsListening) { $getContextAsync = $httpListener.GetContextAsync() # wait in short increments to minimize CPU impact and stay snappy while (-not $getContextAsync.Wait(23)) { - + # while we're waiting, check our lifespan + if ($Lifespan -and ( + ($ServerStartTime + $Lifespan) -ge [DateTime]::Now + )) { + $httpListener.Stop() + } } # Get our listener context $context = $getContextAsync.Result @@ -499,7 +516,7 @@ function Start-OpenPackage { $uriPart = ($request.Url.LocalPath -replace '/$') + '/' if ($uriPart -match '/$') { - Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] $($request.HttpMethod) $($request.Url) Missing index, generating" + Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] $($request.HttpMethod) $($request.Url) Missing index, generating" $listing = $pack.ShowTreeHtml([Regex]::Escape($request.Url.LocalPath)) if ($listing -is [string]) { $response.ContentType = 'text/html' @@ -508,7 +525,7 @@ function Start-OpenPackage { $response.Close() } - continue nextRequest + continue nextRequest } else { Write-Host -ForegroundColor Cyan "[$($requestTime.ToString('o'))] Marco $($request.HttpMethod) $($request.Url)" @@ -520,47 +537,129 @@ function Start-OpenPackage { } $generateEvent = [Runspace]::DefaultRunspace.Events.GenerateEvent } - process { - # Get our package - $package = - if ($inputObject -is [IO.Packaging.Package]) { - $inputObject - } else { - Get-OpenPackage -FilePath $FilePath + end { + # Rapidly collect all pipeline input + $allInput = @($input) + + # Get our packages + # Each server can have any number of packages + # The order packages are defined is the order they are resolved + # This allows us to have any number of layers, in any order we want. + $packages = @( + # First up, lets process our input objects + # (piped in objects come first) + $remainingInput = @() + foreach ($in in $allInput) { + # Anything that is a package works + if ($in -is [IO.Packaging.Package]) { + $in + } + # so does anything that has a .Package property + elseif ( + $in.Package -is [IO.Packaging.Package] + ) { + $in.Package + } + # anything else we will pipe to Get-OpenPackage + else + { + $remainingInput += $in + } + } + # Now lets check a bound -InputObject + # If piped in, this will potentially be a duplicated + # (because `$InputObject` will contain the last bound value) + foreach ($in in $InputObject) { + # Skip any input we already have + if ($allInput -contains $in) { + continue + } + # If the -InputObject was a package + if ($in -is [IO.Packaging.Package]) { + $in # this works + } + # Otherwise, if the -InputObject has a .Package + elseif ( + $in.Package -is [IO.Packaging.Package] -and + # and it is not a package we already have collected + ($allInput.Package -notcontains $in.Package) + ) { + # then .Package works. + $in.Package + } + # Otherwise, we will pipe remaining input to Get-OpenPackage + elseif ($remainingInput -notcontains $in) { + $remainingInput += $in + } + } + # If there was remaining input + if ($remainingInput) { + # pipe it to Get-OpenPackage + $remainingInput | Get-OpenPackage @ArgumentList } + # If we had arguments, + elseif ($ArgumentList) { + # call Get-OpenPackage. + Get-OpenPackage @ArgumentList + } + ) - # and return if we could not + # Now we have a list of all of of potential packages + # Let's make one last pass thru for safety and sanity + $package = @( + # and include only the packages + foreach ($pack in $packages) { + if ($pack -is [IO.Packaging.Package]) { + $pack + } + } + ) + + # If we have no actual packages, return. if (-not $package) { return } - # Create a listener + # Now that we know _what_ we're serving, + # create a server by creating an http listener. $httpListener = [Net.HttpListener]::new() + # and adding the root url prefix $httpListener.Prefixes.Add($RootUrl) # Create an IO object to populate the background runspace + # By using an IO object, we can more easily communicate between runspaces. $IO = [Ordered]@{ HttpListener = $httpListener Package = $package ParentRunspace = [Runspace]::DefaultRunspace } + $PSBoundParameters + # If the IO does not have a typemap if (-not $io.TypeMap) { + # copy the typemap $io.TypeMap = $TypeMap - } + } + # If the IO does not have an -Allow if (-not $io.Allow) { + # use the default values for Allow. $io.Allow = $Allow } + # If the IO does not have a bufferSize if (-not $io.BufferSize) { + # use the default buffer size $io.BufferSize = $BufferSize } + # Now we're almost ready to serve, + # it's time to send an event. + # We need to prepare our message data with the relevant info $messageData = [Ordered]@{ RootUrl = $RootUrl Package = $package HttpListener = $httpListener InvocationInfo = $MyInvocation Command = $MyInvocation.MyCommand + IO = $IO } # Generate an event @@ -589,24 +688,38 @@ function Start-OpenPackage { return } - $httpListener.Start() - + # Now let's start our listener + try { + $IO.HttpListener.Start() + } catch { + # if we could not, return + $PSCmdlet.WriteError($_) + return + } + + # If that worked, if ($?) { + # write a warning. + # This serves two purposes: + # 1. It lets people know that a server is running + # 2. It gives people something a link to click. Write-Warning "Listening on $rootUrl" } + # If the server is invokable if ($Invokable) { + # Write one more warning, with some exclamation points + # to get the seriousness across. Write-Warning "! ! ! Invokable on $rootUrl ! ! !" } + # If there was no package identifier if (-not $package.PackageProperties.Identifier) { + # write another warning. Write-Warning "No Package Identifier" } - - if (-not $site) { - $site = [Ordered]@{} - } + # Get ready to import our own module. $IO.ImportModule = @( if ($myInvocation.MyCommand.Module) { "$($MyInvocation.MyCommand.Module | Split-Path)" @@ -615,7 +728,10 @@ function Start-OpenPackage { } ) + # Remove the trailing slash from the root url + # (prefixes require it, but it makes adding paths more annoying) $RootUrl = $RootUrl -replace '/$' + # Prepare our parameters for Start-ThreadJob $JobParameters = [Ordered]@{ ScriptBlock=$JobDefinition ArgumentList=$IO @@ -624,16 +740,16 @@ function Start-OpenPackage { InitializationScript = $InitializationScript } - # Start a thread job + # Start a thread job and add our properties $startedJob = Start-ThreadJob @JobParameters | Add-Member NoteProperty IO $IO -Force -PassThru | Add-Member NoteProperty HttpListener $httpListener -Force -PassThru | Add-Member NoteProperty Package $package -Force -PassThru | Add-Member NoteProperty Url $RootUrl -Force -PassThru - $null = New-Event -SourceIdentifier Start-OpenPackage -Sender $MyInvocation.MyCommand -EventArguments $startedJob -MessageData $IO - + # Decorate our return + $startedJob.pstypenames.add('OpenPackage.Server') + # and output our server $startedJob - } } \ No newline at end of file From 199706c004a01a2b2bbc3c0e6b02223eda064157 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 16:13:25 -0700 Subject: [PATCH 418/724] feat: `Get-OpenPackage` ( Fixes #2 ) Passing thru packages --- Commands/Get-OpenPackage.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 0d81960..2b89cf8 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -462,6 +462,12 @@ function Get-OpenPackage } $arg = $ArgumentList[$argNumber] + if ($arg -is [IO.Packaging.Package]) { + if ($packages -notcontains $arg) { + $packages += $arg + } + continue nextArgument + } # If it is a string, path info, or uri if ($arg -is [Management.Automation.PSModuleInfo]) { $modulePackage = Get-OpenPackage -Module $arg @namedParameters @@ -470,7 +476,7 @@ function Get-OpenPackage } # and continue to the next argument. continue nextArgument - } + } if ($arg -is [Collections.IDictionary]) { $packages += & $myCommandName -Dictionary $arg @namedParameters @@ -579,7 +585,6 @@ function Get-OpenPackage } #endregion Open Package in At Syntax - #region Open Package from Repository if ($Repository) { $NamedParameters['Repository'] = $Repository From 92c9a284bae76f079e984c853b4950906baa5ce9 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 16:33:40 -0700 Subject: [PATCH 419/724] feat: `OpenPackage.GetUrl` ( Fixes #119, re #150 ) Improving fault tolerance to support @, fixing content length --- Types/OpenPackage/GetUrl.ps1 | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Types/OpenPackage/GetUrl.ps1 b/Types/OpenPackage/GetUrl.ps1 index c0cb9f4..72fa8f8 100644 --- a/Types/OpenPackage/GetUrl.ps1 +++ b/Types/OpenPackage/GetUrl.ps1 @@ -130,15 +130,12 @@ foreach ($uri in $url) { } # At this point lets just poke at the uri and make a package. - $WebResponse = try { - Invoke-WebRequest -Uri $Uri + try { + $WebResponse = Invoke-WebRequest -Uri $Uri } catch { - $_ - } - if ($WebResponse -is [Management.Automation.ErrorRecord]) { - Write-Verbose "$uri - $webResponse" + Write-Verbose "$uri - $_" continue - } + } # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and @@ -200,7 +197,7 @@ foreach ($uri in $url) { # If the response was a byte array if ($WebResponse.Content -is [byte[]]) { # write that to the stream - $newStream.Write($WebResponse.Content, 0, $WebResponse.Length) + $newStream.Write($WebResponse.Content, 0, $WebResponse.Content.Length) } else { # otherwise, turn the stringified content into bytes $buffer = $OutputEncoding.GetBytes("$($WebResponse.Content)") From 06eab9ebff6998549a5cde6ec4973b87eb91d9a5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 23:33:58 +0000 Subject: [PATCH 420/724] feat: `OpenPackage.GetUrl` ( Fixes #119, re #150 ) Improving fault tolerance to support @, fixing content length --- OP.types.ps1xml | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 6dd6ea5..a574391 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1728,15 +1728,12 @@ foreach ($uri in $url) { } # At this point lets just poke at the uri and make a package. - $WebResponse = try { - Invoke-WebRequest -Uri $Uri + try { + $WebResponse = Invoke-WebRequest -Uri $Uri } catch { - $_ - } - if ($WebResponse -is [Management.Automation.ErrorRecord]) { - Write-Verbose "$uri - $webResponse" + Write-Verbose "$uri - $_" continue - } + } # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and @@ -1798,7 +1795,7 @@ foreach ($uri in $url) { # If the response was a byte array if ($WebResponse.Content -is [byte[]]) { # write that to the stream - $newStream.Write($WebResponse.Content, 0, $WebResponse.Length) + $newStream.Write($WebResponse.Content, 0, $WebResponse.Content.Length) } else { # otherwise, turn the stringified content into bytes $buffer = $OutputEncoding.GetBytes("$($WebResponse.Content)") @@ -5415,15 +5412,12 @@ foreach ($uri in $url) { } # At this point lets just poke at the uri and make a package. - $WebResponse = try { - Invoke-WebRequest -Uri $Uri + try { + $WebResponse = Invoke-WebRequest -Uri $Uri } catch { - $_ - } - if ($WebResponse -is [Management.Automation.ErrorRecord]) { - Write-Verbose "$uri - $webResponse" + Write-Verbose "$uri - $_" continue - } + } # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and @@ -5485,7 +5479,7 @@ foreach ($uri in $url) { # If the response was a byte array if ($WebResponse.Content -is [byte[]]) { # write that to the stream - $newStream.Write($WebResponse.Content, 0, $WebResponse.Length) + $newStream.Write($WebResponse.Content, 0, $WebResponse.Content.Length) } else { # otherwise, turn the stringified content into bytes $buffer = $OutputEncoding.GetBytes("$($WebResponse.Content)") @@ -9102,15 +9096,12 @@ foreach ($uri in $url) { } # At this point lets just poke at the uri and make a package. - $WebResponse = try { - Invoke-WebRequest -Uri $Uri + try { + $WebResponse = Invoke-WebRequest -Uri $Uri } catch { - $_ - } - if ($WebResponse -is [Management.Automation.ErrorRecord]) { - Write-Verbose "$uri - $webResponse" + Write-Verbose "$uri - $_" continue - } + } # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and @@ -9172,7 +9163,7 @@ foreach ($uri in $url) { # If the response was a byte array if ($WebResponse.Content -is [byte[]]) { # write that to the stream - $newStream.Write($WebResponse.Content, 0, $WebResponse.Length) + $newStream.Write($WebResponse.Content, 0, $WebResponse.Content.Length) } else { # otherwise, turn the stringified content into bytes $buffer = $OutputEncoding.GetBytes("$($WebResponse.Content)") From 94ea41194085d2eb5b38379a8b040d7c03d17d72 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 16:44:43 -0700 Subject: [PATCH 421/724] feat: `OpenPackage.get_Eleventy` ( Fixes #90 ) Using readers --- Types/OpenPackage/get_Eleventy.ps1 | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Types/OpenPackage/get_Eleventy.ps1 b/Types/OpenPackage/get_Eleventy.ps1 index 75ab7d8..c8b2fd4 100644 --- a/Types/OpenPackage/get_Eleventy.ps1 +++ b/Types/OpenPackage/get_Eleventy.ps1 @@ -12,11 +12,15 @@ .LINK https://www.11ty.dev/docs/config/ #> -[OutputType([xml])] param() $partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' - -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + if ($part.Reader) { + $part.Read() + } else { + $part + } + } +} From 838b4190455aa8b483edd81c1c8e8ab06b1e2773 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 23:45:03 +0000 Subject: [PATCH 422/724] feat: `OpenPackage.get_Eleventy` ( Fixes #90 ) Using readers --- OP.types.ps1xml | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index a574391..6b0105c 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2651,14 +2651,18 @@ foreach ($part in $this.GetParts()) { .LINK https://www.11ty.dev/docs/config/ #> -[OutputType([xml])] param() $partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' - -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + if ($part.Reader) { + $part.Read() + } else { + $part + } + } +} @@ -6335,14 +6339,18 @@ foreach ($part in $this.GetParts()) { .LINK https://www.11ty.dev/docs/config/ #> -[OutputType([xml])] param() $partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' - -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + if ($part.Reader) { + $part.Read() + } else { + $part + } + } +} @@ -10019,14 +10027,18 @@ foreach ($part in $this.GetParts()) { .LINK https://www.11ty.dev/docs/config/ #> -[OutputType([xml])] param() $partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' - -$this.GetContent(@( - $this.FileList -match $partPattern -)) +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + if ($part.Reader) { + $part.Read() + } else { + $part + } + } +} From 29e879326aac7f2935a1b187d5e0743c3cac6b91 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 17:31:06 -0700 Subject: [PATCH 423/724] feat: `Uninstall-OpenPackage` ( Fixes #151 ) --- Commands/Uninstall-OpenPackage.ps1 | 92 ++++++++++++++++++++++++++++++ OP.psd1 | 3 +- Types/OpenPackage/get_Lexicon.ps1 | 14 +++-- 3 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 Commands/Uninstall-OpenPackage.ps1 diff --git a/Commands/Uninstall-OpenPackage.ps1 b/Commands/Uninstall-OpenPackage.ps1 new file mode 100644 index 0000000..c42d3bc --- /dev/null +++ b/Commands/Uninstall-OpenPackage.ps1 @@ -0,0 +1,92 @@ +function Uninstall-OpenPackage +{ + <# + .SYNOPSIS + Uninstalls OpenPackages + .DESCRIPTION + Uninstalls one or more OpenPackages. + .NOTES + Because installed packages are just directories, + this is a very light wrapper of Remove-Item. + + If you provide an -Identifier, + .LINK + Install-OpenPackage + .LINK + Remove-Item + #> + [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] + [Alias( + 'Uninstall-OP', 'usop', 'usOpenPackage' + )] + param( + # The package identifier. + # If this is a fully qualified directory or file name, it will be removed. + [Parameter(Mandatory,ParameterSetName='Identifier', ValueFromPipelineByPropertyName)] + [string[]] + $Identifier, + + # The package version + [Parameter(ParameterSetName='Identifier',ValueFromPipelineByPropertyName)] + [string[]] + $Version, + + # The direct path to any number of packages. + # This will Remove-Item these paths + [Parameter(ParameterSetName='PackagePath',ValueFromPipelineByPropertyName)] + [string[]] + $PackagePath, + + # The root location where packages are stored. + # By default, this will be the locations specified in `$env:OpenPackagePath` + [string[]] + $PackageRoot = @( + if ($IsLinux -or $IsMacOS) { + $env:OpenPackagePath -split ':' + } else { + $env:OpenPackagePath -split ';' + } + ) + ) + + process { + # Get any potential paths in any package root + $potentialPaths = @(foreach ($packageLocation in $PackageRoot) { + # We have to have at least one identifier + foreach ($packageId in $PSBoundParameters['Identifier']) { + # If version was supplied + if ($version) { + foreach ($packageVersion in $version) { + # look for each potential version. + Join-Path $packageLocation "$packageId/$packageVersion" + } + } else { + # If no version was supplied, look for the package id. + Join-Path $packageLocation $packageId + } + } + }) + + # If we have provided a package path + if ($psBoundParameters['PackagePath']) { + # ignore any versioned paths and use that directly. + $potentialPaths = $psBoundParameters['PackagePath'] + } + + + # Check each potential path + foreach ($potentialPath in $potentialPaths) { + # If the path does not exist, continue + if (-not (Test-Path $potentialPath)) { continue } + if ($WhatIfPreference) { # If -WhatIf was passed, + Get-Item $potentialPath #output the item + continue # and continue. + } + # If we confirmed we want to remove it + if ($PSCmdlet.ShouldProcess("Uninstall $potentialPath")) { + # call Remove-Item with `-Recurse` and `-Force` + Remove-Item -Recurse -Force -Path $potentialPath + } + } + } +} \ No newline at end of file diff --git a/OP.psd1 b/OP.psd1 index de05dc4..e81330a 100644 --- a/OP.psd1 +++ b/OP.psd1 @@ -83,6 +83,7 @@ FunctionsToExport = @( 'Set-OpenPackage' 'Split-OpenPackage' 'Start-OpenPackage' + 'Uninstall-OpenPackage' 'Write-OpenPackage' ) @@ -125,7 +126,7 @@ AliasesToExport = @( 'Start-OP','stOpenPackage' - + 'Uninstall-OP', 'usop', 'usOpenPackage' ) # DSC resources to export from this module diff --git a/Types/OpenPackage/get_Lexicon.ps1 b/Types/OpenPackage/get_Lexicon.ps1 index 33cd07e..dcf9bf0 100644 --- a/Types/OpenPackage/get_Lexicon.ps1 +++ b/Types/OpenPackage/get_Lexicon.ps1 @@ -19,21 +19,27 @@ $allLexicons = [Ordered]@{} # Get every part foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG + # and ignore any part that is not json. if ($part.Uri -notmatch '\.json$') { continue } + # Also ignore any package-lock files if ($part.Uri -match 'package-lock') { continue } - + + # If there is no reader, continue if (-not $part.Reader) { continue } try { + # Try to read our data $partData = $part.Read() + # ignore any arrays if ($partData -is [Object[]]) { continue } + # If the data has a .lexicon, .id, and .defs if ($partData.lexicon -and $partData.id -and $partData.defs ) { + # store it in our lexicons table. $allLexicons[$partData.id] = $partData } } catch { @@ -41,5 +47,5 @@ foreach ($part in $this.GetParts()) { continue } } -return $allLexicons -# We are done. \ No newline at end of file +# return all of the lexicons we found. +return $allLexicons \ No newline at end of file From c12e7203bd934633945e3dea7622f14827567d2e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 00:31:26 +0000 Subject: [PATCH 424/724] feat: `Uninstall-OpenPackage` ( Fixes #151 ) --- OP.types.ps1xml | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 6b0105c..7f106de 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3043,21 +3043,27 @@ $allLexicons = [Ordered]@{} # Get every part foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG + # and ignore any part that is not json. if ($part.Uri -notmatch '\.json$') { continue } + # Also ignore any package-lock files if ($part.Uri -match 'package-lock') { continue } - + + # If there is no reader, continue if (-not $part.Reader) { continue } try { + # Try to read our data $partData = $part.Read() + # ignore any arrays if ($partData -is [Object[]]) { continue } + # If the data has a .lexicon, .id, and .defs if ($partData.lexicon -and $partData.id -and $partData.defs ) { + # store it in our lexicons table. $allLexicons[$partData.id] = $partData } } catch { @@ -3065,8 +3071,8 @@ foreach ($part in $this.GetParts()) { continue } } +# return all of the lexicons we found. return $allLexicons -# We are done. @@ -6731,21 +6737,27 @@ $allLexicons = [Ordered]@{} # Get every part foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG + # and ignore any part that is not json. if ($part.Uri -notmatch '\.json$') { continue } + # Also ignore any package-lock files if ($part.Uri -match 'package-lock') { continue } - + + # If there is no reader, continue if (-not $part.Reader) { continue } try { + # Try to read our data $partData = $part.Read() + # ignore any arrays if ($partData -is [Object[]]) { continue } + # If the data has a .lexicon, .id, and .defs if ($partData.lexicon -and $partData.id -and $partData.defs ) { + # store it in our lexicons table. $allLexicons[$partData.id] = $partData } } catch { @@ -6753,8 +6765,8 @@ foreach ($part in $this.GetParts()) { continue } } +# return all of the lexicons we found. return $allLexicons -# We are done. @@ -10419,21 +10431,27 @@ $allLexicons = [Ordered]@{} # Get every part foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG + # and ignore any part that is not json. if ($part.Uri -notmatch '\.json$') { continue } + # Also ignore any package-lock files if ($part.Uri -match 'package-lock') { continue } - + + # If there is no reader, continue if (-not $part.Reader) { continue } try { + # Try to read our data $partData = $part.Read() + # ignore any arrays if ($partData -is [Object[]]) { continue } + # If the data has a .lexicon, .id, and .defs if ($partData.lexicon -and $partData.id -and $partData.defs ) { + # store it in our lexicons table. $allLexicons[$partData.id] = $partData } } catch { @@ -10441,8 +10459,8 @@ foreach ($part in $this.GetParts()) { continue } } +# return all of the lexicons we found. return $allLexicons -# We are done. From 7ad3c5986ad42bf497556a6fce038413ffc7d36c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 19:21:33 -0700 Subject: [PATCH 425/724] feat: `OpenPackage.get_Eponym` ( Fixes #152 ) --- Types/OpenPackage/get_Eponym.ps1 | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Types/OpenPackage/get_Eponym.ps1 diff --git a/Types/OpenPackage/get_Eponym.ps1 b/Types/OpenPackage/get_Eponym.ps1 new file mode 100644 index 0000000..bccfbe3 --- /dev/null +++ b/Types/OpenPackage/get_Eponym.ps1 @@ -0,0 +1,41 @@ +<# +.SYNOPSIS + Gets all Eponyms in a Package +.DESCRIPTION + Gets all Eponyms within an OpenPackage. + + Eponyms are files whose name matches their directory. + + For example: + + `/foo/foo.ps1` is an eponym + `/foo/foo.md` is an eponym + `/foo/bar.ps1` is not an eponym + + If a package has an an identifier, + any files whose name matches this identifier will be considered an eponym. +.NOTES + Multiple eponyms may exist for a given path. + + Anything before the first period `.` will be considered the name of the file. +#> +param() + +if (-not $this.GetParts) { return } +foreach ($part in $this.GetParts()) { + $partSegments = @($part.Uri -split '/+' -ne '') + if ($partSegments.Count -eq 1) { + if ($partSegments[0] -replace '\..+$' -eq $this.Identifier) { + $part + continue + } + } + if ($partSegments.Count -ge 2) { + if ($partSegments[-2] -eq + ($partSegments[-1] -replace '\..+$') + ) { + $part + continue + } + } +} From 6d179906ea813d6ab2a8b99cf297c03f1e67e203 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 02:21:51 +0000 Subject: [PATCH 426/724] feat: `OpenPackage.get_Eponym` ( Fixes #152 ) --- OP.types.ps1xml | 141 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 7f106de..786dcd9 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2666,6 +2666,53 @@ foreach ($part in $this.GetParts()) { + + Eponym + + <# +.SYNOPSIS + Gets all Eponyms in a Package +.DESCRIPTION + Gets all Eponyms within an OpenPackage. + + Eponyms are files whose name matches their directory. + + For example: + + `/foo/foo.ps1` is an eponym + `/foo/foo.md` is an eponym + `/foo/bar.ps1` is not an eponym + + If a package has an an identifier, + any files whose name matches this identifier will be considered an eponym. +.NOTES + Multiple eponyms may exist for a given path. + + Anything before the first period `.` will be considered the name of the file. +#> +param() + +if (-not $this.GetParts) { return } +foreach ($part in $this.GetParts()) { + $partSegments = @($part.Uri -split '/+' -ne '') + if ($partSegments.Count -eq 1) { + if ($partSegments[0] -replace '\..+$' -eq $this.Identifier) { + $part + continue + } + } + if ($partSegments.Count -ge 2) { + if ($partSegments[-2] -eq + ($partSegments[-1] -replace '\..+$') + ) { + $part + continue + } + } +} + + + FileContentType @@ -6360,6 +6407,53 @@ foreach ($part in $this.GetParts()) { + + Eponym + + <# +.SYNOPSIS + Gets all Eponyms in a Package +.DESCRIPTION + Gets all Eponyms within an OpenPackage. + + Eponyms are files whose name matches their directory. + + For example: + + `/foo/foo.ps1` is an eponym + `/foo/foo.md` is an eponym + `/foo/bar.ps1` is not an eponym + + If a package has an an identifier, + any files whose name matches this identifier will be considered an eponym. +.NOTES + Multiple eponyms may exist for a given path. + + Anything before the first period `.` will be considered the name of the file. +#> +param() + +if (-not $this.GetParts) { return } +foreach ($part in $this.GetParts()) { + $partSegments = @($part.Uri -split '/+' -ne '') + if ($partSegments.Count -eq 1) { + if ($partSegments[0] -replace '\..+$' -eq $this.Identifier) { + $part + continue + } + } + if ($partSegments.Count -ge 2) { + if ($partSegments[-2] -eq + ($partSegments[-1] -replace '\..+$') + ) { + $part + continue + } + } +} + + + FileContentType @@ -10054,6 +10148,53 @@ foreach ($part in $this.GetParts()) { + + Eponym + + <# +.SYNOPSIS + Gets all Eponyms in a Package +.DESCRIPTION + Gets all Eponyms within an OpenPackage. + + Eponyms are files whose name matches their directory. + + For example: + + `/foo/foo.ps1` is an eponym + `/foo/foo.md` is an eponym + `/foo/bar.ps1` is not an eponym + + If a package has an an identifier, + any files whose name matches this identifier will be considered an eponym. +.NOTES + Multiple eponyms may exist for a given path. + + Anything before the first period `.` will be considered the name of the file. +#> +param() + +if (-not $this.GetParts) { return } +foreach ($part in $this.GetParts()) { + $partSegments = @($part.Uri -split '/+' -ne '') + if ($partSegments.Count -eq 1) { + if ($partSegments[0] -replace '\..+$' -eq $this.Identifier) { + $part + continue + } + } + if ($partSegments.Count -ge 2) { + if ($partSegments[-2] -eq + ($partSegments[-1] -replace '\..+$') + ) { + $part + continue + } + } +} + + + FileContentType From 85ed33016c923d82fe8b9ebba7691ee0daf05543 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 19:27:41 -0700 Subject: [PATCH 427/724] feat: `OpenPackage.Part.get_IsEponym` ( Fixes #153 ) --- Types/OpenPackage.Part/get_IsEponym.ps1 | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Types/OpenPackage.Part/get_IsEponym.ps1 diff --git a/Types/OpenPackage.Part/get_IsEponym.ps1 b/Types/OpenPackage.Part/get_IsEponym.ps1 new file mode 100644 index 0000000..5233acc --- /dev/null +++ b/Types/OpenPackage.Part/get_IsEponym.ps1 @@ -0,0 +1,37 @@ +<# +.SYNOPSIS + Determines if a part is an eponym +.DESCRIPTION + Eponyms are files whose name matches their directory. + + For example: + + `/foo/foo.ps1` is an eponym + `/foo/foo.md` is an eponym + `/foo/bar.ps1` is not an eponym + + If a package has an an identifier, + any files whose name matches this identifier will be considered an eponym. +.NOTES + Multiple eponyms may exist for a given path. + + Anything before the first period `.` will be considered the name of the file. +#> +param() +$part = $this + +$partSegments = @($part.Uri -split '/+' -ne '') + +if ($partSegments.Count -eq 1 -and $this.Package.Identifier) { + if ($partSegments[0] -replace '\..+$' -eq $this.Package.Identifier) { + return $true + } +} + +if ($partSegments.Count -ge 2) { + if ($partSegments[-2] -eq + ($partSegments[-1] -replace '\..+$') + ) { + return $true + } +} \ No newline at end of file From 48e8f3e6cfd8558d8c53de8da314d3e1a192b6eb Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 02:28:11 +0000 Subject: [PATCH 428/724] feat: `OpenPackage.Part.get_IsEponym` ( Fixes #153 ) --- OP.types.ps1xml | 126 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 786dcd9..88eb721 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -12702,6 +12702,48 @@ $this.WriteText($text, $Option) $this.GetHash().Hash + + IsEponym + + <# +.SYNOPSIS + Determines if a part is an eponym +.DESCRIPTION + Eponyms are files whose name matches their directory. + + For example: + + `/foo/foo.ps1` is an eponym + `/foo/foo.md` is an eponym + `/foo/bar.ps1` is not an eponym + + If a package has an an identifier, + any files whose name matches this identifier will be considered an eponym. +.NOTES + Multiple eponyms may exist for a given path. + + Anything before the first period `.` will be considered the name of the file. +#> +param() +$part = $this + +$partSegments = @($part.Uri -split '/+' -ne '') + +if ($partSegments.Count -eq 1 -and $this.Package.Identifier) { + if ($partSegments[0] -replace '\..+$' -eq $this.Package.Identifier) { + return $true + } +} + +if ($partSegments.Count -ge 2) { + if ($partSegments[-2] -eq + ($partSegments[-1] -replace '\..+$') + ) { + return $true + } +} + + Reader @@ -14204,6 +14246,48 @@ $this.WriteText($text, $Option) $this.GetHash().Hash + + IsEponym + + <# +.SYNOPSIS + Determines if a part is an eponym +.DESCRIPTION + Eponyms are files whose name matches their directory. + + For example: + + `/foo/foo.ps1` is an eponym + `/foo/foo.md` is an eponym + `/foo/bar.ps1` is not an eponym + + If a package has an an identifier, + any files whose name matches this identifier will be considered an eponym. +.NOTES + Multiple eponyms may exist for a given path. + + Anything before the first period `.` will be considered the name of the file. +#> +param() +$part = $this + +$partSegments = @($part.Uri -split '/+' -ne '') + +if ($partSegments.Count -eq 1 -and $this.Package.Identifier) { + if ($partSegments[0] -replace '\..+$' -eq $this.Package.Identifier) { + return $true + } +} + +if ($partSegments.Count -ge 2) { + if ($partSegments[-2] -eq + ($partSegments[-1] -replace '\..+$') + ) { + return $true + } +} + + Reader @@ -15706,6 +15790,48 @@ $this.WriteText($text, $Option) $this.GetHash().Hash + + IsEponym + + <# +.SYNOPSIS + Determines if a part is an eponym +.DESCRIPTION + Eponyms are files whose name matches their directory. + + For example: + + `/foo/foo.ps1` is an eponym + `/foo/foo.md` is an eponym + `/foo/bar.ps1` is not an eponym + + If a package has an an identifier, + any files whose name matches this identifier will be considered an eponym. +.NOTES + Multiple eponyms may exist for a given path. + + Anything before the first period `.` will be considered the name of the file. +#> +param() +$part = $this + +$partSegments = @($part.Uri -split '/+' -ne '') + +if ($partSegments.Count -eq 1 -and $this.Package.Identifier) { + if ($partSegments[0] -replace '\..+$' -eq $this.Package.Identifier) { + return $true + } +} + +if ($partSegments.Count -ge 2) { + if ($partSegments[-2] -eq + ($partSegments[-1] -replace '\..+$') + ) { + return $true + } +} + + Reader From 4fd829c52d246e65128fc23ec3800a121cf3be15 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 19:28:36 -0700 Subject: [PATCH 429/724] feat: `OpenPackage.Part.get_IsEponym` ( Fixes #153 ) Explicitly returning false if not found --- Types/OpenPackage.Part/get_IsEponym.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/get_IsEponym.ps1 b/Types/OpenPackage.Part/get_IsEponym.ps1 index 5233acc..8004e6c 100644 --- a/Types/OpenPackage.Part/get_IsEponym.ps1 +++ b/Types/OpenPackage.Part/get_IsEponym.ps1 @@ -34,4 +34,6 @@ if ($partSegments.Count -ge 2) { ) { return $true } -} \ No newline at end of file +} + +return $false \ No newline at end of file From 8cf045fecee18beb549f5bb44749ea643c28a9b6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 02:28:54 +0000 Subject: [PATCH 430/724] feat: `OpenPackage.Part.get_IsEponym` ( Fixes #153 ) Explicitly returning false if not found --- OP.types.ps1xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 88eb721..02a6e53 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -12742,6 +12742,8 @@ if ($partSegments.Count -ge 2) { return $true } } + +return $false @@ -14286,6 +14288,8 @@ if ($partSegments.Count -ge 2) { return $true } } + +return $false @@ -15830,6 +15834,8 @@ if ($partSegments.Count -ge 2) { return $true } } + +return $false From fa2c27ae685729bc40c47b94fa27bee1c3aebe04 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 21 Mar 2026 19:30:16 -0700 Subject: [PATCH 431/724] feat: `OpenPackage.Part.get_Name` ( Fixes #154 ) --- Types/OpenPackage.Part/get_Name.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Types/OpenPackage.Part/get_Name.ps1 diff --git a/Types/OpenPackage.Part/get_Name.ps1 b/Types/OpenPackage.Part/get_Name.ps1 new file mode 100644 index 0000000..bccba66 --- /dev/null +++ b/Types/OpenPackage.Part/get_Name.ps1 @@ -0,0 +1,10 @@ +<# +.SYNOPSIS + Gets the part name +.DESCRIPTION + Gets the part name. + + This is is anything before the first period (`.`). +#> +param() +@($this.Uri -split '/' -ne '')[-1] -replace '\..+?$' \ No newline at end of file From 24a8eda9b5cc3ef7813de9c08c4f888c4c7a37a3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 02:30:35 +0000 Subject: [PATCH 432/724] feat: `OpenPackage.Part.get_Name` ( Fixes #154 ) --- OP.types.ps1xml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 02a6e53..3d1a4b8 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -12746,6 +12746,21 @@ if ($partSegments.Count -ge 2) { return $false + + Name + + <# +.SYNOPSIS + Gets the part name +.DESCRIPTION + Gets the part name. + + This is is anything before the first period (`.`). +#> +param() +@($this.Uri -split '/' -ne '')[-1] -replace '\..+?$' + + Reader @@ -14292,6 +14307,21 @@ if ($partSegments.Count -ge 2) { return $false + + Name + + <# +.SYNOPSIS + Gets the part name +.DESCRIPTION + Gets the part name. + + This is is anything before the first period (`.`). +#> +param() +@($this.Uri -split '/' -ne '')[-1] -replace '\..+?$' + + Reader @@ -15838,6 +15868,21 @@ if ($partSegments.Count -ge 2) { return $false + + Name + + <# +.SYNOPSIS + Gets the part name +.DESCRIPTION + Gets the part name. + + This is is anything before the first period (`.`). +#> +param() +@($this.Uri -split '/' -ne '')[-1] -replace '\..+?$' + + Reader From c4fc495c9d649d7d1299adcc7f9a1ed867603af7 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 12:29:21 -0700 Subject: [PATCH 433/724] fix: Removing vestigial alias --- Types/OpenPackage/Alias.psd1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Types/OpenPackage/Alias.psd1 b/Types/OpenPackage/Alias.psd1 index 5007434..29c599c 100644 --- a/Types/OpenPackage/Alias.psd1 +++ b/Types/OpenPackage/Alias.psd1 @@ -1,7 +1,6 @@ @{ RemovePart = "DeletePart" - Lexicons = "Lexicon" - DestinationPath = 'InstallPath' + Lexicons = "Lexicon" '11ty' = "Eleventy" 'Underbar' = 'Underscore' '_' = 'Underscore' From e92b10676ae84703db0322e221db308e5f4ff2f1 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 19:29:42 +0000 Subject: [PATCH 434/724] fix: Removing vestigial alias --- OP.types.ps1xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 3d1a4b8..d3c96fd 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -23,10 +23,6 @@ 11ty Eleventy - - DestinationPath - InstallPath - Lexicons Lexicon @@ -3764,10 +3760,6 @@ FileList 11ty Eleventy - - DestinationPath - InstallPath - Lexicons Lexicon @@ -7505,10 +7497,6 @@ FileList 11ty Eleventy - - DestinationPath - InstallPath - Lexicons Lexicon From 71e8207036da7033b8e015ee54e4eb348a4da7db Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 13:55:04 -0700 Subject: [PATCH 435/724] feat: `Install-OpenPackage` ( Fixes #52 ) Opening up syntax by calling Get-OpenPackage. This allows easier and more flexible installation. --- Commands/Install-OpenPackage.ps1 | 231 ++++++++++++++++++++++++------- 1 file changed, 183 insertions(+), 48 deletions(-) diff --git a/Commands/Install-OpenPackage.ps1 b/Commands/Install-OpenPackage.ps1 index c33f43f..cd64c21 100644 --- a/Commands/Install-OpenPackage.ps1 +++ b/Commands/Install-OpenPackage.ps1 @@ -16,8 +16,30 @@ function Install-OpenPackage 'Expand-OpenPackage','Expand-OP', 'enop','enOpenPackage' )] param( - # The destination path. This should be a directory. - [Parameter(Position=0)] + # The arguments to Get-OpenPackage. + [Parameter(ValueFromRemainingArguments)] + [Alias('Arguments','Args','At','Url', 'AtUri', 'FilePath','Repository','Nuget')] + [PSObject[]] + $ArgumentList, + + <# + + The destination path. + + If provided, this should be a directory, but can be a file. + + If multiple packages will be installed and a -DestinationPath was provided, + all packages will be installed into that destination path. + + If no destination path is provided, + only packages with an identifier will be installed. + + Packages will install beneath the first `$env:OpenPackagePath`. + + If the package has a version, it will install into a versioned subdirectory. + + #> + [Parameter()] [ValidateNotNullOrEmpty()] [string] $DestinationPath, @@ -86,85 +108,195 @@ function Install-OpenPackage $PassThru ) - begin { - # We will be using Select-OpenPackage for filtering, so get a reference to it now. - $selectOpenPackage = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-OpenPackage','Function') - } + # We want to collect all piped input first, + # so we can use the implicit `end` block. + # And collect all the piped input by enumerating `$input`. + $allInput = @($input) + + # We will be using Select-OpenPackage for filtering, so get a reference to it now. + $selectOpenPackage = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-OpenPackage','Function') + + # Get our packages + # Each server can have any number of packages + # The order packages are defined is the order they are resolved + # This allows us to have any number of layers, in any order we want. + $packages = @( + # First up, lets process our input objects + # (piped in objects come first) + $remainingInput = @() + foreach ($in in $allInput) { + # Anything that is a package works + if ($in -is [IO.Packaging.Package]) { + $in + } + # so does anything that has a .Package property + elseif ( + $in.Package -is [IO.Packaging.Package] + ) { + $in.Package + } + # anything else we will pipe to Get-OpenPackage + else + { + $remainingInput += $in + } + } + # Now lets check a bound -InputObject + # If piped in, this will potentially be a duplicated + # (because `$InputObject` will contain the last bound value) + foreach ($in in $InputObject) { + # Skip any input we already have + if ($allInput -contains $in) { + continue + } + # If the -InputObject was a package + if ($in -is [IO.Packaging.Package]) { + $in # this works + } + # Otherwise, if the -InputObject has a .Package + elseif ( + $in.Package -is [IO.Packaging.Package] -and + # and it is not a package we already have collected + ($allInput.Package -notcontains $in.Package) + ) { + # then .Package works. + $in.Package + } + # Otherwise, we will pipe remaining input to Get-OpenPackage + elseif ($remainingInput -notcontains $in) { + $remainingInput += $in + } + } + # If there was remaining input + if ($remainingInput) { + # pipe it to Get-OpenPackage + $remainingInput | Get-OpenPackage @ArgumentList + } + # If we had arguments, + elseif ($ArgumentList) { + # call Get-OpenPackage. + Get-OpenPackage @ArgumentList + } + ) - process { - # Pass thru any input that is not a package. - if ($InputObject -isnot [IO.Packaging.Package]) { return $InputObject } + # Packages can only be installed once per execution of this function. + # So we will need to keep track of already installed packages + $alreadyInstalled = @() + + # We will also want to keep track of what we have cleared, so we can install layers. + $Cleared = @() - $package = $inputObject + # Keep track of any files we might overwrite + $existingFiles = @() + # Go over each package we may have + foreach ($package in $packages) { + # skip anything that is not a package + if ($package -isnot [IO.Packaging.Package]) { + continue + } + # or that has already been installed + if ($alreadyInstalled -contains $package) { + continue + } + # If no DestinationPath was provided if (-not $PSBoundParameters['DestinationPath']) { - if ((-not $package.Identifier) -or - (-not $env:OpenPackagePath) - ) { - Write-Error "Must provide -DestinationPath or have a package identifier and version" + # Check for $env:OpenPackagePath + if (-not $env:OpenPackagePath) { + # error out if missing. + Write-Error '$env:OpenPackagePath not defined' return - } else { + } + # If there is no identifier + if (-not $package.Identifier) { + # error out + Write-Error "Must provide -DestinationPath or have a package identifier" + return + } + + # Set the destionation path + $PSBoundParameters['DestinationPath'] = $destinationPath = + Join-Path ( + # based off of the $env:OpenPackagePath + @($env:OpenPackagePath -split $( + if (-not ($IsLinux -or $IsMacOS)) { ';' } + else { ':' } + ))[0] + ) $package.Identifier # and the identifier + + # If the package had a version + if ($package.Version) { + # put it within the versioned directory $PSBoundParameters['DestinationPath'] = $destinationPath = - Join-Path ( - @($env:OpenPackagePath -split $( - if (-not ($IsLinux -or $IsMacOS)) { ';' } - else { ':' } - ))[0] - ) $package.Identifier - - if ($package.Version) { - $PSBoundParameters['DestinationPath'] = $destinationPath = - Join-Path $DestinationPath $package.Version - } + Join-Path $DestinationPath $package.Version } } # Copy our parameters to Select-OpenPackage - $selectSplat = [Ordered]@{InputObject=$InputObject} + $selectSplat = [Ordered]@{InputObject=$package} foreach ($key in $PSBoundParameters.Keys) { if ($selectOpenPackage.Parameters[$key]) { $selectSplat[$key] = $PSBoundParameters[$key] } } - + # Get all of the package parts $inputParts = @(Select-OpenPackage @selectSplat) - # If we are not backing up - if (-not $NoBackup) { + # If we are backing up + if (-not $NoBackup) { + # copy the open package $copyOpenPackage = @(Copy-OpenPackage @selectSplat) - $copyOpenPackage | + # create a `.zip` + $zipCopy = $copyOpenPackage | Export-OpenPackage -DestinationPath ( $DestinationPath | Split-Path | - Join-Path -ChildPath "$($inputObject.PackageProperties.Identifier).zip" + Join-Path -ChildPath "$($package.PackageProperties.Identifier).zip" ) -Force - $copyOpenPackage | + # If -PassThru was passed + if ($passThru) { + $zipCopy # output that zip + } + + # create an `.op` + $opCopy = $copyOpenPackage | Export-OpenPackage -DestinationPath ( $DestinationPath | Split-Path | - Join-Path -ChildPath "$($inputObject.PackageProperties.Identifier).op" + Join-Path -ChildPath "$($package.PackageProperties.Identifier).op" ) -Force + + # If -PassThru was passed + if ($PassThru) { + $opCopy # output that `op` + } } - - - # and prepare our progress. + + # Now let's prepare our progress bars $total = $inputParts.Length $counter = 0 $Progress = [Ordered]@{ - Activity = "Expanding $($InputObject.PackageProperties.Identifier)" + Activity = "Expanding $($package.PackageProperties.Identifier)" Id = Get-Random } - - $existingFiles = @() + - if (Test-Path $DestinationPath) { + # If the destination path exist and has not been cleared + if ( + (Test-Path $DestinationPath) -and + $Cleared -notcontains $DestinationPath + ) { + # Clear it if we want to if ($Clear -and $psCmdlet.ShouldProcess("Clear $destinationPath")) { Remove-Item -ErrorAction Ignore -Path $DestinationPath -Recurse -Force:$Clear } else { - Write-Warning + # and warn if we do not. + Write-Warning "$DestinationPath exists. Use -Clear to clear the directory." } + # Add it to the cleared directories either way, so we do not over warn. + $cleared += $DestinationPath } # Go over each part @@ -232,11 +364,14 @@ function Install-OpenPackage # complete our progress Write-Progress @Progress - if ($existingFiles) { - Write-Warning "$($existingFiles.Length) Files Exist (Use ``-Force`` to overwrite):$( - [Environment]::NewLine - $existingFiles -join [Environment]::NewLine - )" - } + # Mark this package as installed + $alreadyInstalled += $package + } + + if ($existingFiles) { + Write-Warning "$($existingFiles.Length) Files Exist (Use ``-Force`` to overwrite):$( + [Environment]::NewLine + $existingFiles -join [Environment]::NewLine + )" } } \ No newline at end of file From fc56db9e77f4fdab9d6a79bd3f066e2576020e24 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 14:00:35 -0700 Subject: [PATCH 436/724] feat: `Copy-OpenPackage` ( Fixes #7 ) Copying part relationships --- Commands/Copy-OpenPackage.ps1 | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Commands/Copy-OpenPackage.ps1 b/Commands/Copy-OpenPackage.ps1 index dfed725..cdbd997 100644 --- a/Commands/Copy-OpenPackage.ps1 +++ b/Commands/Copy-OpenPackage.ps1 @@ -152,7 +152,7 @@ function Copy-OpenPackage } $inputPackageParts = $selectedParts = Select-OpenPackage @selectSplat - $inputPackageRelationships = $InputObject.GetRelationships() + $inputPackageRelationships = @($InputObject.GetRelationships()) # For each part in the input foreach ($inputPart in $inputPackageParts) { @@ -165,16 +165,29 @@ function Copy-OpenPackage } # and copy the streams. - $inputStream = $inputPart.GetStream() + $inputStream = $inputPart.GetStream('Open', 'Read') $destinationStream = $destinationPart.GetStream() $inputStream.CopyTo($destinationStream) $inputStream.Close() $destinationStream.Close() + + $partRelationships = @( + try { + $inputPart.GetRelationships() + } catch { + # Relationships cannot have relationships + # Also, we can't copy any relationship that throws an exception. + # So simply ignore the error. + } + ) + if ($partRelationships) { + $inputPackageRelationships += $partRelationships + } } # Then, create any relationships that do not exist. - foreach ($inputRelationship in $inputPackageRelationships) { - if ($inputRelationship) { + foreach ($inputRelationship in $inputPackageRelationships) { + if ($inputRelationship.SourceUri -eq '/') { if (-not $destinationPackage.RelationshipExists($inputRelationship.id)) { $null = $destinationPackage.CreateRelationship( $inputRelationship.targetUri, @@ -183,6 +196,16 @@ function Copy-OpenPackage $inputRelationship.id ) } + } elseif ($inputRelationship.SourceUri -match '^/.') { + $destinationPackagePart = $destinationPackage.GetPart($inputRelationship.SourceUri) + if (-not $destinationPackagePart.RelationshipExists($inputRelationship.id)) { + $destinationPackagePart.CreateRelationship( + $inputRelationship.targetUri, + $inputRelationship.targetMode, + $inputRelationship.relationshipType, + $inputRelationship.id + ) + } } } From cdfe8eb4bac5646421b598d11c3e3df504ae99ab Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 14:10:16 -0700 Subject: [PATCH 437/724] feat: `Get-OpenPackage` ( Fixes #2 ) Improving file handling (allowing -IncludeHidden on a file) --- Commands/Get-OpenPackage.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 2b89cf8..3c0ab60 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -643,8 +643,11 @@ function Get-OpenPackage # Get each file beneath the path foreach ($resolved in $resolvedPath) { - # (watch our for escaped characters) - $resolvedItem = Get-Item -LiteralPath ($resolved -replace '`') + # (watch our for escaped characters and hidden files) + $resolvedItem = Get-Item -LiteralPath ($resolved -replace '`') -Force:$IncludeHidden + + # If we could not resolve the item, continue + if (-not $resolvedItem) { continue } # If the item is a file if ($resolvedItem -is [IO.FileInfo]) { @@ -656,7 +659,7 @@ function Get-OpenPackage # We want a package from a directory # Push into that location, for it will make operations easier Push-Location -LiteralPath $resolvedItem.FullName - # Get all files beneath this point + # Get all files beneath this point $namedParameters['Directory'] = $resolvedItem.FullName InvokeOpMethod 'GetDirectory' $NamedParameters Pop-Location From 7103276b74847eaa0239467cc65fd264b16c1fd6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 14:31:09 -0700 Subject: [PATCH 438/724] feat: Open Package Action ( Fixes #155 ) Adding action, workflow, and self test --- .github/workflows/BuildOP.yml | 9 + Build/GitHub/Actions/OPAction.ps1 | 312 ++++++++++++++++++++++++ Build/GitHub/Jobs/BuildOP.psd1 | 59 +---- OP.op.ps1 | 3 + action.yml | 385 ++++++++++++++++++++++++++++++ 5 files changed, 719 insertions(+), 49 deletions(-) create mode 100644 Build/GitHub/Actions/OPAction.ps1 create mode 100644 OP.op.ps1 create mode 100644 action.yml diff --git a/.github/workflows/BuildOP.yml b/.github/workflows/BuildOP.yml index c2ed709..af86672 100644 --- a/.github/workflows/BuildOP.yml +++ b/.github/workflows/BuildOP.yml @@ -496,6 +496,15 @@ jobs: uses: actions/checkout@main - name: UseEZOut uses: StartAutomating/EZOut@master + - name: Run Action (on branch) + if: ${{github.ref_name != 'main'}} + uses: ./ + id: OPAction + - uses: actions/upload-artifact@main + id: artifact-upload-step + with: + name: op-artifact + path: OP.zip env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} diff --git a/Build/GitHub/Actions/OPAction.ps1 b/Build/GitHub/Actions/OPAction.ps1 new file mode 100644 index 0000000..0d97eba --- /dev/null +++ b/Build/GitHub/Actions/OPAction.ps1 @@ -0,0 +1,312 @@ +<# +.Synopsis + GitHub Action for OP +.Description + GitHub Action for OP. This will: + + * Import OP + * If `-Run` is provided, run that script + * Otherwise, unless `-SkipScriptFile` is passed, run all *.OP.ps1 files beneath the workflow directory + * If any `-ActionScript` was provided, run scripts from the action path that match a wildcard pattern. + + If you will be making changes using the GitHubAPI, you should provide a -GitHubToken + If none is provided, and ENV:GITHUB_TOKEN is set, this will be used instead. + Any files changed can be outputted by the script, and those changes can be checked back into the repo. + Make sure to use the "persistCredentials" option with checkout. +#> + +param( +# A PowerShell Script that uses OP. +# Any files outputted from the script will be added to the repository. +# If those files have a .Message attached to them, they will be committed with that message. +[string] +$Run, + +# If set, will not process any files named *.OP.ps1 +[switch] +$SkipScriptFile, + +# A list of modules to be installed from the PowerShell gallery before scripts run. +[string[]] +$InstallModule, + +# The name of one or more scripts to run, from this action's path. +[string[]] +$ActionScript, + +# The github token to use for requests. +[string] +$GitHubToken = '{{ secrets.GITHUB_TOKEN }}', + +# The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com. +[string] +$UserEmail, + +# The user name associated with a git commit. +[string] +$UserName, + +# If set, will push any changes made to the repository. +# (they will still be committed unless `-NoCommit` is passed) +[switch] +$Push +) + +$ErrorActionPreference = 'continue' +"::group::Parameters" | Out-Host +[PSCustomObject]$PSBoundParameters | Format-List | Out-Host +"::endgroup::" | Out-Host + +$gitHubEventJson = [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) +$gitHubEvent = + if ($env:GITHUB_EVENT_PATH) { + $gitHubEventJson | ConvertFrom-Json + } else { $null } +"::group::Parameters" | Out-Host +$gitHubEvent | Format-List | Out-Host +"::endgroup::" | Out-Host + + +$anyFilesChanged = $false +$ActionModuleName = 'OP' +$actorInfo = $null + +if ($GitHubToken) { + $env:GH_TOKEN = $GitHubToken +} + + +$checkDetached = git symbolic-ref -q HEAD +if ($LASTEXITCODE) { + "::warning::On detached head, skipping action" | Out-Host + exit 0 +} + +function InstallActionModule { + param([string]$ModuleToInstall) + $moduleInWorkspace = Get-ChildItem -Path $env:GITHUB_WORKSPACE -Recurse -File | + Where-Object Name -eq "$($moduleToInstall).psd1" | + Where-Object { + $(Get-Content $_.FullName -Raw) -match 'ModuleVersion' + } + if (-not $moduleInWorkspace) { + $availableModules = Get-Module -ListAvailable + if ($availableModules.Name -notcontains $moduleToInstall) { + Install-Module $moduleToInstall -Scope CurrentUser -Force -AcceptLicense -AllowClobber + } + Import-Module $moduleToInstall -Force -PassThru | Out-Host + } else { + Import-Module $moduleInWorkspace.FullName -Force -PassThru | Out-Host + } +} +function ImportActionModule { + #region -InstallModule + if ($InstallModule) { + "::group::Installing Modules" | Out-Host + foreach ($moduleToInstall in $InstallModule) { + InstallActionModule -ModuleToInstall $moduleToInstall + } + "::endgroup::" | Out-Host + } + #endregion -InstallModule + + if ($env:GITHUB_ACTION_PATH) { + $LocalModulePath = Join-Path $env:GITHUB_ACTION_PATH "$ActionModuleName.psd1" + if (Test-path $LocalModulePath) { + Import-Module $LocalModulePath -Force -PassThru | Out-String + } else { + throw "Module '$ActionModuleName' not found" + } + } elseif (-not (Get-Module $ActionModuleName)) { + throw "Module '$ActionModuleName' not found" + } + + "::notice title=ModuleLoaded::$ActionModuleName Loaded from Path - $($LocalModulePath)" | Out-Host + if ($env:GITHUB_STEP_SUMMARY) { + "# $($ActionModuleName)" | + Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } +} +function InitializeAction { + #region Custom + #endregion Custom + + # Configure git based on the $env:GITHUB_ACTOR + if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } + if (-not $actorID) { $actorID = $env:GITHUB_ACTOR_ID } + + if (-not $UserEmail) { $UserEmail = "$actorID+$UserName@users.noreply.github.com" } + git config --global user.email $UserEmail + git config --global user.name $actorInfo.name + + # Pull down any changes + git pull | Out-Host +} + +function InvokeActionModule { + $myScriptStart = [DateTime]::Now + $myScript = $ExecutionContext.SessionState.PSVariable.Get("Run").Value + if ($myScript) { + Invoke-Expression -Command $myScript | + . ProcessOutput | + Out-Host + return + } + $myScriptTook = [Datetime]::Now - $myScriptStart + $MyScriptFilesStart = [DateTime]::Now + + $myScriptList = @() + $shouldSkip = $ExecutionContext.SessionState.PSVariable.Get("SkipScriptFile").Value + if ($shouldSkip) { + return + } + $scriptFiles = @( + Get-ChildItem -Recurse -Path $env:GITHUB_WORKSPACE | + Where-Object Name -Match "\.$($ActionModuleName)\.ps1$" + if ($ActionScript) { + if ($ActionScript -match '^\s{0,}/' -and $ActionScript -match '/\s{0,}$') { + $ActionScriptPattern = $ActionScript.Trim('/').Trim() -as [regex] + if ($ActionScriptPattern) { + $ActionScriptPattern = [regex]::new($ActionScript.Trim('/').Trim(), 'IgnoreCase,IgnorePatternWhitespace', [timespan]::FromSeconds(0.5)) + Get-ChildItem -Recurse -Path $env:GITHUB_ACTION_PATH | + Where-Object { $_.Name -Match "\.$($ActionModuleName)\.ps1$" -and $_.FullName -match $ActionScriptPattern } + } + } else { + Get-ChildItem -Recurse -Path $env:GITHUB_ACTION_PATH | + Where-Object Name -Match "\.$($ActionModuleName)\.ps1$" | + Where-Object FullName -Like $ActionScript + } + } + ) | Select-Object -Unique + $scriptFiles | + ForEach-Object -Begin { + if ($env:GITHUB_STEP_SUMMARY) { + "## $ActionModuleName Scripts" | + Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } + } -Process { + $myScriptList += $_.FullName.Replace($env:GITHUB_WORKSPACE, '').TrimStart('/') + $myScriptCount++ + $scriptFile = $_ + if ($env:GITHUB_STEP_SUMMARY) { + "### $($scriptFile.Fullname -replace [Regex]::Escape($env:GITHUB_WORKSPACE))" | + Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } + $scriptCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand($scriptFile.FullName, 'ExternalScript') + foreach ($requiredModule in $CommandInfo.ScriptBlock.Ast.ScriptRequirements.RequiredModules) { + if ($requiredModule.Name -and + (-not $requiredModule.MaximumVersion) -and + (-not $requiredModule.RequiredVersion) + ) { + InstallActionModule $requiredModule.Name + } + } + Push-Location $scriptFile.Directory.Fullname + $scriptFileOutputs = . $scriptCmd + $scriptFileOutputs | + . ProcessOutput | + Out-Host + Pop-Location + } + + $MyScriptFilesTook = [Datetime]::Now - $MyScriptFilesStart + $SummaryOfMyScripts = "$myScriptCount $ActionModuleName scripts took $($MyScriptFilesTook.TotalSeconds) seconds" + $SummaryOfMyScripts | + Out-Host + if ($env:GITHUB_STEP_SUMMARY) { + $SummaryOfMyScripts | + Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } + #region Custom + #endregion Custom +} + +function OutError { + $anyRuntimeExceptions = $false + foreach ($err in $error) { + $errParts = @( + "::error " + @( + if ($err.InvocationInfo.ScriptName) { + "file=$($err.InvocationInfo.ScriptName)" + } + if ($err.InvocationInfo.ScriptLineNumber -ge 1) { + "line=$($err.InvocationInfo.ScriptLineNumber)" + if ($err.InvocationInfo.OffsetInLine -ge 1) { + "col=$($err.InvocationInfo.OffsetInLine)" + } + } + if ($err.CategoryInfo.Activity) { + "title=$($err.CategoryInfo.Activity)" + } + ) -join ',' + "::" + $err.Exception.Message + if ($err.CategoryInfo.Category -eq 'OperationStopped' -and + $err.CategoryInfo.Reason -eq 'RuntimeException') { + $anyRuntimeExceptions = $true + } + ) -join '' + $errParts | Out-Host + if ($anyRuntimeExceptions) { + exit 1 + } + } +} + +function PushActionOutput { + if ($anyFilesChanged) { + "::notice::$($anyFilesChanged) Files Changed" | Out-Host + } + if ($anyFilesChanged) { + $checkDetached = git symbolic-ref -q HEAD + if (-not $LASTEXITCODE -and $Push -and -not $noCommit) { + if ($anyFilesChanged) { + "::notice::Pushing Changes" | Out-Host + git push + } + "Git Push Output: $($gitPushed | Out-String)" + } else { + "::notice::Not pushing changes (on detached head)" | Out-Host + $LASTEXITCODE = 0 + exit 0 + } + } +} + +filter ProcessOutput { + $out = $_ + $outItem = Get-Item -Path $out -ErrorAction Ignore + if (-not $outItem -and $out -is [string]) { + $out | Out-Host + if ($env:GITHUB_STEP_SUMMARY) { + "> $out" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } + return + } + $fullName, $shouldCommit = + if ($out -is [IO.FileInfo]) { + $out.FullName, (git status $out.Fullname -s) + } elseif ($outItem) { + $outItem.FullName, (git status $outItem.Fullname -s) + } + if ($shouldCommit -and -not $NoCommit) { + "$fullName has changed, and should be committed" | Out-Host + if ($Push) { + git add $fullName + if ($out.Message) { + git commit -m "$($out.Message)" | Out-Host + } elseif ($out.CommitMessage) { + git commit -m "$($out.CommitMessage)" | Out-Host + } + } + $anyFilesChanged = $true + } + $out +} + +. ImportActionModule +. InitializeAction +. InvokeActionModule +. PushActionOutput +. OutError \ No newline at end of file diff --git a/Build/GitHub/Jobs/BuildOP.psd1 b/Build/GitHub/Jobs/BuildOP.psd1 index 2679de4..a269ee6 100644 --- a/Build/GitHub/Jobs/BuildOP.psd1 +++ b/Build/GitHub/Jobs/BuildOP.psd1 @@ -6,59 +6,20 @@ name = 'Check out repository' uses = 'actions/checkout@main' }, - 'RunEZOut' # , - - # 'BuildAndPublishContainer' - <#@{ - 'name'='Log in to ghcr.io' - 'uses'='docker/login-action@master' - 'with'=@{ - 'registry'='${{ env.REGISTRY }}' - 'username'='${{ github.actor }}' - 'password'='${{ secrets.GITHUB_TOKEN }}' - } - }, - @{ - name = 'Extract Docker Metadata (for branch)' - if = '${{github.ref_name != ''main'' && github.ref_name != ''master'' && github.ref_name != ''latest''}}' - id = 'meta' - uses = 'docker/metadata-action@master' - with = @{ - 'images'='${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}' - } - }, + 'RunEZOut' @{ - name = 'Extract Docker Metadata (for main)' - if = '${{github.ref_name == ''main'' || github.ref_name == ''master'' || github.ref_name == ''latest''}}' - id = 'metaMain' - uses = 'docker/metadata-action@master' - with = @{ - 'images'='${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}' - 'flavor'='latest=true' - } - }, - @{ - name = 'Build and push Docker image (from main)' - if = '${{github.ref_name == ''main'' || github.ref_name == ''master'' || github.ref_name == ''latest''}}' - uses = 'docker/build-push-action@master' - with = @{ - 'context'='.' - 'push'='true' - 'tags'='${{ steps.metaMain.outputs.tags }}' - 'labels'='${{ steps.metaMain.outputs.labels }}' - } + name = 'Run Action (on branch)' + if = '${{github.ref_name != ''main''}}' + uses = './' + id = 'OPAction' }, @{ - name = 'Build and push Docker image (from branch)' - if = '${{github.ref_name != ''main'' && github.ref_name != ''master'' && github.ref_name != ''latest''}}' - uses = 'docker/build-push-action@master' - with = @{ - 'context'='.' - 'push'='true' - 'tags'='${{ steps.meta.outputs.tags }}' - 'labels'='${{ steps.meta.outputs.labels }}' + uses = 'actions/upload-artifact@main' + 'id' = 'artifact-upload-step' + 'with' = @{ + name = 'op-artifact' + path = 'OP.zip' } } - #> ) } \ No newline at end of file diff --git a/OP.op.ps1 b/OP.op.ps1 new file mode 100644 index 0000000..04b7697 --- /dev/null +++ b/OP.op.ps1 @@ -0,0 +1,3 @@ +$imported = Import-Module ./OP.psd1 -Force -PassThru +$op = $imported | Get-OpenPackage +$op | Export-OpenPackage -DestinationPath "$($imported.name).zip" diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..cd4e83d --- /dev/null +++ b/action.yml @@ -0,0 +1,385 @@ + +name: OpenPackage +description: Open Package Action: Anything can be a package. +inputs: + Run: + required: false + description: | + A PowerShell Script that uses OP. + Any files outputted from the script will be added to the repository. + If those files have a .Message attached to them, they will be committed with that message. + SkipScriptFile: + required: false + description: 'If set, will not process any files named *.OP.ps1' + InstallModule: + required: false + description: A list of modules to be installed from the PowerShell gallery before scripts run. + ActionScript: + required: false + description: The name of one or more scripts to run, from this action's path. + GitHubToken: + required: false + default: '{{ secrets.GITHUB_TOKEN }}' + description: The github token to use for requests. + UserEmail: + required: false + description: The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com. + UserName: + required: false + description: The user name associated with a git commit. + Push: + required: false + description: | + If set, will push any changes made to the repository. + (they will still be committed unless `-NoCommit` is passed) +branding: + icon: code + color: blue +runs: + using: composite + steps: + - name: OPAction + id: OPAction + shell: pwsh + env: + InstallModule: ${{inputs.InstallModule}} + Push: ${{inputs.Push}} + ActionScript: ${{inputs.ActionScript}} + UserEmail: ${{inputs.UserEmail}} + Run: ${{inputs.Run}} + SkipScriptFile: ${{inputs.SkipScriptFile}} + UserName: ${{inputs.UserName}} + GitHubToken: ${{inputs.GitHubToken}} + run: | + $Parameters = @{} + $Parameters.Run = ${env:Run} + $Parameters.SkipScriptFile = ${env:SkipScriptFile} + $Parameters.SkipScriptFile = $parameters.SkipScriptFile -match 'true'; + $Parameters.InstallModule = ${env:InstallModule} + $Parameters.InstallModule = $parameters.InstallModule -split ';' -replace '^[''"]' -replace '[''"]$' + $Parameters.ActionScript = ${env:ActionScript} + $Parameters.ActionScript = $parameters.ActionScript -split ';' -replace '^[''"]' -replace '[''"]$' + $Parameters.GitHubToken = ${env:GitHubToken} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + $Parameters.Push = ${env:Push} + $Parameters.Push = $parameters.Push -match 'true'; + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: OPAction $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + GitHub Action for OP + .Description + GitHub Action for OP. This will: + + * Import OP + * If `-Run` is provided, run that script + * Otherwise, unless `-SkipScriptFile` is passed, run all *.OP.ps1 files beneath the workflow directory + * If any `-ActionScript` was provided, run scripts from the action path that match a wildcard pattern. + + If you will be making changes using the GitHubAPI, you should provide a -GitHubToken + If none is provided, and ENV:GITHUB_TOKEN is set, this will be used instead. + Any files changed can be outputted by the script, and those changes can be checked back into the repo. + Make sure to use the "persistCredentials" option with checkout. + #> + + param( + # A PowerShell Script that uses OP. + # Any files outputted from the script will be added to the repository. + # If those files have a .Message attached to them, they will be committed with that message. + [string] + $Run, + + # If set, will not process any files named *.OP.ps1 + [switch] + $SkipScriptFile, + + # A list of modules to be installed from the PowerShell gallery before scripts run. + [string[]] + $InstallModule, + + # The name of one or more scripts to run, from this action's path. + [string[]] + $ActionScript, + + # The github token to use for requests. + [string] + $GitHubToken = '{{ secrets.GITHUB_TOKEN }}', + + # The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName, + + # If set, will push any changes made to the repository. + # (they will still be committed unless `-NoCommit` is passed) + [switch] + $Push + ) + + $ErrorActionPreference = 'continue' + "::group::Parameters" | Out-Host + [PSCustomObject]$PSBoundParameters | Format-List | Out-Host + "::endgroup::" | Out-Host + + $gitHubEventJson = [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) + $gitHubEvent = + if ($env:GITHUB_EVENT_PATH) { + $gitHubEventJson | ConvertFrom-Json + } else { $null } + "::group::Parameters" | Out-Host + $gitHubEvent | Format-List | Out-Host + "::endgroup::" | Out-Host + + + $anyFilesChanged = $false + $ActionModuleName = 'OP' + $actorInfo = $null + + if ($GitHubToken) { + $env:GH_TOKEN = $GitHubToken + } + + + $checkDetached = git symbolic-ref -q HEAD + if ($LASTEXITCODE) { + "::warning::On detached head, skipping action" | Out-Host + exit 0 + } + + function InstallActionModule { + param([string]$ModuleToInstall) + $moduleInWorkspace = Get-ChildItem -Path $env:GITHUB_WORKSPACE -Recurse -File | + Where-Object Name -eq "$($moduleToInstall).psd1" | + Where-Object { + $(Get-Content $_.FullName -Raw) -match 'ModuleVersion' + } + if (-not $moduleInWorkspace) { + $availableModules = Get-Module -ListAvailable + if ($availableModules.Name -notcontains $moduleToInstall) { + Install-Module $moduleToInstall -Scope CurrentUser -Force -AcceptLicense -AllowClobber + } + Import-Module $moduleToInstall -Force -PassThru | Out-Host + } else { + Import-Module $moduleInWorkspace.FullName -Force -PassThru | Out-Host + } + } + function ImportActionModule { + #region -InstallModule + if ($InstallModule) { + "::group::Installing Modules" | Out-Host + foreach ($moduleToInstall in $InstallModule) { + InstallActionModule -ModuleToInstall $moduleToInstall + } + "::endgroup::" | Out-Host + } + #endregion -InstallModule + + if ($env:GITHUB_ACTION_PATH) { + $LocalModulePath = Join-Path $env:GITHUB_ACTION_PATH "$ActionModuleName.psd1" + if (Test-path $LocalModulePath) { + Import-Module $LocalModulePath -Force -PassThru | Out-String + } else { + throw "Module '$ActionModuleName' not found" + } + } elseif (-not (Get-Module $ActionModuleName)) { + throw "Module '$ActionModuleName' not found" + } + + "::notice title=ModuleLoaded::$ActionModuleName Loaded from Path - $($LocalModulePath)" | Out-Host + if ($env:GITHUB_STEP_SUMMARY) { + "# $($ActionModuleName)" | + Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } + } + function InitializeAction { + #region Custom + #endregion Custom + + # Configure git based on the $env:GITHUB_ACTOR + if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } + if (-not $actorID) { $actorID = $env:GITHUB_ACTOR_ID } + + if (-not $UserEmail) { $UserEmail = "$actorID+$UserName@users.noreply.github.com" } + git config --global user.email $UserEmail + git config --global user.name $actorInfo.name + + # Pull down any changes + git pull | Out-Host + } + + function InvokeActionModule { + $myScriptStart = [DateTime]::Now + $myScript = $ExecutionContext.SessionState.PSVariable.Get("Run").Value + if ($myScript) { + Invoke-Expression -Command $myScript | + . ProcessOutput | + Out-Host + return + } + $myScriptTook = [Datetime]::Now - $myScriptStart + $MyScriptFilesStart = [DateTime]::Now + + $myScriptList = @() + $shouldSkip = $ExecutionContext.SessionState.PSVariable.Get("SkipScriptFile").Value + if ($shouldSkip) { + return + } + $scriptFiles = @( + Get-ChildItem -Recurse -Path $env:GITHUB_WORKSPACE | + Where-Object Name -Match "\.$($ActionModuleName)\.ps1$" + if ($ActionScript) { + if ($ActionScript -match '^\s{0,}/' -and $ActionScript -match '/\s{0,}$') { + $ActionScriptPattern = $ActionScript.Trim('/').Trim() -as [regex] + if ($ActionScriptPattern) { + $ActionScriptPattern = [regex]::new($ActionScript.Trim('/').Trim(), 'IgnoreCase,IgnorePatternWhitespace', [timespan]::FromSeconds(0.5)) + Get-ChildItem -Recurse -Path $env:GITHUB_ACTION_PATH | + Where-Object { $_.Name -Match "\.$($ActionModuleName)\.ps1$" -and $_.FullName -match $ActionScriptPattern } + } + } else { + Get-ChildItem -Recurse -Path $env:GITHUB_ACTION_PATH | + Where-Object Name -Match "\.$($ActionModuleName)\.ps1$" | + Where-Object FullName -Like $ActionScript + } + } + ) | Select-Object -Unique + $scriptFiles | + ForEach-Object -Begin { + if ($env:GITHUB_STEP_SUMMARY) { + "## $ActionModuleName Scripts" | + Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } + } -Process { + $myScriptList += $_.FullName.Replace($env:GITHUB_WORKSPACE, '').TrimStart('/') + $myScriptCount++ + $scriptFile = $_ + if ($env:GITHUB_STEP_SUMMARY) { + "### $($scriptFile.Fullname -replace [Regex]::Escape($env:GITHUB_WORKSPACE))" | + Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } + $scriptCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand($scriptFile.FullName, 'ExternalScript') + foreach ($requiredModule in $CommandInfo.ScriptBlock.Ast.ScriptRequirements.RequiredModules) { + if ($requiredModule.Name -and + (-not $requiredModule.MaximumVersion) -and + (-not $requiredModule.RequiredVersion) + ) { + InstallActionModule $requiredModule.Name + } + } + Push-Location $scriptFile.Directory.Fullname + $scriptFileOutputs = . $scriptCmd + $scriptFileOutputs | + . ProcessOutput | + Out-Host + Pop-Location + } + + $MyScriptFilesTook = [Datetime]::Now - $MyScriptFilesStart + $SummaryOfMyScripts = "$myScriptCount $ActionModuleName scripts took $($MyScriptFilesTook.TotalSeconds) seconds" + $SummaryOfMyScripts | + Out-Host + if ($env:GITHUB_STEP_SUMMARY) { + $SummaryOfMyScripts | + Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } + #region Custom + #endregion Custom + } + + function OutError { + $anyRuntimeExceptions = $false + foreach ($err in $error) { + $errParts = @( + "::error " + @( + if ($err.InvocationInfo.ScriptName) { + "file=$($err.InvocationInfo.ScriptName)" + } + if ($err.InvocationInfo.ScriptLineNumber -ge 1) { + "line=$($err.InvocationInfo.ScriptLineNumber)" + if ($err.InvocationInfo.OffsetInLine -ge 1) { + "col=$($err.InvocationInfo.OffsetInLine)" + } + } + if ($err.CategoryInfo.Activity) { + "title=$($err.CategoryInfo.Activity)" + } + ) -join ',' + "::" + $err.Exception.Message + if ($err.CategoryInfo.Category -eq 'OperationStopped' -and + $err.CategoryInfo.Reason -eq 'RuntimeException') { + $anyRuntimeExceptions = $true + } + ) -join '' + $errParts | Out-Host + if ($anyRuntimeExceptions) { + exit 1 + } + } + } + + function PushActionOutput { + if ($anyFilesChanged) { + "::notice::$($anyFilesChanged) Files Changed" | Out-Host + } + if ($anyFilesChanged) { + $checkDetached = git symbolic-ref -q HEAD + if (-not $LASTEXITCODE -and $Push -and -not $noCommit) { + if ($anyFilesChanged) { + "::notice::Pushing Changes" | Out-Host + git push + } + "Git Push Output: $($gitPushed | Out-String)" + } else { + "::notice::Not pushing changes (on detached head)" | Out-Host + $LASTEXITCODE = 0 + exit 0 + } + } + } + + filter ProcessOutput { + $out = $_ + $outItem = Get-Item -Path $out -ErrorAction Ignore + if (-not $outItem -and $out -is [string]) { + $out | Out-Host + if ($env:GITHUB_STEP_SUMMARY) { + "> $out" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + } + return + } + $fullName, $shouldCommit = + if ($out -is [IO.FileInfo]) { + $out.FullName, (git status $out.Fullname -s) + } elseif ($outItem) { + $outItem.FullName, (git status $outItem.Fullname -s) + } + if ($shouldCommit -and -not $NoCommit) { + "$fullName has changed, and should be committed" | Out-Host + if ($Push) { + git add $fullName + if ($out.Message) { + git commit -m "$($out.Message)" | Out-Host + } elseif ($out.CommitMessage) { + git commit -m "$($out.CommitMessage)" | Out-Host + } + } + $anyFilesChanged = $true + } + $out + } + + . ImportActionModule + . InitializeAction + . InvokeActionModule + . PushActionOutput + . OutError} @Parameters + From db1a1c1b1b14f409a6989e3bed44d166fc45fe14 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 14:33:15 -0700 Subject: [PATCH 439/724] feat: Open Package Action ( Fixes #155 ) Not mapping github token --- action.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/action.yml b/action.yml index cd4e83d..dd90251 100644 --- a/action.yml +++ b/action.yml @@ -19,7 +19,6 @@ inputs: description: The name of one or more scripts to run, from this action's path. GitHubToken: required: false - default: '{{ secrets.GITHUB_TOKEN }}' description: The github token to use for requests. UserEmail: required: false @@ -108,7 +107,7 @@ runs: # The github token to use for requests. [string] - $GitHubToken = '{{ secrets.GITHUB_TOKEN }}', + $GitHubToken, # The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com. [string] From 53c1f06f3eedbfc220bc8392b6c5941422fa55cb Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 14:37:58 -0700 Subject: [PATCH 440/724] feat: Open Package Action ( Fixes #155 ) Removing colon from description --- Build/OP.GitHubAction.PSDevOps.ps1 | 10 ++++++++++ action.yml | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 Build/OP.GitHubAction.PSDevOps.ps1 diff --git a/Build/OP.GitHubAction.PSDevOps.ps1 b/Build/OP.GitHubAction.PSDevOps.ps1 new file mode 100644 index 0000000..27dda92 --- /dev/null +++ b/Build/OP.GitHubAction.PSDevOps.ps1 @@ -0,0 +1,10 @@ +#requires -Module PSDevOps +Import-BuildStep -SourcePath ( + Join-Path $PSScriptRoot 'GitHub' +) -BuildSystem GitHubAction + +Push-Location ($PSScriptRoot | Split-Path) +New-GitHubAction -Name "OpenPackage" -Description @' +Open Package Action - Open anything as a package +'@ -Action OPAction -Icon code -OutputPath .\action.yml +Pop-Location \ No newline at end of file diff --git a/action.yml b/action.yml index dd90251..00766c3 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,6 @@ name: OpenPackage -description: Open Package Action: Anything can be a package. +description: Open Package Action - Open anything as a package inputs: Run: required: false @@ -19,6 +19,7 @@ inputs: description: The name of one or more scripts to run, from this action's path. GitHubToken: required: false + default: '{{ secrets.GITHUB_TOKEN }}' description: The github token to use for requests. UserEmail: required: false @@ -107,7 +108,7 @@ runs: # The github token to use for requests. [string] - $GitHubToken, + $GitHubToken = '{{ secrets.GITHUB_TOKEN }}', # The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com. [string] From 185771790622e2ddd569141c28d776c1616f1241 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 15:05:38 -0700 Subject: [PATCH 441/724] feat: `Export-OpenPackage` ( Fixes #39 ) Adding inner docs, ignoring dots in parent directories. --- Commands/Export-OpenPackage.ps1 | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Commands/Export-OpenPackage.ps1 b/Commands/Export-OpenPackage.ps1 index 16a418f..38e54f6 100644 --- a/Commands/Export-OpenPackage.ps1 +++ b/Commands/Export-OpenPackage.ps1 @@ -69,15 +69,27 @@ function Export-OpenPackage { # Get the item if it already exists $destinationItem = Get-Item $DestinationPath -ErrorAction Ignore - if ($destinationItem -is [IO.DirectoryInfo] -or ($DestinationPath -notlike '*.*')) + # Copy parameters to simply any future debugging. + $parameterCopy = [Ordered]@{} + $PSBoundParameters + + if ( + # If the destination is a directory + ($destinationItem -is [IO.DirectoryInfo]) -or + # Or the last segment has no extension. + (@($DestinationPath -split '[\\/]' -ne '')[-1] -notlike '*.*') + ) { - Expand-OpenPackage @PSBoundParameters -PassThru + # install the open package into that directory. + Install-OpenPackage @parameterCopy -PassThru } else { - $copiedPackage = Copy-OpenPackage @PSBoundParameters + # Otherwise, copy the package to that path + $copiedPackage = Copy-OpenPackage @parameterCopy + # Get the output item $outputFile = Get-Item $DestinationPath - $outputFile | + # and add the package to the file. + $outputFile | Add-Member NoteProperty Package $copiedPackage -Force -PassThru - } + } } } From b99f2b2ad930416ca0cdf6eaf28d74f24ea43790 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 15:22:28 -0700 Subject: [PATCH 442/724] feat: `Get-OpenPackage -Running` ( Fixes #156 ) --- Commands/Get-OpenPackage.ps1 | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 3c0ab60..ace4f67 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -203,6 +203,21 @@ function Get-OpenPackage [Alias('Package')] [PSObject] $InputObject, + + # Gets the packages that are currently opened in this runspace + [Parameter(Mandatory,ParameterSetName='Opened')] + [switch] + $Opened, + + # Gets the packages that are currently installed + [Parameter(Mandatory,ParameterSetName='Opened')] + [switch] + $Installed, + + # Gets packages that are currently running in a server + [Parameter(Mandatory,ParameterSetName='Running')] + [switch] + $Running, # If set, will force the redownload of various resources and remove existing files or directories [switch] @@ -567,6 +582,23 @@ function Get-OpenPackage if ($inputObject -is [IO.Packaging.Package]) { $namedParameters['Package'] = $InputObject } + + if ($running) { + Get-Job | + Where-Object { + $job = $_ + if ($job.JobStateInfo.State -ne 'Running') { + return $false + } + + foreach ($pack in $job.Package) { + if ($pack -is [IO.Packaging.Package]) { + return $true + } + } + } + return + } # If we are passed a uri if ($Uri) { From 99e24d5ec953e7ebfc51e9a64a7b90e650ec81b6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 22 Mar 2026 15:43:31 -0700 Subject: [PATCH 443/724] feat: `Get-OpenPackage -Installed` ( Fixes #157 ) --- Commands/Get-OpenPackage.ps1 | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index ace4f67..8e4656c 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -203,14 +203,9 @@ function Get-OpenPackage [Alias('Package')] [PSObject] $InputObject, - - # Gets the packages that are currently opened in this runspace - [Parameter(Mandatory,ParameterSetName='Opened')] - [switch] - $Opened, - + # Gets the packages that are currently installed - [Parameter(Mandatory,ParameterSetName='Opened')] + [Parameter(Mandatory,ParameterSetName='Installed')] [switch] $Installed, @@ -583,6 +578,22 @@ function Get-OpenPackage $namedParameters['Package'] = $InputObject } + if ($Installed) { + if (-not $env:OpenPackagePath) { + Write-Error '$env:OpenPackagePath not defined' + return + } + + Get-ChildItem -Path ($env:OpenPackagePath -split $( + if ($isLinux -or $IsMacOs) { + ':' + } else { + ';' + } + )) -ErrorAction Ignore + return + } + if ($running) { Get-Job | Where-Object { From cf0f44d1516946aa1be20674ebbc252459b61185 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Mar 2026 13:29:16 -0700 Subject: [PATCH 444/724] feat: `OpenPackage.GetAt` ( Fixes #150 ) Supporting -First and -Skip --- Types/OpenPackage/GetAt.ps1 | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage/GetAt.ps1 b/Types/OpenPackage/GetAt.ps1 index 8ab6459..8aef6fb 100644 --- a/Types/OpenPackage/GetAt.ps1 +++ b/Types/OpenPackage/GetAt.ps1 @@ -64,11 +64,21 @@ $IncludeNodeModule, # The current package [IO.Packaging.Package] -$Package +$Package, + +# The number of records to get +[long] +$First, + +# If number of records to skip +[long] +$Skip ) +# If a package does not exist if (-not $package) { + # create one $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') $package.pstypenames.insert(0, 'OP') @@ -77,8 +87,8 @@ if (-not $package) { } +# Collect all of our named parameters $namedParameters = [Ordered]@{} - foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { $var = $ExecutionContext.SessionState.PSVariable.Get($key) if (-not [string]::IsNullOrEmpty($var.value)) { From b10d134267c733a38ca3df39a63f84df24c5d73d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Mar 2026 20:29:38 +0000 Subject: [PATCH 445/724] feat: `OpenPackage.GetAt` ( Fixes #150 ) Supporting -First and -Skip --- OP.types.ps1xml | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d3c96fd..171dc59 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -112,11 +112,21 @@ $IncludeNodeModule, # The current package [IO.Packaging.Package] -$Package +$Package, + +# The number of records to get +[long] +$First, + +# If number of records to skip +[long] +$Skip ) +# If a package does not exist if (-not $package) { + # create one $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') $package.pstypenames.insert(0, 'OP') @@ -125,8 +135,8 @@ if (-not $package) { } +# Collect all of our named parameters $namedParameters = [Ordered]@{} - foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { $var = $ExecutionContext.SessionState.PSVariable.Get($key) if (-not [string]::IsNullOrEmpty($var.value)) { @@ -3849,11 +3859,21 @@ $IncludeNodeModule, # The current package [IO.Packaging.Package] -$Package +$Package, + +# The number of records to get +[long] +$First, + +# If number of records to skip +[long] +$Skip ) +# If a package does not exist if (-not $package) { + # create one $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') $package.pstypenames.insert(0, 'OP') @@ -3862,8 +3882,8 @@ if (-not $package) { } +# Collect all of our named parameters $namedParameters = [Ordered]@{} - foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { $var = $ExecutionContext.SessionState.PSVariable.Get($key) if (-not [string]::IsNullOrEmpty($var.value)) { @@ -7586,11 +7606,21 @@ $IncludeNodeModule, # The current package [IO.Packaging.Package] -$Package +$Package, + +# The number of records to get +[long] +$First, + +# If number of records to skip +[long] +$Skip ) +# If a package does not exist if (-not $package) { + # create one $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') $package.pstypenames.insert(0, 'OP') @@ -7599,8 +7629,8 @@ if (-not $package) { } +# Collect all of our named parameters $namedParameters = [Ordered]@{} - foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { $var = $ExecutionContext.SessionState.PSVariable.Get($key) if (-not [string]::IsNullOrEmpty($var.value)) { From ee70bbc0027ca206db887af694c06d12ea666573 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Mar 2026 13:34:27 -0700 Subject: [PATCH 446/724] feat: `OpenPackage.Part.ReadJsonL` ( Fixes #104 ) Supporting .jsonnd files, adding a note. --- Types/OpenPackage.Part/ReadJsonL.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/ReadJsonL.ps1 b/Types/OpenPackage.Part/ReadJsonL.ps1 index a15022a..ff4cc7c 100644 --- a/Types/OpenPackage.Part/ReadJsonL.ps1 +++ b/Types/OpenPackage.Part/ReadJsonL.ps1 @@ -3,10 +3,13 @@ Reads Part Content as Json Lines .DESCRIPTION Reads Open Package Part Content as Json Lines +.NOTES + Also should work for asciienma `.cast` files and `.jsonnd`, + which are also json files delimited by newlines. #> [Reflection.AssemblyMetadata( 'FilePattern', - '\.(?>cast|jsonl)?$' + '\.(?>cast|jsonl|jsonnd)?$' )] param( # An optional input object From 61c3e3b5aaaa806cb59dc798c044067372b538e4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Mar 2026 20:34:53 +0000 Subject: [PATCH 447/724] feat: `OpenPackage.Part.ReadJsonL` ( Fixes #104 ) Supporting .jsonnd files, adding a note. --- OP.types.ps1xml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 171dc59..788416a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11664,10 +11664,13 @@ ConvertFrom-Json -InputObject $partText Reads Part Content as Json Lines .DESCRIPTION Reads Open Package Part Content as Json Lines +.NOTES + Also should work for asciienma `.cast` files and `.jsonnd`, + which are also json files delimited by newlines. #> [Reflection.AssemblyMetadata( 'FilePattern', - '\.(?>cast|jsonl)?$' + '\.(?>cast|jsonl|jsonnd)?$' )] param( # An optional input object @@ -13225,10 +13228,13 @@ ConvertFrom-Json -InputObject $partText Reads Part Content as Json Lines .DESCRIPTION Reads Open Package Part Content as Json Lines +.NOTES + Also should work for asciienma `.cast` files and `.jsonnd`, + which are also json files delimited by newlines. #> [Reflection.AssemblyMetadata( 'FilePattern', - '\.(?>cast|jsonl)?$' + '\.(?>cast|jsonl|jsonnd)?$' )] param( # An optional input object @@ -14786,10 +14792,13 @@ ConvertFrom-Json -InputObject $partText Reads Part Content as Json Lines .DESCRIPTION Reads Open Package Part Content as Json Lines +.NOTES + Also should work for asciienma `.cast` files and `.jsonnd`, + which are also json files delimited by newlines. #> [Reflection.AssemblyMetadata( 'FilePattern', - '\.(?>cast|jsonl)?$' + '\.(?>cast|jsonl|jsonnd)?$' )] param( # An optional input object From a2871e64743f4dcb8c19625b59d65ecb9cadd8ac Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Mar 2026 15:32:44 -0700 Subject: [PATCH 448/724] feat: `OpenPackage.Part.ReadMarkdown` ( Fixes #105 ) Improving performance by using assemblies --- Types/OpenPackage.Part/ReadMarkdown.ps1 | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Types/OpenPackage.Part/ReadMarkdown.ps1 b/Types/OpenPackage.Part/ReadMarkdown.ps1 index 05db5bf..7db6439 100644 --- a/Types/OpenPackage.Part/ReadMarkdown.ps1 +++ b/Types/OpenPackage.Part/ReadMarkdown.ps1 @@ -27,19 +27,25 @@ param( ) if (-not $this.ReadText) { return } -$convertFromMarkdownCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Markdown', 'Cmdlet,Function') $partString = $this.ReadText($InputObject, $Option) -if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parameters.InputObject) { - Write-Warning "ConvertFrom-Markdown not found" + +if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { + Write-Warning "Markdig not loaded (ConvertFrom-Markdown is not installed)" $partString = [PSObject]::new($partString) $partString.pstypenames.add('text/markdown') $partString -} else { - try { - $partString | - & $convertFromMarkdownCommand -ErrorAction Stop | - Add-Member NoteProperty Markdown "$partString" -Force -PassThru - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid markdown: $_" - } -} \ No newline at end of file + return +} + +$mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new() +$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build() + +try { + [PSCustomObject]@{ + PSTypeName = 'text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$partString" + } +} catch { + Write-Warning "'$($this.Uri)' was not valid markdown: $_" +} From 1d997550545e8f1ed260c27e7a52dfa502effc89 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Mar 2026 22:33:34 +0000 Subject: [PATCH 449/724] feat: `OpenPackage.Part.ReadMarkdown` ( Fixes #105 ) Improving performance by using assemblies --- OP.types.ps1xml | 87 ++++++++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 788416a..90f76eb 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -11723,22 +11723,29 @@ param( ) if (-not $this.ReadText) { return } -$convertFromMarkdownCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Markdown', 'Cmdlet,Function') $partString = $this.ReadText($InputObject, $Option) -if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parameters.InputObject) { - Write-Warning "ConvertFrom-Markdown not found" + +if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { + Write-Warning "Markdig not loaded (ConvertFrom-Markdown is not installed)" $partString = [PSObject]::new($partString) $partString.pstypenames.add('text/markdown') $partString -} else { - try { - $partString | - & $convertFromMarkdownCommand -ErrorAction Stop | - Add-Member NoteProperty Markdown "$partString" -Force -PassThru - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid markdown: $_" - } + return } + +$mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new() +$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build() + +try { + [PSCustomObject]@{ + PSTypeName = 'text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$partString" + } +} catch { + Write-Warning "'$($this.Uri)' was not valid markdown: $_" +} + @@ -13287,22 +13294,29 @@ param( ) if (-not $this.ReadText) { return } -$convertFromMarkdownCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Markdown', 'Cmdlet,Function') $partString = $this.ReadText($InputObject, $Option) -if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parameters.InputObject) { - Write-Warning "ConvertFrom-Markdown not found" + +if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { + Write-Warning "Markdig not loaded (ConvertFrom-Markdown is not installed)" $partString = [PSObject]::new($partString) $partString.pstypenames.add('text/markdown') $partString -} else { - try { - $partString | - & $convertFromMarkdownCommand -ErrorAction Stop | - Add-Member NoteProperty Markdown "$partString" -Force -PassThru - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid markdown: $_" - } + return } + +$mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new() +$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build() + +try { + [PSCustomObject]@{ + PSTypeName = 'text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$partString" + } +} catch { + Write-Warning "'$($this.Uri)' was not valid markdown: $_" +} + @@ -14851,22 +14865,29 @@ param( ) if (-not $this.ReadText) { return } -$convertFromMarkdownCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Markdown', 'Cmdlet,Function') $partString = $this.ReadText($InputObject, $Option) -if (-not $convertFromMarkdownCommand -or -not $convertFromMarkdownCommand.Parameters.InputObject) { - Write-Warning "ConvertFrom-Markdown not found" + +if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { + Write-Warning "Markdig not loaded (ConvertFrom-Markdown is not installed)" $partString = [PSObject]::new($partString) $partString.pstypenames.add('text/markdown') $partString -} else { - try { - $partString | - & $convertFromMarkdownCommand -ErrorAction Stop | - Add-Member NoteProperty Markdown "$partString" -Force -PassThru - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid markdown: $_" - } + return } + +$mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new() +$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build() + +try { + [PSCustomObject]@{ + PSTypeName = 'text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$partString" + } +} catch { + Write-Warning "'$($this.Uri)' was not valid markdown: $_" +} + From 73f85a2e929990871b2cd01e8ba35cb21c550372 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Mar 2026 19:45:19 -0700 Subject: [PATCH 450/724] feat: `Install-OpenPackage` ( Fixes #52 ) Not installing backups to explicit paths. Always creating a new file to avoid underwrites. --- Commands/Install-OpenPackage.ps1 | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Commands/Install-OpenPackage.ps1 b/Commands/Install-OpenPackage.ps1 index cd64c21..97d6d11 100644 --- a/Commands/Install-OpenPackage.ps1 +++ b/Commands/Install-OpenPackage.ps1 @@ -242,8 +242,13 @@ function Install-OpenPackage # Get all of the package parts $inputParts = @(Select-OpenPackage @selectSplat) - # If we are backing up - if (-not $NoBackup) { + + if ( + # If -NoBackup was passed, + (-not $NoBackup) -or + # or we are explicitly installing into a path + $PSBoundParameters['DestinationPath'] + ) { # copy the open package $copyOpenPackage = @(Copy-OpenPackage @selectSplat) @@ -321,7 +326,7 @@ function Install-OpenPackage } continue nextPart # (and continue to the next part). } - Get-Item -LiteralPath $partDestination + New-Item -ItemType File -Path $partDestination -Force } else { # create a file if it did not exist. New-Item -ItemType File -Path $partDestination -Force From 21c6243b045d599dc6b7a8376455e0af2f0149cc Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Mar 2026 20:26:43 -0700 Subject: [PATCH 451/724] feat: `OpenPackage.GetDirectory` ( Fixes #116 ) Supporting versioned directories and adding git relationship --- Types/OpenPackage/GetDirectory.ps1 | 54 +++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/Types/OpenPackage/GetDirectory.ps1 b/Types/OpenPackage/GetDirectory.ps1 index 1ea8bb5..b70d733 100644 --- a/Types/OpenPackage/GetDirectory.ps1 +++ b/Types/OpenPackage/GetDirectory.ps1 @@ -1,13 +1,14 @@ +<# +.SYNOPSIS + Gets a Directory as a package +.DESCRIPTION + Gets a Directory as an Open Package +#> param( +# The list of directories [string[]] $Directory, -# One or more optional sparse filters to a repository. -# If these are provided, only files matching these filters will be downloaded. -[Parameter(ValueFromPipelineByPropertyName)] -[string[]] -$SparseFilter, - # A list of file wildcards to include. [Parameter(ValueFromPipelineByPropertyName)] [SupportsWildcards()] @@ -85,16 +86,47 @@ foreach ($resolvedItem in $resolvedItems) { $counter = 0 # We will use file types to provide package metadata - if (-not $package) { $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') $package.pstypenames.insert(0, 'OP') $package.pstypenames.insert(0, 'OpenPackage') - $package.PackageProperties.Identifier = $resolvedItem.Name + if ($resolvedItem.Parent.Name -and + ( + $resolvedItem.Name -as [version] -or + $resolvedItem.Name -as [semver] + ) + ) { + $package.PackageProperties.Identifier = $resolvedItem.Parent.Name + $package.PackageProperties.Version = $resolvedItem.Name + } else { + $package.PackageProperties.Identifier = $resolvedItem.Name + } + Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force } + + # Try to get the git app + $gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git','application') + + if ($gitApp -and # If git is loaded + (Test-Path ( # and a .git directory exists + Join-Path $resolvedItem.FullName '.git' + )) + ) { + # get it's first remote + @(& $gitApp '-C' $resolvedItem.FullName remote)[0] + ForEach-Object { + & $gitApp '-C' $resolvedItem.FullName remote get-url $_ + } | + Foreach-Object { + if ($package.RelationshipExists('repository')) { + $package.DeleteRelationship('repository') + } + $null = $package.CreateRelationship($_, 'External', 'git', 'repository') + } + } # So declare an oldest created file and newest write time. @@ -157,11 +189,7 @@ foreach ($resolvedItem in $resolvedItems) { } # encode our URI, - $encodedUri = [Web.HttpUtility]::UrlEncode($relativeUri) -replace - # but don't forget to fix spaces and decode slashes - '\+', '%20' -replace '%2f', '/' - # (our relative URI may already contain them) - + $encodedUri = [IO.Packaging.PackUriHelper]::CreatePartUri($relativeUri) $relativeUri = '/' + ($encodedUri -replace '^/') # Determine the right content type for the extension From ac61be5d3758a45dee348055d2e57e97e4404d6f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 03:27:00 +0000 Subject: [PATCH 452/724] feat: `OpenPackage.GetDirectory` ( Fixes #116 ) Supporting versioned directories and adding git relationship --- OP.types.ps1xml | 168 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 126 insertions(+), 42 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 90f76eb..4d6b7bb 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -868,16 +868,17 @@ foreach ($dictionary in $dictionaryList) { GetDirectory - - - GetAtBlob - - - - GetAtProto - - - - GetAtRecord - - - - GetAtType - - - - GetContent - - - - GetDictionary - - - - GetDirectory - - - - GetNuget - - - - GetRepository - - - - GetTar - - - - GetTree - - - - GetUrl - - - - GetZip - - - - Match - - - - SetContent - - - - ShowTreeHtml - - - - ShowTreeText - - - - Astro - - <# -.SYNOPSIS - Gets Open Package Astro Files -.DESCRIPTION - Gets Astro File Content in an Open Package -#> - -param() - -$this.GetContent($this.FileList -match '\.astro$') - - - - Cache - - <# -.SYNOPSIS - Gets cache -.DESCRIPTION - Gets an open package's cache. - - This is an ordered dictionary of data attached to the object, but not saved to disk. -#> -param() - -if (-not $this) { return } - -if (-not $this.'#Cache') { - Add-Member -InputObject $this NoteProperty '#Cache' ([Ordered]@{}) -Force -} - -return $this.'#Cache' - - - - Category - - <# -.SYNOPSIS - Gets OpenPackage `Category` -.DESCRIPTION - Gets the OpenPackage `Category` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Category - - - <# -.SYNOPSIS - Sets OpenPackage `Category` -.DESCRIPTION - Sets the OpenPackage `Category` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 -#> -param([string]$Category) - -$this.PackageProperties.Category = $Category - - - - CHANGELOG.md - - <# -.SYNOPSIS - Gets a package's changelog -.DESCRIPTION - Gets the content of any parts in the package named CHANGELOG.md -#> -[OutputType("text/markdown")] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG - if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - ChocolateyInstall - - <# -.SYNOPSIS - Gets the Chocolatey Install Script -.DESCRIPTION - Gets the Chocolatey Install Script from an OpenPackage, if one is present. - - The Chocolatey install script must be located at /tools/chocolateyInstall.ps1 -#> -param() -$this.GetContent("/tools/chocolateyInstall.ps1") - - - - Claude.md - - <# -.SYNOPSIS - Gets a package's Claude Markdown -.DESCRIPTION - Gets any `claude.md` or `/.claude/*.md` files in an Open Package -.LINK - https://claude.com/blog/using-claude-md-files -#> -foreach ($part in $this.GetParts()) { - if ($part.Uri -match '/CLAUDE\.(?>md|markdown)$' -or - $part.Uri -match '/\.claude/.+?\.(?>md|markdown)$' - ) { - if ($part.Reader) { - $part.Read() - } else { - $part - } - } -} - - - - CodeOfConduct.md - - <# -.SYNOPSIS - Gets a package's code of conduct -.DESCRIPTION - Gets any parts in the package named Code_Of_Conduct.md -#> -[OutputType("text/markdown")] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CODE_OF_CONDUCT - if ($part.Uri -notmatch '/CODE_OF_CONDUCT\.md$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Config.json - - <# -.SYNOPSIS - Gets a package's config json -.DESCRIPTION - Gets the objects stored in any config.json files in the package. -.NOTES - config.json files are used by several static site generators. -#> -[OutputType([PSObject])] -param() - -$partPattern = '[/\+_]config\.json$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch $partPattern) { continue } - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - - - - - Config.yaml - - <# -.SYNOPSIS - Gets a package's config yaml -.DESCRIPTION - Gets the objects stored in any config.yaml files in the package. -.NOTES - config.yaml files are used by several static site generators. -#> -[OutputType([PSObject])] -param() - -$partPattern = '[/\+_]config\.ya?ml$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch $partPattern) { continue } - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - - - - - ContentStatus - - <# -.SYNOPSIS - Gets OpenPackage `ContentStatus` -.DESCRIPTION - Gets the OpenPackage `ContentStatus` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.ContentStatus - - - <# -.SYNOPSIS - Sets OpenPackage `ContentStatus` -.DESCRIPTION - Sets the OpenPackage `ContentStatus` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 -#> -param([string]$ContentStatus) - -$this.PackageProperties.ContentStatus = $ContentStatus - - - - ContentType - - <# -.SYNOPSIS - Gets OpenPackage `ContentType` -.DESCRIPTION - Gets the OpenPackage `ContentType` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.ContentType -split ';' - - - <# -.SYNOPSIS - Sets OpenPackage `ContentType` -.DESCRIPTION - Sets the OpenPackage `ContentType` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 -#> -param([string]$ContentType) - -$this.PackageProperties.ContentType = $ContentType - - - - Contributing.md - - <# -.SYNOPSIS - Gets a package's contribution guide -.DESCRIPTION - Gets any parts in the package named Contributing.md -#> -[OutputType("text/markdown")] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named Contributing - if ($part.Uri -notmatch '/Contributing\.md$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Count - - <# -.SYNOPSIS - Gets the package files count -.DESCRIPTION - Gets the number of files in a package. -#> -return @($this.GetParts()).Length - - - - Created - - <# -.SYNOPSIS - Gets OpenPackage creation time -.DESCRIPTION - Gets the OpenPackage `Created` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Created - - - <# -.SYNOPSIS - Sets OpenPackage `Created` -.DESCRIPTION - Sets the OpenPackage `Created` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 -#> -param([DateTime]$Created) - -$this.PackageProperties.Created = $Created - - - - Creator - - <# -.SYNOPSIS - Gets OpenPackage `Creator` -.DESCRIPTION - Gets the OpenPackage `Creator` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Creator - - - <# -.SYNOPSIS - Sets OpenPackage `Creator` -.DESCRIPTION - Sets the OpenPackage `Creator` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 -#> -param([string]$Creator) - -$this.PackageProperties.Creator = $Creator - - - - Description - - return $this.PackageProperties.Description - - - $this.PackageProperties.Description = $args -join [Environment]::NewLine - - - - Dockerfile - - <# -.SYNOPSIS - Gets a package's dockerfile -.DESCRIPTION - Gets the content of any `Dockerfile`s in the package. -#> -[OutputType([string])] -param() - -$partPattern = '[/\.]DockerFile$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -match $partPattern) { - $part.Read() - } -} - - - - Eleventy - - <# -.SYNOPSIS - Gets a package's eleventy files -.DESCRIPTION - Gets the content of any elevent config files in the package. - - This includes any files named: - * `.eleventy.js` - * `eleventy.config.js` - * `eleventy.config.mjs` - * `eleventy.config.cjs` -.LINK - https://www.11ty.dev/docs/config/ -#> -param() - -$partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' -foreach ($part in $this.GetParts()) { - if ($part.Uri -match $partPattern) { - if ($part.Reader) { - $part.Read() - } else { - $part - } - } -} - - - - - Eponym - - <# -.SYNOPSIS - Gets all Eponyms in a Package -.DESCRIPTION - Gets all Eponyms within an OpenPackage. - - Eponyms are files whose name matches their directory. - - For example: - - `/foo/foo.ps1` is an eponym - `/foo/foo.md` is an eponym - `/foo/bar.ps1` is not an eponym - - If a package has an an identifier, - any files whose name matches this identifier will be considered an eponym. -.NOTES - Multiple eponyms may exist for a given path. - - Anything before the first period `.` will be considered the name of the file. -#> -param() - -if (-not $this.GetParts) { return } -foreach ($part in $this.GetParts()) { - $partSegments = @($part.Uri -split '/+' -ne '') - if ($partSegments.Count -eq 1) { - if ($partSegments[0] -replace '\..+$' -eq $this.Identifier) { - $part - continue - } - } - if ($partSegments.Count -ge 2) { - if ($partSegments[-2] -eq - ($partSegments[-1] -replace '\..+$') - ) { - $part - continue - } - } -} - - - - - FileContentType - - <# -.SYNOPSIS - Gets file content types -.DESCRIPTION - Gets a table of all files in the package and their associated content types. -#> -$fileContentTypes = [Ordered]@{} -foreach ($part in @($this.GetParts())) { - $fileContentTypes[$part.Uri] = $part.ContentType -} -$fileContentTypes - - - - - FileHash - - <# -.SYNOPSIS - Gets the file hashes -.DESCRIPTION - Gets the file hashes of each part using any supported algorithm (default SHA256) -.NOTES - Supports any algorithm from Get-FileHash -.LINK - Get-FileHash -#> -param([string]$Algorithm = 'SHA256') - -foreach ($part in $this.GetParts()) { - $part.GetHash($Algorithm) -} - - - - - FileList - - <# -.SYNOPSIS - Gets OpenPackage file list -.DESCRIPTION - Gets the list of files in an OpenPackage. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.package.getparts?wt.mc_id=MVP_321542 -#> -[OutputType([string[]])] -param() - -@($this.GetParts()).Uri -as [string[]] - - - - FileSize - - <# -.SYNOPSIS - Gets file content types -.DESCRIPTION - Gets a table of all files in the package and their associated content types. -#> -$fileLengths = [Ordered]@{} -foreach ($part in @($this.GetParts())) { - $partStream = $part.GetStream() - $fileLengths[$part.Uri] = $partStream.Length - $partStream.Close() - $partStream.Dispose() -} -$fileLengths - - - - - Identifier - - $this.PackageProperties.Identifier - - - $this.PackageProperties.Identifier = $args -join ' ' - - - - ImageFileList - - <# -.SYNOPSIS - Gets package image files -.DESCRIPTION - Gets the list of image files within a package. -#> -@(foreach ($part in $this.GetParts()) { - if ($part.ContentType -match 'image/' -or - $part.Uri -match '\.(?>a?png|jpe?g|gif|tiff?|svg|ico|bmp|exr)$' - ) { - $part.Uri - } -}) -as [string[]] - - - - ImportMap - - <# -.SYNOPSIS - Gets a package's `importMap.json` -.DESCRIPTION - Gets the content of any `importMap.json` files in the package -#> -[OutputType([psobject])] -param() - - -$this.GetContent(@($this.FileList -match '\importMap\.json$')) - - - - - - Keywords - - <# -.SYNOPSIS - Gets OpenPackage `Keywords` -.DESCRIPTION - Gets the OpenPackage `Keywords` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Keywords -split '[\s\r\n]+' - - - <# -.SYNOPSIS - Sets OpenPackage `Keywords` -.DESCRIPTION - Sets the OpenPackage `Keywords` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Keywords = $args -join ' ' - - - - Language - - <# -.SYNOPSIS - Gets OpenPackage Language time -.DESCRIPTION - Gets the OpenPackage `Language` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Language - - - <# -.SYNOPSIS - Sets OpenPackage `Language` -.DESCRIPTION - Sets the OpenPackage `Language` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 -#> -param([string]$Language) - -$this.PackageProperties.Language = $Language - - - - LanguagePercent - - <# -.SYNOPSIS - Gets the language percentages of a package -.DESCRIPTION - Gets the language percentages present in the package. -.NOTES - Definitions of what constitutes a language have been quite contentious. - - For the purposes of accurately identifying what lies within a package, we want a very broad definition. - - If you believe a language should be included, file an issue. - - If you believe any given file format is or is not a language, do not file an issue. -#> -$LanguagesByLength = [Ordered]@{} - -$totalLength = 0 -$fileSizes = $this.FileSize -foreach ($part in $this.GetParts()) { - $partLength = $fileSizes[$part.Uri] - - $recognizedLanguage = - switch -regex ($part.Uri) { - '\.3mf$' { '3MF'} - '\.astro' { 'Astro' } - '\.c$' { 'C' } - '\.cast$' { 'Asciiema' } - '\.clixml$' { 'Clixml'} - '\.cjs$' { 'Common JavaScript'} - '\.cpp$' { 'C++' } - '\.cs$' { 'C# '} - '\.csv$' { 'Comma Separated Values' } - '\.csh$' { 'CShell'} - '\.css$' { 'Cascading Stylesheets' } - '(?>/word/.+?\.xml|\.docx?)$' { 'Word '} - '\.dll$' { 'Binary' } - '\.exe$' { 'Binary' } - '\.gif$' { 'GIF' } - '\.go$' { 'Go Language' } - '\.h$' { 'C Header' } - '\.html?$' { 'Hypertext Markup Language' } - '\.java$' {'Java' } - '\.jpe?g$' { 'Joint Pictures Expert Group'} - '\.json$' {'JavaScript Object Notation' } - '\.jsonc$' {'Commented JavaScript Object Notation' } - '\.jsonl$' {'JavaScript Object Notation Lines' } - '\.js$' { 'Javascript'} - '\.jsx$' { 'JavaScript XML'} - '\.(?>md|mdx|markdown)$' { 'Markdown' } - '\.midi?$' { 'MIDI' } - '\.(?>jsm|mjs)$' { 'JavaScript Module'} - '\.mkv$' { 'Matroska Video'} - '\.mka$' { 'Matroska Audio'} - '\.mks$' { 'Matroska Subtitle'} - '\.mk3d$' { 'Matroska Stereoscopic Video'} - '\.mp3$' { 'MP3' } - '\.mp4$' { 'MP4' } - '\.nix$' { 'Nix' } - '\.oog$' { 'OOG' } - '\.pl$' { 'Perl' } - '\.png$' { 'Portable Network Graphics' } - '(?>/ppt/.+?\.xml|\.pptx?)$' { 'PowerPoint'} - '\.psm?1$' { 'PowerShell' } - '\.psd1$' {'PowerShell Data Language' } - '\.ps1xml$' { 'PowerShell Xml' } - '\.py$' { 'Python' } - '\.rs$' { 'Rust '} - '\.rss$' { 'RSS' } - '\.sh$' { 'BourneShell'} - '\.stl$' { 'STL'} - '\.svg$' { 'SVG' } - '\.tar$' { 'Tarfile' } - '(?>\.tar\.gz|\.tgz)$' { 'GZippedTarfile' } - '\.tsx?$' { 'TypeScript' } - '\.tsv$' { 'Tab Separated Values' } - '\.toml$' { 'Tom''s Obvious Minimal Language' } - '\.xhtml$' { 'XHTML' } - '(?>/xl/.+?\.xml|\.xlsx?)$' { 'Excel'} - '\.xsl$' { 'XSL' } - '\.xml$' { 'XML' } - '\.ya?ml$' { 'Yaml' } - '\.zip$' { 'Zip' } - '\.webm' { 'Web Movie' } - '\.weba' { 'Web Audio' } - '\.webp' { 'Web Photo' } - default { 'Unknown' } - } - - if (-not $recognizedLanguage) { - continue - } - - - if (-not $LanguagesByLength[$recognizedLanguage]) { - $LanguagesByLength[$recognizedLanguage] = 0 - } - - $LanguagesByLength[$recognizedLanguage]+=$partLength - - $totalLength += $partLength -} - - -$SortedByLength = [Ordered]@{} - -foreach ($keyValue in $languagesByLength.GetEnumerator() | - Sort-Object Value -Descending -) { - $SortedByLength[$keyValue.Key] = $keyValue.Value / $totalLength -} - -return $SortedByLength - - - - LastModifiedBy - - <# -.SYNOPSIS - Gets OpenPackage LastModifiedBy time -.DESCRIPTION - Gets the OpenPackage `LastModifiedBy` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastmodifiedby?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.LastModifiedBy - - - <# -.SYNOPSIS - Sets OpenPackage `LastModifiedBy` -.DESCRIPTION - Sets the OpenPackage `LastModifiedBy` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.Lastmodifiedby?wt.mc_id=MVP_321542 -#> -param([string]$LastModifiedBy) - -$this.PackageProperties.LastModifiedBy = $LastModifiedBy - - - - LastPrinted - - <# -.SYNOPSIS - Gets OpenPackage LastPrinted time -.DESCRIPTION - Gets the OpenPackage `LastPrinted` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.LastPrinted - - - <# -.SYNOPSIS - Sets OpenPackage `LastPrinted` -.DESCRIPTION - Sets the OpenPackage `LastPrinted` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 -#> -param([DateTime]$LastPrinted) - -$this.PackageProperties.LastPrinted = $LastPrinted - - - - Lexicon - - <# -.SYNOPSIS - Gets any lexicons -.DESCRIPTION - Gets all at protocol lexicons in the package - - A lexicon is defined by the presence of three keys: - - * `id` - * `lexicon` - * `defs - - The output will be a table mapping ids to contents. -#> -[OutputType([Ordered])] -param() - -$allLexicons = [Ordered]@{} - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part that is not json. - if ($part.Uri -notmatch '\.json$') { continue } - # Also ignore any package-lock files - if ($part.Uri -match 'package-lock') { continue } - - # If there is no reader, continue - if (-not $part.Reader) { continue } - - try { - # Try to read our data - $partData = $part.Read() - - # ignore any arrays - if ($partData -is [Object[]]) { continue } - - # If the data has a .lexicon, .id, and .defs - if ($partData.lexicon -and - $partData.id -and - $partData.defs - ) { - # store it in our lexicons table. - $allLexicons[$partData.id] = $partData - } - } catch { - Write-Warning "$($part.Uri) read error $($_)" - continue - } -} -# return all of the lexicons we found. -return $allLexicons - - - - Manifest.json - - <# -.SYNOPSIS - Gets a package's `manifest.json` -.DESCRIPTION - Gets the content of any `manifest.json` files in the package -#> -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named manifest.json - if ($part.Uri -notmatch '/manifest\.json$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Mcp.json - - <# -.SYNOPSIS - Gets a Package's mcp.json -.DESCRIPTION - Gets any mcp definitions in an Open Package - - Definitions can be in parts matching: - * `/mcp.json` - * `/claude_desktop_config.json` - * `/\.?mcp/server.json` -#> -param() - -$pattern = @( - "/mcp\.json" - "/claude_desktop_config\.json" - '/\.?mcp/server.json$' -) - -$pattern = "(?>$($pattern -join '|'))$" - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch $pattern) { - continue - } - - if ($part.Reader) { - try { - $part.Read() - } catch { - Write-Warning "Error reading $($part.Uri) : $_" - } - } else { - $part - } -} - - - - - Modelfile - - <# -.SYNOPSIS - Gets any Modelfiles in a package -.DESCRIPTION - Gets any Ollama model files within a package -.LINK - https://docs.ollama.com/modelfile -#> -[OutputType([string])] -param() - -$partPattern = '[/\.]Modelfile$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -match $partPattern) { - $part.Read() - } -} - - - - Modified - - <# -.SYNOPSIS - Gets OpenPackage modified time -.DESCRIPTION - Gets the OpenPackage `Modified` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Modified - - - <# -.SYNOPSIS - Sets OpenPackage `Modified` -.DESCRIPTION - Sets the OpenPackage `Modified` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 -#> -param([DateTime]$Modified) - -$this.PackageProperties.Modifiedd = $Modified - - - - Nix - - <# -.SYNOPSIS - Gets a package's `*.nix` files -.DESCRIPTION - Gets the content of any `*.nix` files in the package. -#> -[OutputType([string])] -param() - -$this.GetContent(@($this.FileList) -match '\.nix$') - - - - - Nuspec - - <# -.SYNOPSIS - Gets a package's nuspec -.DESCRIPTION - Gets the content of any `*.nuspec` files in the package -#> -[OutputType([xml])] -param() - -$this.GetContent( - $this.FileList -match '\.nuspec$' -) - - - - - Package.json - - <# -.SYNOPSIS - Gets a package's `package.json` -.DESCRIPTION - Gets the content of any `package.json` files in the package -#> -[OutputType([PSObject])] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG - if ($part.Uri -notmatch '/package\.json$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Palette - - <# -.SYNOPSIS - Gets an Open Package palette -.DESCRIPTION - Gets an Open Package's palette. -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param() - -if (-not $this.RelationshipExists -or - -not $this.GetRelationship) { - return -} - -if ($this.RelationshipExists('palette')) { - return $this.GetRelationship('palette').TargetUri -} - - - <# -.SYNOPSIS - Sets an Open Package palette -.DESCRIPTION - Sets an Open Package's palette. -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param( -[Parameter(Mandatory)] -[ValidatePattern('.css$')] -[uri]$Palette -) -if (-not $this.RelationshipExists) { - return -} -if ($this.RelationshipExists('palette')) { - $this.DeleteRelationship('palette') - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} else { - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} - - - - - - PowerShellCommandAst - - <# -.SYNOPSIS - Gets PowerShell Command References -.DESCRIPTION - Gets PowerShell Command Ast references within an Open Package. -#> -foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { - if (-not $content.Ast) { continue } - $content.Ast.FindAll({ - param($ast) - - $ast -is [Management.Automation.Language.CommandAst] - }, $true) | - Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | - Add-Member NoteProperty Package $content.Package -Force -PassThru -} - - - - PowerShellManifest - - <# -.SYNOPSIS - Gets a package's PowerShell manifest files -.DESCRIPTION - Gets a package's PowerShell manifest files. - - These are any `*.psd1` files in the package that: - - * Are valid PowerShell data blocks - * Contain a ModuleVersion -#> -[OutputType([PSObject])] -param() - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch '\.psd1$') { continue } - try { - $psd1 = $part.Read() - if (-not $psd1.ModuleVersion) { continue } - $psd1 - } catch { - Write-Debug "Could not read $($part.Uri): $_" - } -} - - - - PowerShellParameterAst - - <# -.SYNOPSIS - Gets PowerShell Parameter Definitions -.DESCRIPTION - Gets all PowerShell ParameterAst references within an Open Package. -#> -foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { - if (-not $content.Ast) { continue } - $content.Ast.FindAll({ - param($ast) - - $ast -is [Management.Automation.Language.ParameterAst] - }, $true) | - Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | - Add-Member NoteProperty Package $content.Package -Force -PassThru -} - - - - PowerShellTypeAst - - <# -.SYNOPSIS - Gets PowerShell Type References -.DESCRIPTION - Gets PowerShell TypeExpressionAst references within an Open Package. -#> -foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { - if (-not $content.Ast) { continue } - $content.Ast.FindAll({ - param($ast) - - $ast -is [Management.Automation.Language.TypeExpressionAst] - }, $true) | - Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | - Add-Member NoteProperty Package $content.Package -Force -PassThru -} - - - - ProjectFile - - <# -.SYNOPSIS - Gets a package's project files -.DESCRIPTION - Gets the content of any `*.*proj` files in the package -#> -[OutputType([xml])] -param() - -$this.GetContent( - $this.FileList -match '\..+?proj$' -) - - - - README - - <# -.SYNOPSIS - Gets a package's README -.DESCRIPTION - Gets the content of any parts in the package named README.md -#> -[OutputType("text/markdown")] -param() - -# Get all Readme files -foreach ($content in $this.GetContent( - $this.FileList -match '/README\.md$' -)) { - # decorate them as text/markdown - if ($content.pstypenames -notcontains 'text/markdown') { - $content.pstypenames.insert(0, 'text/markdown') - } - # and return them. - $content -} - -return - - - - Revision - - <# -.SYNOPSIS - Gets OpenPackage `Revision` -.DESCRIPTION - Gets the OpenPackage `Revision` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Revision - - - <# -.SYNOPSIS - Sets OpenPackage `Revision` -.DESCRIPTION - Sets the OpenPackage `Revision` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 -#> -param([string]$Revision) - -$this.PackageProperties.Revision = $Revision - - - - Security.md - - <# -.SYNOPSIS - Gets a package's security notice -.DESCRIPTION - Gets any parts in the package named Security.md -#> -[OutputType("text/markdown")] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named SECURITY - if ($part.Uri -notmatch '/SECURITY\.md$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - ServiceWorker.js - - <# -.SYNOPSIS - Gets any service workers in a package -.DESCRIPTION - Gets any clearly named service workers in a package. - - Will find any files named `sw.js` or `ServiceWorker.js` -#> -param() - -$pattern = '/(?>sw|ServiceWorker).js$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -match $pattern) { - if ($part.Reader) { - $part.Read() - } else { - $part - } - } -} - -# We are done. - - - - Subject - - <# -.SYNOPSIS - Gets OpenPackage `Subject` -.DESCRIPTION - Gets the OpenPackage `Subject` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Subject - - - <# -.SYNOPSIS - Sets OpenPackage `Subject` -.DESCRIPTION - Sets the OpenPackage `Subject` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 -#> -param([string]$Subject) - -$this.PackageProperties.Subject = $Subject - - - - Template.json - - <# -.SYNOPSIS - Gets a package's `template.json` -.DESCRIPTION - Gets the content of any `template.json` files in the package -#> -[OutputType([PSObject])] -param() - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch '/template\.json$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - - - - Title - - <# -.SYNOPSIS - Gets OpenPackage `Title` -.DESCRIPTION - Gets the OpenPackage `Title` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Title - - - <# -.SYNOPSIS - Sets OpenPackage `Title` -.DESCRIPTION - Sets the OpenPackage `Title` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 -#> -param([string]$Title) - -$this.PackageProperties.Title = $Title - - - - TypeScriptConfig.json - - <# -.SYNOPSIS - Gets a package's `tsconfig.json` -.DESCRIPTION - Gets the content of any TypeScript configuration `tsconfig.json` files in the package -#> -[OutputType([psobject])] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named manifest.json - if ($part.Uri -notmatch '/tsconfig\.json$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Underscore - - <# -.SYNOPSIS - Gets package underscore files -.DESCRIPTION - Gets underscore files defined in a package. - - These are any files that contain an underscore `_` in their path. - - This returns a series of dictionaries containing the contents of the package -#> -param() - - -return $this.GetTree("/_") - - - - - Version - - <# -.SYNOPSIS - Gets OpenPackage `Version` -.DESCRIPTION - Gets the OpenPackage `Version` property. - - If this has not been explicitly set, looks for potential version informatin -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 -#> -param() - -if ($this.PackageProperties.Version) { - return $this.PackageProperties.Version -} - -$moduleManifest = $this.PowerShellManifest -if ($moduleManifest) { - $this.PackageProperties.Version = $moduleManifest.ModuleVersion - return $this.PackageProperties.Version -} - -$packageJson = $this.'Package.json' -if ($packageJson -and $packageJson.version) { - $this.PackageProperties.Version = $packageJson.version - return $this.PackageProperties.Version -} - - - <# -.SYNOPSIS - Sets OpenPackage `Version` -.DESCRIPTION - Sets the OpenPackage `Version` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 -#> -param([string]$Version) - -$this.PackageProperties.Version = $Version - - - - VsixManifest - - <# -.SYNOPSIS - Gets VsixManfiests from a package -.DESCRIPTION - Gets any Visual Studio Extension Manifests (*.vsixManifest) files in a package. -#> -$partPattern = '\.vsixManifest$' - -$this.GetContent(@( - $this.FileList -match $partPattern -)) - - - - - XRPC - - <# -.SYNOPSIS - Gets package xrpc -.DESCRIPTION - Gets any xrpc parts within the package. -#> -if (-not $this.GetParts) { return } -foreach ($part in $this.GetParts()) { - if ( - $part.Uri -notmatch '/xrpc' -or - $part.Uri -notlike '*.*.*' - ) { - continue - } - $part -} - - - - DefaultDisplay - Identifier -FileList - - - - - OpenPackage - - - PSStandardMembers - - - DefaultDisplayPropertySet - - Identifier - FileList - - - - - - _ - Underscore - - - 11ty - Eleventy - - - Lexicons - Lexicon - - - manifest.psd1 - PowerShellManifest - - - RemovePart - DeletePart - - - tsconfig.json - TypeScriptConfig.json - - - Underbar - Underscore - - - GetAt - - - - GetAtBlob - - - - GetAtProto - - - - GetAtRecord - - - - GetAtType - - - - GetContent - - - - GetDictionary - - - - GetDirectory - - - - GetNuget - - - - GetRepository - - - - GetTar - - - - GetTree - - - - GetUrl - - - - GetZip - - - - Match - - - - SetContent - - - - ShowTreeHtml - - - - ShowTreeText - - - - Astro - - <# -.SYNOPSIS - Gets Open Package Astro Files -.DESCRIPTION - Gets Astro File Content in an Open Package -#> - -param() - -$this.GetContent($this.FileList -match '\.astro$') - - - - Cache - - <# -.SYNOPSIS - Gets cache -.DESCRIPTION - Gets an open package's cache. - - This is an ordered dictionary of data attached to the object, but not saved to disk. -#> -param() - -if (-not $this) { return } - -if (-not $this.'#Cache') { - Add-Member -InputObject $this NoteProperty '#Cache' ([Ordered]@{}) -Force -} - -return $this.'#Cache' - - - - Category - - <# -.SYNOPSIS - Gets OpenPackage `Category` -.DESCRIPTION - Gets the OpenPackage `Category` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Category - - - <# -.SYNOPSIS - Sets OpenPackage `Category` -.DESCRIPTION - Sets the OpenPackage `Category` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 -#> -param([string]$Category) - -$this.PackageProperties.Category = $Category - - - - CHANGELOG.md - - <# -.SYNOPSIS - Gets a package's changelog -.DESCRIPTION - Gets the content of any parts in the package named CHANGELOG.md -#> -[OutputType("text/markdown")] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG - if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - ChocolateyInstall - - <# -.SYNOPSIS - Gets the Chocolatey Install Script -.DESCRIPTION - Gets the Chocolatey Install Script from an OpenPackage, if one is present. - - The Chocolatey install script must be located at /tools/chocolateyInstall.ps1 -#> -param() -$this.GetContent("/tools/chocolateyInstall.ps1") - - - - Claude.md - - <# -.SYNOPSIS - Gets a package's Claude Markdown -.DESCRIPTION - Gets any `claude.md` or `/.claude/*.md` files in an Open Package -.LINK - https://claude.com/blog/using-claude-md-files -#> -foreach ($part in $this.GetParts()) { - if ($part.Uri -match '/CLAUDE\.(?>md|markdown)$' -or - $part.Uri -match '/\.claude/.+?\.(?>md|markdown)$' - ) { - if ($part.Reader) { - $part.Read() - } else { - $part - } - } -} - - - - CodeOfConduct.md - - <# -.SYNOPSIS - Gets a package's code of conduct -.DESCRIPTION - Gets any parts in the package named Code_Of_Conduct.md -#> -[OutputType("text/markdown")] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CODE_OF_CONDUCT - if ($part.Uri -notmatch '/CODE_OF_CONDUCT\.md$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Config.json - - <# -.SYNOPSIS - Gets a package's config json -.DESCRIPTION - Gets the objects stored in any config.json files in the package. -.NOTES - config.json files are used by several static site generators. -#> -[OutputType([PSObject])] -param() - -$partPattern = '[/\+_]config\.json$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch $partPattern) { continue } - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - - - - - Config.yaml - - <# -.SYNOPSIS - Gets a package's config yaml -.DESCRIPTION - Gets the objects stored in any config.yaml files in the package. -.NOTES - config.yaml files are used by several static site generators. -#> -[OutputType([PSObject])] -param() - -$partPattern = '[/\+_]config\.ya?ml$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch $partPattern) { continue } - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - - - - - ContentStatus - - <# -.SYNOPSIS - Gets OpenPackage `ContentStatus` -.DESCRIPTION - Gets the OpenPackage `ContentStatus` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.ContentStatus - - - <# -.SYNOPSIS - Sets OpenPackage `ContentStatus` -.DESCRIPTION - Sets the OpenPackage `ContentStatus` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 -#> -param([string]$ContentStatus) - -$this.PackageProperties.ContentStatus = $ContentStatus - - - - ContentType - - <# -.SYNOPSIS - Gets OpenPackage `ContentType` -.DESCRIPTION - Gets the OpenPackage `ContentType` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.ContentType -split ';' - - - <# -.SYNOPSIS - Sets OpenPackage `ContentType` -.DESCRIPTION - Sets the OpenPackage `ContentType` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 -#> -param([string]$ContentType) - -$this.PackageProperties.ContentType = $ContentType - - - - Contributing.md - - <# -.SYNOPSIS - Gets a package's contribution guide -.DESCRIPTION - Gets any parts in the package named Contributing.md -#> -[OutputType("text/markdown")] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named Contributing - if ($part.Uri -notmatch '/Contributing\.md$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Count - - <# -.SYNOPSIS - Gets the package files count -.DESCRIPTION - Gets the number of files in a package. -#> -return @($this.GetParts()).Length - - - - Created - - <# -.SYNOPSIS - Gets OpenPackage creation time -.DESCRIPTION - Gets the OpenPackage `Created` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Created - - - <# -.SYNOPSIS - Sets OpenPackage `Created` -.DESCRIPTION - Sets the OpenPackage `Created` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 -#> -param([DateTime]$Created) - -$this.PackageProperties.Created = $Created - - - - Creator - - <# -.SYNOPSIS - Gets OpenPackage `Creator` -.DESCRIPTION - Gets the OpenPackage `Creator` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Creator - - - <# -.SYNOPSIS - Sets OpenPackage `Creator` -.DESCRIPTION - Sets the OpenPackage `Creator` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 -#> -param([string]$Creator) - -$this.PackageProperties.Creator = $Creator - - - - Description - - return $this.PackageProperties.Description - - - $this.PackageProperties.Description = $args -join [Environment]::NewLine - - - - Dockerfile - - <# -.SYNOPSIS - Gets a package's dockerfile -.DESCRIPTION - Gets the content of any `Dockerfile`s in the package. -#> -[OutputType([string])] -param() - -$partPattern = '[/\.]DockerFile$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -match $partPattern) { - $part.Read() - } -} - - - - Eleventy - - <# -.SYNOPSIS - Gets a package's eleventy files -.DESCRIPTION - Gets the content of any elevent config files in the package. - - This includes any files named: - * `.eleventy.js` - * `eleventy.config.js` - * `eleventy.config.mjs` - * `eleventy.config.cjs` -.LINK - https://www.11ty.dev/docs/config/ -#> -param() - -$partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' -foreach ($part in $this.GetParts()) { - if ($part.Uri -match $partPattern) { - if ($part.Reader) { - $part.Read() - } else { - $part - } - } -} - - - - - Eponym - - <# -.SYNOPSIS - Gets all Eponyms in a Package -.DESCRIPTION - Gets all Eponyms within an OpenPackage. - - Eponyms are files whose name matches their directory. - - For example: - - `/foo/foo.ps1` is an eponym - `/foo/foo.md` is an eponym - `/foo/bar.ps1` is not an eponym - - If a package has an an identifier, - any files whose name matches this identifier will be considered an eponym. -.NOTES - Multiple eponyms may exist for a given path. - - Anything before the first period `.` will be considered the name of the file. -#> -param() - -if (-not $this.GetParts) { return } -foreach ($part in $this.GetParts()) { - $partSegments = @($part.Uri -split '/+' -ne '') - if ($partSegments.Count -eq 1) { - if ($partSegments[0] -replace '\..+$' -eq $this.Identifier) { - $part - continue - } - } - if ($partSegments.Count -ge 2) { - if ($partSegments[-2] -eq - ($partSegments[-1] -replace '\..+$') - ) { - $part - continue - } - } -} - - - - - FileContentType - - <# -.SYNOPSIS - Gets file content types -.DESCRIPTION - Gets a table of all files in the package and their associated content types. -#> -$fileContentTypes = [Ordered]@{} -foreach ($part in @($this.GetParts())) { - $fileContentTypes[$part.Uri] = $part.ContentType -} -$fileContentTypes - - - - - FileHash - - <# -.SYNOPSIS - Gets the file hashes -.DESCRIPTION - Gets the file hashes of each part using any supported algorithm (default SHA256) -.NOTES - Supports any algorithm from Get-FileHash -.LINK - Get-FileHash -#> -param([string]$Algorithm = 'SHA256') - -foreach ($part in $this.GetParts()) { - $part.GetHash($Algorithm) -} - - - - - FileList - - <# -.SYNOPSIS - Gets OpenPackage file list -.DESCRIPTION - Gets the list of files in an OpenPackage. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.package.getparts?wt.mc_id=MVP_321542 -#> -[OutputType([string[]])] -param() - -@($this.GetParts()).Uri -as [string[]] - - - - FileSize - - <# -.SYNOPSIS - Gets file content types -.DESCRIPTION - Gets a table of all files in the package and their associated content types. -#> -$fileLengths = [Ordered]@{} -foreach ($part in @($this.GetParts())) { - $partStream = $part.GetStream() - $fileLengths[$part.Uri] = $partStream.Length - $partStream.Close() - $partStream.Dispose() -} -$fileLengths - - - - - Identifier - - $this.PackageProperties.Identifier - - - $this.PackageProperties.Identifier = $args -join ' ' - - - - ImageFileList - - <# -.SYNOPSIS - Gets package image files -.DESCRIPTION - Gets the list of image files within a package. -#> -@(foreach ($part in $this.GetParts()) { - if ($part.ContentType -match 'image/' -or - $part.Uri -match '\.(?>a?png|jpe?g|gif|tiff?|svg|ico|bmp|exr)$' - ) { - $part.Uri - } -}) -as [string[]] - - - - ImportMap - - <# -.SYNOPSIS - Gets a package's `importMap.json` -.DESCRIPTION - Gets the content of any `importMap.json` files in the package -#> -[OutputType([psobject])] -param() - - -$this.GetContent(@($this.FileList -match '\importMap\.json$')) - - - - - - Keywords - - <# -.SYNOPSIS - Gets OpenPackage `Keywords` -.DESCRIPTION - Gets the OpenPackage `Keywords` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Keywords -split '[\s\r\n]+' - - - <# -.SYNOPSIS - Sets OpenPackage `Keywords` -.DESCRIPTION - Sets the OpenPackage `Keywords` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Keywords = $args -join ' ' - - - - Language - - <# -.SYNOPSIS - Gets OpenPackage Language time -.DESCRIPTION - Gets the OpenPackage `Language` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Language - - - <# -.SYNOPSIS - Sets OpenPackage `Language` -.DESCRIPTION - Sets the OpenPackage `Language` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.language?wt.mc_id=MVP_321542 -#> -param([string]$Language) - -$this.PackageProperties.Language = $Language - - - - LanguagePercent - - <# -.SYNOPSIS - Gets the language percentages of a package -.DESCRIPTION - Gets the language percentages present in the package. -.NOTES - Definitions of what constitutes a language have been quite contentious. - - For the purposes of accurately identifying what lies within a package, we want a very broad definition. - - If you believe a language should be included, file an issue. - - If you believe any given file format is or is not a language, do not file an issue. -#> -$LanguagesByLength = [Ordered]@{} - -$totalLength = 0 -$fileSizes = $this.FileSize -foreach ($part in $this.GetParts()) { - $partLength = $fileSizes[$part.Uri] - - $recognizedLanguage = - switch -regex ($part.Uri) { - '\.3mf$' { '3MF'} - '\.astro' { 'Astro' } - '\.c$' { 'C' } - '\.cast$' { 'Asciiema' } - '\.clixml$' { 'Clixml'} - '\.cjs$' { 'Common JavaScript'} - '\.cpp$' { 'C++' } - '\.cs$' { 'C# '} - '\.csv$' { 'Comma Separated Values' } - '\.csh$' { 'CShell'} - '\.css$' { 'Cascading Stylesheets' } - '(?>/word/.+?\.xml|\.docx?)$' { 'Word '} - '\.dll$' { 'Binary' } - '\.exe$' { 'Binary' } - '\.gif$' { 'GIF' } - '\.go$' { 'Go Language' } - '\.h$' { 'C Header' } - '\.html?$' { 'Hypertext Markup Language' } - '\.java$' {'Java' } - '\.jpe?g$' { 'Joint Pictures Expert Group'} - '\.json$' {'JavaScript Object Notation' } - '\.jsonc$' {'Commented JavaScript Object Notation' } - '\.jsonl$' {'JavaScript Object Notation Lines' } - '\.js$' { 'Javascript'} - '\.jsx$' { 'JavaScript XML'} - '\.(?>md|mdx|markdown)$' { 'Markdown' } - '\.midi?$' { 'MIDI' } - '\.(?>jsm|mjs)$' { 'JavaScript Module'} - '\.mkv$' { 'Matroska Video'} - '\.mka$' { 'Matroska Audio'} - '\.mks$' { 'Matroska Subtitle'} - '\.mk3d$' { 'Matroska Stereoscopic Video'} - '\.mp3$' { 'MP3' } - '\.mp4$' { 'MP4' } - '\.nix$' { 'Nix' } - '\.oog$' { 'OOG' } - '\.pl$' { 'Perl' } - '\.png$' { 'Portable Network Graphics' } - '(?>/ppt/.+?\.xml|\.pptx?)$' { 'PowerPoint'} - '\.psm?1$' { 'PowerShell' } - '\.psd1$' {'PowerShell Data Language' } - '\.ps1xml$' { 'PowerShell Xml' } - '\.py$' { 'Python' } - '\.rs$' { 'Rust '} - '\.rss$' { 'RSS' } - '\.sh$' { 'BourneShell'} - '\.stl$' { 'STL'} - '\.svg$' { 'SVG' } - '\.tar$' { 'Tarfile' } - '(?>\.tar\.gz|\.tgz)$' { 'GZippedTarfile' } - '\.tsx?$' { 'TypeScript' } - '\.tsv$' { 'Tab Separated Values' } - '\.toml$' { 'Tom''s Obvious Minimal Language' } - '\.xhtml$' { 'XHTML' } - '(?>/xl/.+?\.xml|\.xlsx?)$' { 'Excel'} - '\.xsl$' { 'XSL' } - '\.xml$' { 'XML' } - '\.ya?ml$' { 'Yaml' } - '\.zip$' { 'Zip' } - '\.webm' { 'Web Movie' } - '\.weba' { 'Web Audio' } - '\.webp' { 'Web Photo' } - default { 'Unknown' } - } - - if (-not $recognizedLanguage) { - continue - } - - - if (-not $LanguagesByLength[$recognizedLanguage]) { - $LanguagesByLength[$recognizedLanguage] = 0 - } - - $LanguagesByLength[$recognizedLanguage]+=$partLength - - $totalLength += $partLength -} - - -$SortedByLength = [Ordered]@{} - -foreach ($keyValue in $languagesByLength.GetEnumerator() | - Sort-Object Value -Descending -) { - $SortedByLength[$keyValue.Key] = $keyValue.Value / $totalLength -} - -return $SortedByLength - - - - LastModifiedBy - - <# -.SYNOPSIS - Gets OpenPackage LastModifiedBy time -.DESCRIPTION - Gets the OpenPackage `LastModifiedBy` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastmodifiedby?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.LastModifiedBy - - - <# -.SYNOPSIS - Sets OpenPackage `LastModifiedBy` -.DESCRIPTION - Sets the OpenPackage `LastModifiedBy` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.Lastmodifiedby?wt.mc_id=MVP_321542 -#> -param([string]$LastModifiedBy) - -$this.PackageProperties.LastModifiedBy = $LastModifiedBy - - - - LastPrinted - - <# -.SYNOPSIS - Gets OpenPackage LastPrinted time -.DESCRIPTION - Gets the OpenPackage `LastPrinted` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.LastPrinted - - - <# -.SYNOPSIS - Sets OpenPackage `LastPrinted` -.DESCRIPTION - Sets the OpenPackage `LastPrinted` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.lastprinted?wt.mc_id=MVP_321542 -#> -param([DateTime]$LastPrinted) - -$this.PackageProperties.LastPrinted = $LastPrinted - - - - Lexicon - - <# -.SYNOPSIS - Gets any lexicons -.DESCRIPTION - Gets all at protocol lexicons in the package - - A lexicon is defined by the presence of three keys: - - * `id` - * `lexicon` - * `defs - - The output will be a table mapping ids to contents. -#> -[OutputType([Ordered])] -param() - -$allLexicons = [Ordered]@{} - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part that is not json. - if ($part.Uri -notmatch '\.json$') { continue } - # Also ignore any package-lock files - if ($part.Uri -match 'package-lock') { continue } - - # If there is no reader, continue - if (-not $part.Reader) { continue } - - try { - # Try to read our data - $partData = $part.Read() - - # ignore any arrays - if ($partData -is [Object[]]) { continue } - - # If the data has a .lexicon, .id, and .defs - if ($partData.lexicon -and - $partData.id -and - $partData.defs - ) { - # store it in our lexicons table. - $allLexicons[$partData.id] = $partData - } - } catch { - Write-Warning "$($part.Uri) read error $($_)" - continue - } -} -# return all of the lexicons we found. -return $allLexicons - - - - Manifest.json - - <# -.SYNOPSIS - Gets a package's `manifest.json` -.DESCRIPTION - Gets the content of any `manifest.json` files in the package -#> -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named manifest.json - if ($part.Uri -notmatch '/manifest\.json$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Mcp.json - - <# -.SYNOPSIS - Gets a Package's mcp.json -.DESCRIPTION - Gets any mcp definitions in an Open Package - - Definitions can be in parts matching: - * `/mcp.json` - * `/claude_desktop_config.json` - * `/\.?mcp/server.json` -#> -param() - -$pattern = @( - "/mcp\.json" - "/claude_desktop_config\.json" - '/\.?mcp/server.json$' -) - -$pattern = "(?>$($pattern -join '|'))$" - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch $pattern) { - continue - } - - if ($part.Reader) { - try { - $part.Read() - } catch { - Write-Warning "Error reading $($part.Uri) : $_" - } - } else { - $part - } -} - - - - - Modelfile - - <# -.SYNOPSIS - Gets any Modelfiles in a package -.DESCRIPTION - Gets any Ollama model files within a package -.LINK - https://docs.ollama.com/modelfile -#> -[OutputType([string])] -param() - -$partPattern = '[/\.]Modelfile$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -match $partPattern) { - $part.Read() - } -} - - - - Modified - - <# -.SYNOPSIS - Gets OpenPackage modified time -.DESCRIPTION - Gets the OpenPackage `Modified` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Modified - - - <# -.SYNOPSIS - Sets OpenPackage `Modified` -.DESCRIPTION - Sets the OpenPackage `Modified` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 -#> -param([DateTime]$Modified) - -$this.PackageProperties.Modifiedd = $Modified - - - - Nix - - <# -.SYNOPSIS - Gets a package's `*.nix` files -.DESCRIPTION - Gets the content of any `*.nix` files in the package. -#> -[OutputType([string])] -param() - -$this.GetContent(@($this.FileList) -match '\.nix$') - - - - - Nuspec - - <# -.SYNOPSIS - Gets a package's nuspec -.DESCRIPTION - Gets the content of any `*.nuspec` files in the package -#> -[OutputType([xml])] -param() - -$this.GetContent( - $this.FileList -match '\.nuspec$' -) - - - - - Package.json - - <# -.SYNOPSIS - Gets a package's `package.json` -.DESCRIPTION - Gets the content of any `package.json` files in the package -#> -[OutputType([PSObject])] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named CHANGELOG - if ($part.Uri -notmatch '/package\.json$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Palette - - <# -.SYNOPSIS - Gets an Open Package palette -.DESCRIPTION - Gets an Open Package's palette. -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param() - -if (-not $this.RelationshipExists -or - -not $this.GetRelationship) { - return -} - -if ($this.RelationshipExists('palette')) { - return $this.GetRelationship('palette').TargetUri -} - - - <# -.SYNOPSIS - Sets an Open Package palette -.DESCRIPTION - Sets an Open Package's palette. -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param( -[Parameter(Mandatory)] -[ValidatePattern('.css$')] -[uri]$Palette -) -if (-not $this.RelationshipExists) { - return -} -if ($this.RelationshipExists('palette')) { - $this.DeleteRelationship('palette') - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} else { - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} - - - - - - PowerShellCommandAst - - <# -.SYNOPSIS - Gets PowerShell Command References -.DESCRIPTION - Gets PowerShell Command Ast references within an Open Package. -#> -foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { - if (-not $content.Ast) { continue } - $content.Ast.FindAll({ - param($ast) - - $ast -is [Management.Automation.Language.CommandAst] - }, $true) | - Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | - Add-Member NoteProperty Package $content.Package -Force -PassThru -} - - - - PowerShellManifest - - <# -.SYNOPSIS - Gets a package's PowerShell manifest files -.DESCRIPTION - Gets a package's PowerShell manifest files. - - These are any `*.psd1` files in the package that: - - * Are valid PowerShell data blocks - * Contain a ModuleVersion -#> -[OutputType([PSObject])] -param() - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch '\.psd1$') { continue } - try { - $psd1 = $part.Read() - if (-not $psd1.ModuleVersion) { continue } - $psd1 - } catch { - Write-Debug "Could not read $($part.Uri): $_" - } -} - - - - PowerShellParameterAst - - <# -.SYNOPSIS - Gets PowerShell Parameter Definitions -.DESCRIPTION - Gets all PowerShell ParameterAst references within an Open Package. -#> -foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { - if (-not $content.Ast) { continue } - $content.Ast.FindAll({ - param($ast) - - $ast -is [Management.Automation.Language.ParameterAst] - }, $true) | - Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | - Add-Member NoteProperty Package $content.Package -Force -PassThru -} - - - - PowerShellTypeAst - - <# -.SYNOPSIS - Gets PowerShell Type References -.DESCRIPTION - Gets PowerShell TypeExpressionAst references within an Open Package. -#> -foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { - if (-not $content.Ast) { continue } - $content.Ast.FindAll({ - param($ast) - - $ast -is [Management.Automation.Language.TypeExpressionAst] - }, $true) | - Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | - Add-Member NoteProperty Package $content.Package -Force -PassThru -} - - - - ProjectFile - - <# -.SYNOPSIS - Gets a package's project files -.DESCRIPTION - Gets the content of any `*.*proj` files in the package -#> -[OutputType([xml])] -param() - -$this.GetContent( - $this.FileList -match '\..+?proj$' -) - - - - README - - <# -.SYNOPSIS - Gets a package's README -.DESCRIPTION - Gets the content of any parts in the package named README.md -#> -[OutputType("text/markdown")] -param() - -# Get all Readme files -foreach ($content in $this.GetContent( - $this.FileList -match '/README\.md$' -)) { - # decorate them as text/markdown - if ($content.pstypenames -notcontains 'text/markdown') { - $content.pstypenames.insert(0, 'text/markdown') - } - # and return them. - $content -} - -return - - - - Revision - - <# -.SYNOPSIS - Gets OpenPackage `Revision` -.DESCRIPTION - Gets the OpenPackage `Revision` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Revision - - - <# -.SYNOPSIS - Sets OpenPackage `Revision` -.DESCRIPTION - Sets the OpenPackage `Revision` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 -#> -param([string]$Revision) - -$this.PackageProperties.Revision = $Revision - - - - Security.md - - <# -.SYNOPSIS - Gets a package's security notice -.DESCRIPTION - Gets any parts in the package named Security.md -#> -[OutputType("text/markdown")] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named SECURITY - if ($part.Uri -notmatch '/SECURITY\.md$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - ServiceWorker.js - - <# -.SYNOPSIS - Gets any service workers in a package -.DESCRIPTION - Gets any clearly named service workers in a package. - - Will find any files named `sw.js` or `ServiceWorker.js` -#> -param() - -$pattern = '/(?>sw|ServiceWorker).js$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -match $pattern) { - if ($part.Reader) { - $part.Read() - } else { - $part - } - } -} - -# We are done. - - - - Subject - - <# -.SYNOPSIS - Gets OpenPackage `Subject` -.DESCRIPTION - Gets the OpenPackage `Subject` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Subject - - - <# -.SYNOPSIS - Sets OpenPackage `Subject` -.DESCRIPTION - Sets the OpenPackage `Subject` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 -#> -param([string]$Subject) - -$this.PackageProperties.Subject = $Subject - - - - Template.json - - <# -.SYNOPSIS - Gets a package's `template.json` -.DESCRIPTION - Gets the content of any `template.json` files in the package -#> -[OutputType([PSObject])] -param() - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch '/template\.json$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - - - - Title - - <# -.SYNOPSIS - Gets OpenPackage `Title` -.DESCRIPTION - Gets the OpenPackage `Title` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 -#> -param() - -$this.PackageProperties.Title - - - <# -.SYNOPSIS - Sets OpenPackage `Title` -.DESCRIPTION - Sets the OpenPackage `Title` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 -#> -param([string]$Title) - -$this.PackageProperties.Title = $Title - - - - TypeScriptConfig.json - - <# -.SYNOPSIS - Gets a package's `tsconfig.json` -.DESCRIPTION - Gets the content of any TypeScript configuration `tsconfig.json` files in the package -#> -[OutputType([psobject])] -param() - -# Get every part -foreach ($part in $this.GetParts()) { - # and ignore any part not named manifest.json - if ($part.Uri -notmatch '/tsconfig\.json$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part - } -} - -# We are done. - - - - Underscore - - <# -.SYNOPSIS - Gets package underscore files -.DESCRIPTION - Gets underscore files defined in a package. - - These are any files that contain an underscore `_` in their path. - - This returns a series of dictionaries containing the contents of the package -#> -param() - - -return $this.GetTree("/_") - - - - - Version - - <# -.SYNOPSIS - Gets OpenPackage `Version` -.DESCRIPTION - Gets the OpenPackage `Version` property. - - If this has not been explicitly set, looks for potential version informatin -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 -#> -param() - -if ($this.PackageProperties.Version) { - return $this.PackageProperties.Version -} - -$moduleManifest = $this.PowerShellManifest -if ($moduleManifest) { - $this.PackageProperties.Version = $moduleManifest.ModuleVersion - return $this.PackageProperties.Version -} - -$packageJson = $this.'Package.json' -if ($packageJson -and $packageJson.version) { - $this.PackageProperties.Version = $packageJson.version - return $this.PackageProperties.Version -} - - - <# -.SYNOPSIS - Sets OpenPackage `Version` -.DESCRIPTION - Sets the OpenPackage `Version` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 -#> -param([string]$Version) - -$this.PackageProperties.Version = $Version - - - - VsixManifest - - <# -.SYNOPSIS - Gets VsixManfiests from a package -.DESCRIPTION - Gets any Visual Studio Extension Manifests (*.vsixManifest) files in a package. -#> -$partPattern = '\.vsixManifest$' - -$this.GetContent(@( - $this.FileList -match $partPattern -)) - - - - - XRPC - - <# -.SYNOPSIS - Gets package xrpc -.DESCRIPTION - Gets any xrpc parts within the package. -#> -if (-not $this.GetParts) { return } -foreach ($part in $this.GetParts()) { - if ( - $part.Uri -notmatch '/xrpc' -or - $part.Uri -notlike '*.*.*' - ) { - continue - } - $part -} - - - - DefaultDisplay - Identifier -FileList - - - System.IO.Packaging.Package @@ -7900,8 +134,6 @@ if (-not $package) { # create one $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') - $package.pstypenames.insert(0, 'OP') - $package.pstypenames.insert(0, 'OpenPackage') Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force } @@ -8590,8 +822,6 @@ foreach ($dictionary in $dictionaryList) { if (-not $package) { $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') - $package.pstypenames.insert(0, 'OP') - $package.pstypenames.insert(0, 'OpenPackage') $package.PackageProperties.Identifier = $resolvedItem.Name Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force } @@ -8766,8 +996,6 @@ foreach ($resolvedItem in $resolvedItems) { if (-not $package) { $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') - $package.pstypenames.insert(0, 'OP') - $package.pstypenames.insert(0, 'OpenPackage') if ($resolvedItem.Parent.Name -and ( $resolvedItem.Name -as [version] -or @@ -8989,9 +1217,7 @@ if ($nuget.Segments[1] -match 'api') { if ($downloadPackage.Content -is [byte[]]) { $memoryStream = [IO.MemoryStream]::new($downloadPackage.Content) $nugetPackage = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') - $nugetPackage | Add-Member NoteProperty MemoryStream $memoryStream -Force - $nugetPackage.pstypenames.insert(0, 'OP') - $nugetPackage.pstypenames.insert(0, 'OpenPackage') + $nugetPackage | Add-Member NoteProperty MemoryStream $memoryStream -Force $nugetPackage } } else { @@ -9568,13 +1794,7 @@ foreach ($uri in $url) { $slashKey = "$uri" -replace "^\.?/?", '/' # try to find it in the package if we have one if ($Package -and $Package -is [IO.Packaging.Package] -and - $Package.PartExists($slashKey)) { - if ($Package.pstypenames -notcontains 'OP') { - $Package.pstypenames.add('OP') - } - if ($Package.pstypenames -notcontains 'OpenPackage') { - $Package.pstypenames.add('OpenPackage') - } + $Package.PartExists($slashKey)) { $Package.GetContent($slashKey) } @@ -9609,9 +1829,6 @@ foreach ($uri in $url) { } # Attach the memory stream to the package $currentPackage | Add-Member NoteProperty MemoryStream $memoryStream -Force - # and decorate its type names - $currentPackage.pstypenames.insert(0, 'OP') - $currentPackage.pstypenames.insert(0, 'OpenPackage') # and emit the package. $currentPackage } else { @@ -9811,10 +2028,7 @@ foreach ($resolvedItem in Get-Item -Path $ZipFile) { $currentPackage = $currentPackage | Add-Member NoteProperty FilePath $filePath -Force -PassThru | Add-Member NoteProperty MemoryStream $memoryStream -Force -PassThru - - $currentPackage.pstypenames.insert(0, 'OP') - $currentPackage.pstypenames.insert(0, 'OpenPackage') - + # If there is no identifier, set it to the file name if (-not $currentPackage.PackageProperties.Identifier) { $currentPackage.PackageProperties.Identifier = $resolvedItem.Name From 0ac2c7309f3aaa8b4ad5fd3ae597bf24b69e5a39 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:25:55 -0700 Subject: [PATCH 465/724] feat: `OpenPackage.Source.At` ( Fixes #150 ) Moving sources into OpenPackage.Source --- Types/{OpenPackage/GetAt.ps1 => OpenPackage.Source/At.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetAt.ps1 => OpenPackage.Source/At.ps1} (100%) diff --git a/Types/OpenPackage/GetAt.ps1 b/Types/OpenPackage.Source/At.ps1 similarity index 100% rename from Types/OpenPackage/GetAt.ps1 rename to Types/OpenPackage.Source/At.ps1 From 6f7fc91b4757e8ab43294e672df99b425f960927 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:26:51 -0700 Subject: [PATCH 466/724] feat: `OpenPackage.Source.AtBlob` ( Fixes #112 ) Moving sources into OpenPackage.Source --- .../{OpenPackage/GetAtBlob.ps1 => OpenPackage.Source/AtBlob.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetAtBlob.ps1 => OpenPackage.Source/AtBlob.ps1} (100%) diff --git a/Types/OpenPackage/GetAtBlob.ps1 b/Types/OpenPackage.Source/AtBlob.ps1 similarity index 100% rename from Types/OpenPackage/GetAtBlob.ps1 rename to Types/OpenPackage.Source/AtBlob.ps1 From ea625930caf0462ab21b914344547fcdb035df1d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:27:31 -0700 Subject: [PATCH 467/724] feat: `OpenPackage.Source.AtRecord` ( Fixes #113 ) Moving sources into OpenPackage.Source --- .../GetAtRecord.ps1 => OpenPackage.Source/AtRecord.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetAtRecord.ps1 => OpenPackage.Source/AtRecord.ps1} (100%) diff --git a/Types/OpenPackage/GetAtRecord.ps1 b/Types/OpenPackage.Source/AtRecord.ps1 similarity index 100% rename from Types/OpenPackage/GetAtRecord.ps1 rename to Types/OpenPackage.Source/AtRecord.ps1 From 81b1a2d7166602ed053cc31eb098bf5f9cf49907 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:28:29 -0700 Subject: [PATCH 468/724] feat: `OpenPackage.Source.AtProtocol` ( Fixes #125 ) Moving sources into OpenPackage.Source --- .../GetAtProto.ps1 => OpenPackage.Source/AtProtocol.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetAtProto.ps1 => OpenPackage.Source/AtProtocol.ps1} (100%) diff --git a/Types/OpenPackage/GetAtProto.ps1 b/Types/OpenPackage.Source/AtProtocol.ps1 similarity index 100% rename from Types/OpenPackage/GetAtProto.ps1 rename to Types/OpenPackage.Source/AtProtocol.ps1 From 84a7cecf8bd7711d9a5725817d5da17900e2c050 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:29:23 -0700 Subject: [PATCH 469/724] feat: `OpenPackage.Source.AtType` ( Fixes #114 ) Moving sources into OpenPackage.Source --- .../{OpenPackage/GetAtType.ps1 => OpenPackage.Source/AtType.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetAtType.ps1 => OpenPackage.Source/AtType.ps1} (100%) diff --git a/Types/OpenPackage/GetAtType.ps1 b/Types/OpenPackage.Source/AtType.ps1 similarity index 100% rename from Types/OpenPackage/GetAtType.ps1 rename to Types/OpenPackage.Source/AtType.ps1 From cfc6ee8aa9f2ac57a459527d7719bf08704daf13 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:30:15 -0700 Subject: [PATCH 470/724] feat: `OpenPackage.Source.Dictionary` ( Fixes #120 ) Moving sources into OpenPackage.Source --- .../GetDictionary.ps1 => OpenPackage.Source/Dictionary.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetDictionary.ps1 => OpenPackage.Source/Dictionary.ps1} (100%) diff --git a/Types/OpenPackage/GetDictionary.ps1 b/Types/OpenPackage.Source/Dictionary.ps1 similarity index 100% rename from Types/OpenPackage/GetDictionary.ps1 rename to Types/OpenPackage.Source/Dictionary.ps1 From 76c31008218dca4c614149d615c68714dd4fee15 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:31:10 -0700 Subject: [PATCH 471/724] feat: `OpenPackage.Source.Directory` ( Fixes #116 ) Moving sources into OpenPackage.Source --- .../GetDirectory.ps1 => OpenPackage.Source/Directory.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetDirectory.ps1 => OpenPackage.Source/Directory.ps1} (100%) diff --git a/Types/OpenPackage/GetDirectory.ps1 b/Types/OpenPackage.Source/Directory.ps1 similarity index 100% rename from Types/OpenPackage/GetDirectory.ps1 rename to Types/OpenPackage.Source/Directory.ps1 From af227af7212f3b1c2c0b401c10bcbbfb57c6b593 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:31:41 -0700 Subject: [PATCH 472/724] feat: `OpenPackage.Source.Nuget` ( Fixes #111 ) Moving sources into OpenPackage.Source --- Types/{OpenPackage/GetNuget.ps1 => OpenPackage.Source/Nuget.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetNuget.ps1 => OpenPackage.Source/Nuget.ps1} (100%) diff --git a/Types/OpenPackage/GetNuget.ps1 b/Types/OpenPackage.Source/Nuget.ps1 similarity index 100% rename from Types/OpenPackage/GetNuget.ps1 rename to Types/OpenPackage.Source/Nuget.ps1 From be0ed37a2c06efb10c019bb6821f405396b6064b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:32:20 -0700 Subject: [PATCH 473/724] feat: `OpenPackage.Source.Repository` ( Fixes #115 ) Moving sources into OpenPackage.Source --- .../GetRepository.ps1 => OpenPackage.Source/Repository.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetRepository.ps1 => OpenPackage.Source/Repository.ps1} (100%) diff --git a/Types/OpenPackage/GetRepository.ps1 b/Types/OpenPackage.Source/Repository.ps1 similarity index 100% rename from Types/OpenPackage/GetRepository.ps1 rename to Types/OpenPackage.Source/Repository.ps1 From 9b4f9515233fb39055ca88b0685cd13facfe79cb Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:32:57 -0700 Subject: [PATCH 474/724] feat: `OpenPackage.Source.Tar` ( Fixes #117 ) Moving sources into OpenPackage.Source --- Types/{OpenPackage/GetTar.ps1 => OpenPackage.Source/Tar.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetTar.ps1 => OpenPackage.Source/Tar.ps1} (100%) diff --git a/Types/OpenPackage/GetTar.ps1 b/Types/OpenPackage.Source/Tar.ps1 similarity index 100% rename from Types/OpenPackage/GetTar.ps1 rename to Types/OpenPackage.Source/Tar.ps1 From 85d61b53bb5fec90955dd08c59d0d9bc18011c48 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:33:25 -0700 Subject: [PATCH 475/724] feat: `OpenPackage.Source.Url` ( Fixes #119 ) Moving sources into OpenPackage.Source --- Types/{OpenPackage/GetUrl.ps1 => OpenPackage.Source/Url.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetUrl.ps1 => OpenPackage.Source/Url.ps1} (100%) diff --git a/Types/OpenPackage/GetUrl.ps1 b/Types/OpenPackage.Source/Url.ps1 similarity index 100% rename from Types/OpenPackage/GetUrl.ps1 rename to Types/OpenPackage.Source/Url.ps1 From 7dc71d284b77589e05ace07cef2eeecec3bda00e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:33:51 -0700 Subject: [PATCH 476/724] feat: `OpenPackage.Source.Zip` ( Fixes #118 ) Moving sources into OpenPackage.Source --- Types/{OpenPackage/GetZip.ps1 => OpenPackage.Source/Zip.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/{OpenPackage/GetZip.ps1 => OpenPackage.Source/Zip.ps1} (100%) diff --git a/Types/OpenPackage/GetZip.ps1 b/Types/OpenPackage.Source/Zip.ps1 similarity index 100% rename from Types/OpenPackage/GetZip.ps1 rename to Types/OpenPackage.Source/Zip.ps1 From 27ed09a53a6a9c86c3d098b4f8c79bca413e8fc0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:36:48 -0700 Subject: [PATCH 477/724] feat: `Get-OpenPackage` ( Fixes #2 ) Using OpenPackage.Source and renaming methods accordingly --- Commands/Get-OpenPackage.ps1 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 5dbd78c..2dd25bc 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -254,7 +254,7 @@ function Get-OpenPackage $typeMap = ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap # Gets the open package type data, as we will need this - $OpTypeData = Get-TypeData -TypeName System.IO.Packaging.Package + $OpTypeData = Get-TypeData -TypeName OpenPackage.Source function InvokeOpMethod { param([string]$Name, [Collections.IDictionary]$Parameter) @@ -383,12 +383,12 @@ function Get-OpenPackage filter packTar { $namedParameters['TarFile'] = $_ - InvokeOpMethod 'GetTar' $NamedParameters + InvokeOpMethod 'Tar' $NamedParameters } filter packZip { $namedParameters['ZipFile'] = $_ - InvokeOpMethod 'GetZip' $NamedParameters + InvokeOpMethod 'Zip' $NamedParameters } $generateEvent = [Runspace]::DefaultRunspace.Events.GenerateEvent @@ -615,14 +615,14 @@ function Get-OpenPackage # pack it up $namedParameters['Url'] = $uri $namedParameters.Remove('Uri') - InvokeOpMethod 'GetUrl' $NamedParameters + InvokeOpMethod 'Url' $NamedParameters return } #region Open Package in At Syntax if ($At) { $NamedParameters['At'] = $At - InvokeOpMethod 'GetAt' $NamedParameters + InvokeOpMethod 'At' $NamedParameters return } #endregion Open Package in At Syntax @@ -630,7 +630,7 @@ function Get-OpenPackage #region Open Package from Repository if ($Repository) { $NamedParameters['Repository'] = $Repository - InvokeOpMethod 'GetRepository' $NamedParameters + InvokeOpMethod 'Repository' $NamedParameters return } #endregion Open Package from Repository @@ -638,7 +638,7 @@ function Get-OpenPackage #region Open Package from At Uri if ($AtUri) { $NamedParameters['AtUri'] = $AtUri - InvokeOpMethod 'GetAtProto' $NamedParameters + InvokeOpMethod 'AtProto' $NamedParameters return } #endregion Open Package From At Uri @@ -664,14 +664,14 @@ function Get-OpenPackage if ($Dictionary) { $namedParameters['DictionaryList'] = $_ - InvokeOpMethod 'GetDictionary' $NamedParameters + InvokeOpMethod 'Dictionary' $NamedParameters return } #region Open Package from Nuget if ($Nuget) { # Try to call our GetNuget method, and pass the `$Nuget` - InvokeOpMethod 'GetNuget' $PSBoundParameters + InvokeOpMethod 'Nuget' $PSBoundParameters return } #endregion Open Package from Nuget @@ -703,7 +703,7 @@ function Get-OpenPackage Push-Location -LiteralPath $resolvedItem.FullName # Get all files beneath this point $namedParameters['Directory'] = $resolvedItem.FullName - InvokeOpMethod 'GetDirectory' $NamedParameters + InvokeOpMethod 'Directory' $NamedParameters Pop-Location # make packages from the directory } From 69740aad4e871907e53b43bc16da6e4bf25f4ce7 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 19:37:13 +0000 Subject: [PATCH 478/724] feat: `Get-OpenPackage` ( Fixes #2 ) Using OpenPackage.Source and renaming methods accordingly --- OP.types.ps1xml | 10797 +++++++++++++++++++++++----------------------- 1 file changed, 5401 insertions(+), 5396 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f3638b2..48f6107 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -44,2353 +44,1502 @@ Underscore - GetAt + GetContent - GetAtBlob + GetTree - GetAtProto + Match - GetAtRecord + SetContent - GetAtType + ShowTreeHtml - GetContent + ShowTreeText - - GetDictionary - - - - GetDirectory - - - - GetNuget - - - - GetRepository - - - - GetTar - - - - GetTree - - - - GetUrl - - - - GetZip - - - - Match - - - - SetContent - - - - ShowTreeHtml - - - - ShowTreeText - - + + - Astro + Modelfile <# .SYNOPSIS - Gets Open Package Astro Files + Gets any Modelfiles in a package .DESCRIPTION - Gets Astro File Content in an Open Package + Gets any Ollama model files within a package +.LINK + https://docs.ollama.com/modelfile #> - +[OutputType([string])] param() -$this.GetContent($this.FileList -match '\.astro$') +$partPattern = '[/\.]Modelfile$' + +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $partPattern) { + $part.Read() + } +} - Cache + Modified <# .SYNOPSIS - Gets cache + Gets OpenPackage modified time .DESCRIPTION - Gets an open package's cache. - - This is an ordered dictionary of data attached to the object, but not saved to disk. + Gets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 #> param() -if (-not $this) { return } - -if (-not $this.'#Cache') { - Add-Member -InputObject $this NoteProperty '#Cache' ([Ordered]@{}) -Force -} - -return $this.'#Cache' +$this.PackageProperties.Modified + + <# +.SYNOPSIS + Sets OpenPackage `Modified` +.DESCRIPTION + Sets the OpenPackage `Modified` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.modified?wt.mc_id=MVP_321542 +#> +param([DateTime]$Modified) + +$this.PackageProperties.Modifiedd = $Modified + - Category + Nix <# .SYNOPSIS - Gets OpenPackage `Category` + Gets a package's `*.nix` files .DESCRIPTION - Gets the OpenPackage `Category` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 + Gets the content of any `*.nix` files in the package. #> +[OutputType([string])] param() -$this.PackageProperties.Category +$this.GetContent(@($this.FileList) -match '\.nix$') + - + + + Nuspec + <# .SYNOPSIS - Sets OpenPackage `Category` + Gets a package's nuspec .DESCRIPTION - Sets the OpenPackage `Category` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.category?wt.mc_id=MVP_321542 + Gets the content of any `*.nuspec` files in the package #> -param([string]$Category) +[OutputType([xml])] +param() -$this.PackageProperties.Category = $Category - +$this.GetContent( + $this.FileList -match '\.nuspec$' +) + + - CHANGELOG.md + Package.json <# .SYNOPSIS - Gets a package's changelog + Gets a package's `package.json` .DESCRIPTION - Gets the content of any parts in the package named CHANGELOG.md + Gets the content of any `package.json` files in the package #> -[OutputType("text/markdown")] +[OutputType([PSObject])] param() # Get every part foreach ($part in $this.GetParts()) { # and ignore any part not named CHANGELOG - if ($part.Uri -notmatch '/CHANGELOG\.md$') { continue } + if ($part.Uri -notmatch '/package\.json$') { continue } if ($part.Reader) { $part.Read() @@ -2403,199 +1552,235 @@ foreach ($part in $this.GetParts()) { - ChocolateyInstall + Palette <# .SYNOPSIS - Gets the Chocolatey Install Script + Gets an Open Package palette .DESCRIPTION - Gets the Chocolatey Install Script from an OpenPackage, if one is present. - - The Chocolatey install script must be located at /tools/chocolateyInstall.ps1 + Gets an Open Package's palette. +.NOTES + A palette is a relationship between a package and a stylesheet. #> param() -$this.GetContent("/tools/chocolateyInstall.ps1") + +if (-not $this.RelationshipExists -or + -not $this.GetRelationship) { + return +} + +if ($this.RelationshipExists('palette')) { + return $this.GetRelationship('palette').TargetUri +} + + <# +.SYNOPSIS + Sets an Open Package palette +.DESCRIPTION + Sets an Open Package's palette. +.NOTES + A palette is a relationship between a package and a stylesheet. +#> +param( +[Parameter(Mandatory)] +[ValidatePattern('.css$')] +[uri]$Palette +) +if (-not $this.RelationshipExists) { + return +} +if ($this.RelationshipExists('palette')) { + $this.DeleteRelationship('palette') + $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') +} else { + $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') +} + + + - Claude.md + PowerShellCommandAst <# .SYNOPSIS - Gets a package's Claude Markdown + Gets PowerShell Command References .DESCRIPTION - Gets any `claude.md` or `/.claude/*.md` files in an Open Package -.LINK - https://claude.com/blog/using-claude-md-files + Gets PowerShell Command Ast references within an Open Package. #> -foreach ($part in $this.GetParts()) { - if ($part.Uri -match '/CLAUDE\.(?>md|markdown)$' -or - $part.Uri -match '/\.claude/.+?\.(?>md|markdown)$' - ) { - if ($part.Reader) { - $part.Read() - } else { - $part - } - } -} +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.CommandAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} - CodeOfConduct.md + PowerShellManifest <# .SYNOPSIS - Gets a package's code of conduct + Gets a package's PowerShell manifest files .DESCRIPTION - Gets any parts in the package named Code_Of_Conduct.md + Gets a package's PowerShell manifest files. + + These are any `*.psd1` files in the package that: + + * Are valid PowerShell data blocks + * Contain a ModuleVersion #> -[OutputType("text/markdown")] +[OutputType([PSObject])] param() -# Get every part foreach ($part in $this.GetParts()) { - # and ignore any part not named CODE_OF_CONDUCT - if ($part.Uri -notmatch '/CODE_OF_CONDUCT\.md$') { continue } - - if ($part.Reader) { - $part.Read() - } else { - $part + if ($part.Uri -notmatch '\.psd1$') { continue } + try { + $psd1 = $part.Read() + if (-not $psd1.ModuleVersion) { continue } + $psd1 + } catch { + Write-Debug "Could not read $($part.Uri): $_" } } - -# We are done. - Config.json + PowerShellParameterAst <# .SYNOPSIS - Gets a package's config json + Gets PowerShell Parameter Definitions .DESCRIPTION - Gets the objects stored in any config.json files in the package. -.NOTES - config.json files are used by several static site generators. + Gets all PowerShell ParameterAst references within an Open Package. #> -[OutputType([PSObject])] -param() - -$partPattern = '[/\+_]config\.json$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch $partPattern) { continue } - if ($part.Reader) { - $part.Read() - } else { - $part - } -} +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + $ast -is [Management.Automation.Language.ParameterAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} - Config.yaml + PowerShellTypeAst <# .SYNOPSIS - Gets a package's config yaml + Gets PowerShell Type References .DESCRIPTION - Gets the objects stored in any config.yaml files in the package. -.NOTES - config.yaml files are used by several static site generators. + Gets PowerShell TypeExpressionAst references within an Open Package. #> -[OutputType([PSObject])] -param() - -$partPattern = '[/\+_]config\.ya?ml$' - -foreach ($part in $this.GetParts()) { - if ($part.Uri -notmatch $partPattern) { continue } - if ($part.Reader) { - $part.Read() - } else { - $part - } -} +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + $ast -is [Management.Automation.Language.TypeExpressionAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} - ContentStatus + ProjectFile <# .SYNOPSIS - Gets OpenPackage `ContentStatus` + Gets a package's project files .DESCRIPTION - Gets the OpenPackage `ContentStatus` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 + Gets the content of any `*.*proj` files in the package #> +[OutputType([xml])] param() -$this.PackageProperties.ContentStatus +$this.GetContent( + $this.FileList -match '\..+?proj$' +) - + + + README + <# .SYNOPSIS - Sets OpenPackage `ContentStatus` + Gets a package's README .DESCRIPTION - Sets the OpenPackage `ContentStatus` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contentstatus?wt.mc_id=MVP_321542 + Gets the content of any parts in the package named README.md #> -param([string]$ContentStatus) +[OutputType("text/markdown")] +param() -$this.PackageProperties.ContentStatus = $ContentStatus - +# Get all Readme files +foreach ($content in $this.GetContent( + $this.FileList -match '/README\.md$' +)) { + # decorate them as text/markdown + if ($content.pstypenames -notcontains 'text/markdown') { + $content.pstypenames.insert(0, 'text/markdown') + } + # and return them. + $content +} + +return + - ContentType + Revision <# .SYNOPSIS - Gets OpenPackage `ContentType` + Gets OpenPackage `Revision` .DESCRIPTION - Gets the OpenPackage `ContentType` property. + Gets the OpenPackage `Revision` property. .LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 #> param() -$this.PackageProperties.ContentType -split ';' +$this.PackageProperties.Revision <# .SYNOPSIS - Sets OpenPackage `ContentType` + Sets OpenPackage `Revision` .DESCRIPTION - Sets the OpenPackage `ContentType` property. + Sets the OpenPackage `Revision` property. .LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.contenttype?wt.mc_id=MVP_321542 + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.revision?wt.mc_id=MVP_321542 #> -param([string]$ContentType) +param([string]$Revision) -$this.PackageProperties.ContentType = $ContentType +$this.PackageProperties.Revision = $Revision - Contributing.md + Security.md <# .SYNOPSIS - Gets a package's contribution guide + Gets a package's security notice .DESCRIPTION - Gets any parts in the package named Contributing.md + Gets any parts in the package named Security.md #> [OutputType("text/markdown")] param() # Get every part foreach ($part in $this.GetParts()) { - # and ignore any part not named Contributing - if ($part.Uri -notmatch '/Contributing\.md$') { continue } + # and ignore any part not named SECURITY + if ($part.Uri -notmatch '/SECURITY\.md$') { continue } if ($part.Reader) { $part.Read() @@ -2608,1446 +1793,2056 @@ foreach ($part in $this.GetParts()) { - Count - - <# -.SYNOPSIS - Gets the package files count -.DESCRIPTION - Gets the number of files in a package. -#> -return @($this.GetParts()).Length - - - - Created + ServiceWorker.js <# .SYNOPSIS - Gets OpenPackage creation time + Gets any service workers in a package .DESCRIPTION - Gets the OpenPackage `Created` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 + Gets any clearly named service workers in a package. + + Will find any files named `sw.js` or `ServiceWorker.js` #> param() -$this.PackageProperties.Created - - - <# -.SYNOPSIS - Sets OpenPackage `Created` -.DESCRIPTION - Sets the OpenPackage `Created` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.created?wt.mc_id=MVP_321542 -#> -param([DateTime]$Created) +$pattern = '/(?>sw|ServiceWorker).js$' -$this.PackageProperties.Created = $Created - +foreach ($part in $this.GetParts()) { + if ($part.Uri -match $pattern) { + if ($part.Reader) { + $part.Read() + } else { + $part + } + } +} + +# We are done. + - Creator + Subject <# .SYNOPSIS - Gets OpenPackage `Creator` + Gets OpenPackage `Subject` .DESCRIPTION - Gets the OpenPackage `Creator` property. + Gets the OpenPackage `Subject` property. .LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 #> param() -$this.PackageProperties.Creator +$this.PackageProperties.Subject <# .SYNOPSIS - Sets OpenPackage `Creator` + Sets OpenPackage `Subject` .DESCRIPTION - Sets the OpenPackage `Creator` property. + Sets the OpenPackage `Subject` property. .LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.creator?wt.mc_id=MVP_321542 + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.subject?wt.mc_id=MVP_321542 #> -param([string]$Creator) +param([string]$Subject) -$this.PackageProperties.Creator = $Creator - - - - Description - - return $this.PackageProperties.Description - - - $this.PackageProperties.Description = $args -join [Environment]::NewLine +$this.PackageProperties.Subject = $Subject - Dockerfile + Template.json <# .SYNOPSIS - Gets a package's dockerfile + Gets a package's `template.json` .DESCRIPTION - Gets the content of any `Dockerfile`s in the package. + Gets the content of any `template.json` files in the package #> -[OutputType([string])] +[OutputType([PSObject])] param() -$partPattern = '[/\.]DockerFile$' - foreach ($part in $this.GetParts()) { - if ($part.Uri -match $partPattern) { + if ($part.Uri -notmatch '/template\.json$') { continue } + + if ($part.Reader) { $part.Read() + } else { + $part } } - Eleventy + Title <# .SYNOPSIS - Gets a package's eleventy files + Gets OpenPackage `Title` .DESCRIPTION - Gets the content of any elevent config files in the package. - - This includes any files named: - * `.eleventy.js` - * `eleventy.config.js` - * `eleventy.config.mjs` - * `eleventy.config.cjs` + Gets the OpenPackage `Title` property. .LINK - https://www.11ty.dev/docs/config/ + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 #> param() -$partPattern = '/(?>\.eleventy.js|elventy\.config\.[mc]?js$)' -foreach ($part in $this.GetParts()) { - if ($part.Uri -match $partPattern) { - if ($part.Reader) { - $part.Read() - } else { - $part - } - } -} - +$this.PackageProperties.Title + + <# +.SYNOPSIS + Sets OpenPackage `Title` +.DESCRIPTION + Sets the OpenPackage `Title` property. +.LINK + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.title?wt.mc_id=MVP_321542 +#> +param([string]$Title) + +$this.PackageProperties.Title = $Title + - Eponym + TypeScriptConfig.json <# .SYNOPSIS - Gets all Eponyms in a Package + Gets a package's `tsconfig.json` .DESCRIPTION - Gets all Eponyms within an OpenPackage. - - Eponyms are files whose name matches their directory. - - For example: - - `/foo/foo.ps1` is an eponym - `/foo/foo.md` is an eponym - `/foo/bar.ps1` is not an eponym - - If a package has an an identifier, - any files whose name matches this identifier will be considered an eponym. -.NOTES - Multiple eponyms may exist for a given path. - - Anything before the first period `.` will be considered the name of the file. + Gets the content of any TypeScript configuration `tsconfig.json` files in the package #> +[OutputType([psobject])] param() -if (-not $this.GetParts) { return } +# Get every part foreach ($part in $this.GetParts()) { - $partSegments = @($part.Uri -split '/+' -ne '') - if ($partSegments.Count -eq 1) { - if ($partSegments[0] -replace '\..+$' -eq $this.Identifier) { - $part - continue - } - } - if ($partSegments.Count -ge 2) { - if ($partSegments[-2] -eq - ($partSegments[-1] -replace '\..+$') - ) { - $part - continue - } + # and ignore any part not named manifest.json + if ($part.Uri -notmatch '/tsconfig\.json$') { continue } + + if ($part.Reader) { + $part.Read() + } else { + $part } } +# We are done. - FileContentType + Underscore <# .SYNOPSIS - Gets file content types + Gets package underscore files .DESCRIPTION - Gets a table of all files in the package and their associated content types. + Gets underscore files defined in a package. + + These are any files that contain an underscore `_` in their path. + + This returns a series of dictionaries containing the contents of the package #> -$fileContentTypes = [Ordered]@{} -foreach ($part in @($this.GetParts())) { - $fileContentTypes[$part.Uri] = $part.ContentType -} -$fileContentTypes +param() + + +return $this.GetTree("/_") - FileHash + Version <# .SYNOPSIS - Gets the file hashes + Gets OpenPackage `Version` .DESCRIPTION - Gets the file hashes of each part using any supported algorithm (default SHA256) -.NOTES - Supports any algorithm from Get-FileHash + Gets the OpenPackage `Version` property. + + If this has not been explicitly set, looks for potential version informatin .LINK - Get-FileHash + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 #> -param([string]$Algorithm = 'SHA256') +param() -foreach ($part in $this.GetParts()) { - $part.GetHash($Algorithm) +if ($this.PackageProperties.Version) { + return $this.PackageProperties.Version +} + +$moduleManifest = $this.PowerShellManifest +if ($moduleManifest) { + $this.PackageProperties.Version = $moduleManifest.ModuleVersion + return $this.PackageProperties.Version } +$packageJson = $this.'Package.json' +if ($packageJson -and $packageJson.version) { + $this.PackageProperties.Version = $packageJson.version + return $this.PackageProperties.Version +} - - - FileList - + <# .SYNOPSIS - Gets OpenPackage file list + Sets OpenPackage `Version` .DESCRIPTION - Gets the list of files in an OpenPackage. + Sets the OpenPackage `Version` property. .LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.package.getparts?wt.mc_id=MVP_321542 -#> -[OutputType([string[]])] -param() + https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 +#> +param([string]$Version) -@($this.GetParts()).Uri -as [string[]] - +$this.PackageProperties.Version = $Version + - FileSize + VsixManifest <# .SYNOPSIS - Gets file content types + Gets VsixManfiests from a package .DESCRIPTION - Gets a table of all files in the package and their associated content types. + Gets any Visual Studio Extension Manifests (*.vsixManifest) files in a package. #> -$fileLengths = [Ordered]@{} -foreach ($part in @($this.GetParts())) { - $partStream = $part.GetStream() - $fileLengths[$part.Uri] = $partStream.Length - $partStream.Close() - $partStream.Dispose() -} -$fileLengths +$partPattern = '\.vsixManifest$' + +$this.GetContent(@( + $this.FileList -match $partPattern +)) - Identifier - - $this.PackageProperties.Identifier - - - $this.PackageProperties.Identifier = $args -join ' ' - - - - ImageFileList + XRPC <# .SYNOPSIS - Gets package image files + Gets package xrpc .DESCRIPTION - Gets the list of image files within a package. + Gets any xrpc parts within the package. #> -@(foreach ($part in $this.GetParts()) { - if ($part.ContentType -match 'image/' -or - $part.Uri -match '\.(?>a?png|jpe?g|gif|tiff?|svg|ico|bmp|exr)$' +if (-not $this.GetParts) { return } +foreach ($part in $this.GetParts()) { + if ( + $part.Uri -notmatch '/xrpc' -or + $part.Uri -notlike '*.*.*' ) { - $part.Uri + continue } -}) -as [string[]] + $part +} + + DefaultDisplay + Identifier +FileList + + + + + OpenPackage.ContentTypeMap + + + PSStandardMembers + + + DefaultDisplayPropertySet + + TypeMap + + + + - ImportMap + DefaultTypeMap - <# -.SYNOPSIS - Gets a package's `importMap.json` -.DESCRIPTION - Gets the content of any `importMap.json` files in the package -#> -[OutputType([psobject])] -param() - - -$this.GetContent(@($this.FileList -match '\importMap\.json$')) - - + data { @{ + '.aac' = 'audio/aac' + '.abw' = 'application/x-abiword' + '.apng' = 'image/apng' + '.arc' = 'application/x-freearc' + '.avif' = 'image/avif' + '.avi' = 'video/x-msvideo' + '.azw' = 'application/vnd.amazon.ebook' + '.bin' = 'application/octet-stream' + '.bmp' = 'image/bmp' + '.bz' = 'application/x-bzip' + '.bz2' = 'application/x-bzip2' + '.c' = 'text/plain' + '.cda' = 'application/x-cdf' + '.cpp' = 'text/plain' + '.cs' = 'text/plain' + '.csh' = 'application/x-csh' + '.csproj' = 'application/xml' + '.css' = 'text/css' + '.csv' = 'text/csv' + '.doc' = 'application/msword' + '.docx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + '.eot' = 'application/vnd.ms-fontobject' + '.epub' = 'application/epub+zip' + '.exe' = 'application/executeable' + '.fsproj' = 'application/xml' + '.gz' = 'application/gzip' + '.go' = 'text/plain' + '.gif' = 'image/gif' + '.h' = 'text/plain' + '.htm' = 'text/html' + '.html' = 'text/html' + '.ico' = 'image/vnd.microsoft.icon' + '.ics' = 'text/calendar' + '.ini' = 'text/plain' + '.jar' = 'application/java-archive' + '.jpeg' = 'image/jpeg' + '.jpg' = 'image/jpeg' + '.js' = 'text/javascript' + '.jsm' = 'text/javascript' + '.json' = 'application/json' + '.jsonld' = 'application/ld+json' + '.md' = 'text/markdown' + '.mod' = 'text/plain' + '.mid' = 'audio/midi' + '.midi' = 'audio/midi' + '.mjs' = 'text/javascript' + '.mp3' = 'audio/mpeg' + '.mp4' = 'video/mp4' + '.mpeg' = 'video/mpeg' + '.mpkg' = 'application/vnd.apple.installer+xml' + '.nuspec' = 'application/xml' + '.op' = 'application/zip' + '.odp' = 'application/vnd.oasis.opendocument.presentation' + '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' + '.odt' = 'application/vnd.oasis.opendocument.text' + '.oga' = 'audio/ogg' + '.ogv' = 'video/ogg' + '.ogx' = 'application/ogg' + '.opus' = 'audio/ogg' + '.otf' = 'font/otf' + '.png' = 'image/png' + '.pdf' = 'application/pdf' + '.php' = 'application/x-httpd-php' + '.ps1' = 'text/x-powershell' + '.ps1xml' = 'text/x-powershell+xml' + '.psm1' = 'text/x-powershell' + '.psd1' = 'text/x-powershell-data' + '.ppt' = 'application/vnd.ms-powerpoint' + '.pptx' = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + '.rar' = 'application/vnd.rar' + '.rtf' = 'application/rtf' + '.rs' = 'text/plain' + '.s' = 'text/plain' + '.sh' = 'application/x-sh' + '.srt' = 'application/x-subrip' + '.svg' = 'image/svg+xml' + '.tar' = 'application/x-tar' + '.tif' = 'image/tiff' + '.tiff' = 'image/tiff' + '.ts' = 'application/x-typescript' + '.ttf' = 'font/ttf' + '.ttl' = 'text/turtle' + '.txt' = 'text/plain' + '.url' = 'text/plain' + '.vb' = 'text/plain' + '.vbproj' = 'application/xml' + '.vbs' = 'text/plain' + '.vsd' = 'application/vnd.visio' + '.vtt' = 'text/vtt' + '.wav' = 'audio/wav' + '.weba' = 'audio/webm' + '.webm' = 'video/webm' + '.webp' = 'image/webp' + '.webmanifest' = 'application/manifest+json' + '.woff' = 'font/woff' + '.woff2' = 'font/woff2' + '.xhtml' = 'application/xhtml+xml' + '.xls' = 'application/vnd.ms-excel' + '.xlsx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + '.xml' = 'application/xml' + '.xsd' = 'text/xsd' + '.xsl' = 'text/xsl' + '.xul' = 'application/vnd.mozilla.xul+xml' + '.zip' = 'application/zip' + '.3gp' = 'video/3gpp' + '.3g2' = 'video/3gpp2' + '.7z' = 'application/x-7z-compressed' +} } - Keywords + TypeMap <# .SYNOPSIS - Gets OpenPackage `Keywords` + Gets the TypeMap .DESCRIPTION - Gets the OpenPackage `Keywords` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 + Gets the TypeMap. + + Will default to the values in `DefaultTypeMap` #> -param() - -$this.PackageProperties.Keywords -split '[\s\r\n]+' +if ($this.'.TypeMap') { + return $this.'.TypeMap' +} +$typeMap = [Ordered]@{} +$defaultTypeMap = $this.DefaultTypeMap +foreach ($extension in ($defaultTypeMap.Keys | Sort-Object)) { + $typeMap[$extension] = $defaultTypeMap[$extension] +} +$this | Add-Member NoteProperty $typeMap '.TypeMap' -Force +return $typeMap <# .SYNOPSIS - Sets OpenPackage `Keywords` + Sets the TypeMap .DESCRIPTION - Sets the OpenPackage `Keywords` property. -.LINK - https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.keywords?wt.mc_id=MVP_321542 + Sets the TypeMap. + + If an empty dictionary is provided, will clear the typemap. + + Any other values provided will override the existing content type map. #> -param() +param( +[Collections.IDictionary] +$TypeMap +) -$this.PackageProperties.Keywords = $args -join ' ' +$thisTypeMap = $this.TypeMap + +if ($TypeMap.Count -eq 0) { + $thisTypeMap.Clear() +} else { + foreach ($key in $TypeMap.Keys) { + $thisTypeMap[$key] = $TypeMap[$key] + } +} - - Language - + + DefaultDisplay + TypeMap + + + + + OP.Part + + + PSStandardMembers + + + DefaultDisplayPropertySet + + Uri + ContentType + Reader + + + + + + GetHash + + + + Read + + + + ReadCliXml + + + + ReadCSharp + + + + ReadFormData + + + + ReadJson + + + + ReadJsonL + + + + ReadMarkdown + + + + ReadPowerShell + + + + ReadPowerShellData + + + + ReadText + + + + ReadToml + + + + ReadXml + + + + ReadXsd + + + + ReadXslt + + + + ReadYaml + + + + Write + + + + WriteClixml + + + + WriteJson + + + + WritePowerShell + + + + WriteText + + + + WriteToml + + + + WriteXml + + + + WriteYaml + + - VsixManifest + Hash <# .SYNOPSIS - Gets VsixManfiests from a package + Gets the part hash .DESCRIPTION - Gets any Visual Studio Extension Manifests (*.vsixManifest) files in a package. + Gets the part hash as a string #> -$partPattern = '\.vsixManifest$' - -$this.GetContent(@( - $this.FileList -match $partPattern -)) - +$this.GetHash().Hash - XRPC + IsEponym <# .SYNOPSIS - Gets package xrpc -.DESCRIPTION - Gets any xrpc parts within the package. + Determines if a part is an eponym +.DESCRIPTION + Eponyms are files whose name matches their directory. + + For example: + + `/foo/foo.ps1` is an eponym + `/foo/foo.md` is an eponym + `/foo/bar.ps1` is not an eponym + + If a package has an an identifier, + any files whose name matches this identifier will be considered an eponym. +.NOTES + Multiple eponyms may exist for a given path. + + Anything before the first period `.` will be considered the name of the file. #> -if (-not $this.GetParts) { return } -foreach ($part in $this.GetParts()) { - if ( - $part.Uri -notmatch '/xrpc' -or - $part.Uri -notlike '*.*.*' - ) { - continue - } - $part -} - - - - DefaultDisplay - Identifier -FileList - - - - - OpenPackage.ContentTypeMap - - - PSStandardMembers - - - DefaultDisplayPropertySet - - TypeMap - - - - - - DefaultTypeMap - - data { @{ - '.aac' = 'audio/aac' - '.abw' = 'application/x-abiword' - '.apng' = 'image/apng' - '.arc' = 'application/x-freearc' - '.avif' = 'image/avif' - '.avi' = 'video/x-msvideo' - '.azw' = 'application/vnd.amazon.ebook' - '.bin' = 'application/octet-stream' - '.bmp' = 'image/bmp' - '.bz' = 'application/x-bzip' - '.bz2' = 'application/x-bzip2' - '.c' = 'text/plain' - '.cda' = 'application/x-cdf' - '.cpp' = 'text/plain' - '.cs' = 'text/plain' - '.csh' = 'application/x-csh' - '.csproj' = 'application/xml' - '.css' = 'text/css' - '.csv' = 'text/csv' - '.doc' = 'application/msword' - '.docx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - '.eot' = 'application/vnd.ms-fontobject' - '.epub' = 'application/epub+zip' - '.exe' = 'application/executeable' - '.fsproj' = 'application/xml' - '.gz' = 'application/gzip' - '.go' = 'text/plain' - '.gif' = 'image/gif' - '.h' = 'text/plain' - '.htm' = 'text/html' - '.html' = 'text/html' - '.ico' = 'image/vnd.microsoft.icon' - '.ics' = 'text/calendar' - '.ini' = 'text/plain' - '.jar' = 'application/java-archive' - '.jpeg' = 'image/jpeg' - '.jpg' = 'image/jpeg' - '.js' = 'text/javascript' - '.jsm' = 'text/javascript' - '.json' = 'application/json' - '.jsonld' = 'application/ld+json' - '.md' = 'text/markdown' - '.mod' = 'text/plain' - '.mid' = 'audio/midi' - '.midi' = 'audio/midi' - '.mjs' = 'text/javascript' - '.mp3' = 'audio/mpeg' - '.mp4' = 'video/mp4' - '.mpeg' = 'video/mpeg' - '.mpkg' = 'application/vnd.apple.installer+xml' - '.nuspec' = 'application/xml' - '.op' = 'application/zip' - '.odp' = 'application/vnd.oasis.opendocument.presentation' - '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' - '.odt' = 'application/vnd.oasis.opendocument.text' - '.oga' = 'audio/ogg' - '.ogv' = 'video/ogg' - '.ogx' = 'application/ogg' - '.opus' = 'audio/ogg' - '.otf' = 'font/otf' - '.png' = 'image/png' - '.pdf' = 'application/pdf' - '.php' = 'application/x-httpd-php' - '.ps1' = 'text/x-powershell' - '.ps1xml' = 'text/x-powershell+xml' - '.psm1' = 'text/x-powershell' - '.psd1' = 'text/x-powershell-data' - '.ppt' = 'application/vnd.ms-powerpoint' - '.pptx' = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' - '.rar' = 'application/vnd.rar' - '.rtf' = 'application/rtf' - '.rs' = 'text/plain' - '.s' = 'text/plain' - '.sh' = 'application/x-sh' - '.srt' = 'application/x-subrip' - '.svg' = 'image/svg+xml' - '.tar' = 'application/x-tar' - '.tif' = 'image/tiff' - '.tiff' = 'image/tiff' - '.ts' = 'application/x-typescript' - '.ttf' = 'font/ttf' - '.ttl' = 'text/turtle' - '.txt' = 'text/plain' - '.url' = 'text/plain' - '.vb' = 'text/plain' - '.vbproj' = 'application/xml' - '.vbs' = 'text/plain' - '.vsd' = 'application/vnd.visio' - '.vtt' = 'text/vtt' - '.wav' = 'audio/wav' - '.weba' = 'audio/webm' - '.webm' = 'video/webm' - '.webp' = 'image/webp' - '.webmanifest' = 'application/manifest+json' - '.woff' = 'font/woff' - '.woff2' = 'font/woff2' - '.xhtml' = 'application/xhtml+xml' - '.xls' = 'application/vnd.ms-excel' - '.xlsx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - '.xml' = 'application/xml' - '.xsd' = 'text/xsd' - '.xsl' = 'text/xsl' - '.xul' = 'application/vnd.mozilla.xul+xml' - '.zip' = 'application/zip' - '.3gp' = 'video/3gpp' - '.3g2' = 'video/3gpp2' - '.7z' = 'application/x-7z-compressed' -} } +param() +$part = $this + +$partSegments = @($part.Uri -split '/+' -ne '') + +if ($partSegments.Count -eq 1 -and $this.Package.Identifier) { + if ($partSegments[0] -replace '\..+$' -eq $this.Package.Identifier) { + return $true + } +} + +if ($partSegments.Count -ge 2) { + if ($partSegments[-2] -eq + ($partSegments[-1] -replace '\..+$') + ) { + return $true + } +} + +return $false - TypeMap + Name <# .SYNOPSIS - Gets the TypeMap + Gets the part name .DESCRIPTION - Gets the TypeMap. + Gets the part name. - Will default to the values in `DefaultTypeMap` + This is is anything before the first period (`.`). #> -if ($this.'.TypeMap') { - return $this.'.TypeMap' +param() +@($this.Uri -split '/' -ne '')[-1] -replace '\..+?$' + + + + Palette + + <# +.SYNOPSIS + Gets an Open Package Part's palette +.DESCRIPTION + Gets an Open Package palette for a part +.NOTES + A palette is a relationship between a package and a stylesheet. +#> +param() + +if (-not $this.RelationshipExists -or + -not $this.GetRelationship) { + return } -$typeMap = [Ordered]@{} -$defaultTypeMap = $this.DefaultTypeMap -foreach ($extension in ($defaultTypeMap.Keys | Sort-Object)) { - $typeMap[$extension] = $defaultTypeMap[$extension] + +if ($this.RelationshipExists('palette')) { + return $this.GetRelationship('palette').TargetUri } -$this | Add-Member NoteProperty $typeMap '.TypeMap' -Force -return $typeMap <# .SYNOPSIS - Sets the TypeMap + Sets an Open Package Part's palette .DESCRIPTION - Sets the TypeMap. - - If an empty dictionary is provided, will clear the typemap. - - Any other values provided will override the existing content type map. + Sets an Open Package palette for a part. +.NOTES + A palette is a relationship between a package and a stylesheet. #> param( -[Collections.IDictionary] -$TypeMap +[Parameter(Mandatory)] +[ValidatePattern('.css$')] +[uri]$Palette +) +if (-not $this.RelationshipExists) { + return +} +if ($this.RelationshipExists('palette')) { + $this.DeleteRelationship('palette') + $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') +} else { + $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') +} + + + + + + Reader + + <# +.SYNOPSIS + Gets a Part's available Readers +.DESCRIPTION + Gets the different methods we can use to Read a part. +#> +param() + +# If we already have a cached list of Readers +if ($this.'#Readers') { + # return it. + return $this.'#Readers' +} + +# To get all of our methods, go over each method on this object +$ReadMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { + # and ignore any methods that do not start with `Read`. + if ($method.Name -notmatch 'Read.+?') { + continue + } + # If there is no script, ignore the method. + if (-not $method.Script) { continue } + $script = $method.Script + # If there are no attributes, ignore the method + if (-not $script.Attributes) { continue } + # Gather all of our attribute data + $attributeData = [Ordered]@{} + :nextAttribute foreach ($attribute in $script.Attributes) { + # If the attribute does not have a key and value, + if (-not ($attribute.Key -and $attribute.value)) { + continue nextAttribute # continue to the next attribute + } + + # If we do not already know about this key + if (-not $attributeData[$attribute.key]) { + # set it + $attributeData[$attribute.key] = $attribute.value + } else { + # otherwise, make our data a list and add the new value + $attributeData[$attribute.key] = @( + $attributeData[$attribute.key] + ) + $attribute.Value + } + } + + # Now we want to try to match. + try { + # If either the `FilePattern` or `ContentTypePattern` matches + # we consider this to be a potential Reader. + + # Loop thru the file patterns + foreach ($pattern in $attributeData.FilePattern) { + # skip parts that do not match. + if ($this.Uri -notmatch $pattern) { continue } + # Output the matching method + $method + continue nextMethod # and continue to the next method + } + # Loop thru the content type patterns + foreach ($pattern in $attributeData.ContentTypePattern) { + # skip parts that do not match. + if ($this.ContentType -notmatch $pattern) { continue } + # Output the matching method + $method + continue nextMethod # and continue to the next method. + } + } catch { + Write-Debug "$_" + } +}) + +# Now that we know all of the methods, we need to put them in order. +$ReadMethods = @( + $ReadMethods | # We can do this with a dynamic sort. + Sort-Object { + $in = $_ + # Simply walk over each attribute + foreach ($attr in $in.Script.Attributes) { + # any named `Order` with an integer value + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + # will use that value + return ($attr.Value -as [int]) + } + } + # Without an order attribute, use zero + return 0 + }, Name # and perform a secondary sort by name. +) + +# Now that we've gotten our Readers and sorted them +# cache them on the object. +$this | Add-Member NoteProperty '#Readers' $ReadMethods -Force +# and return the cached value. +return $this.'#Readers' + + + + Writer + + <# +.SYNOPSIS + Gets a Part's available writers +.DESCRIPTION + Gets the different methods we can use to Write to a part. +#> +param() + +# If we already have a cached list of writers +if ($this.'#Writers') { + # return it. + return $this.'#Writers' +} + +# To get all of our methods, go over each method on this object +$WriteMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { + # and ignore any methods that do not start with `Write`. + if ($method.Name -notmatch 'Write.+?') { + continue + } + # If there is no script, ignore the method. + if (-not $method.Script) { continue } + $script = $method.Script + # If there are no attributes, ignore the method + if (-not $script.Attributes) { continue } + # Gather all of our attribute data + $attributeData = [Ordered]@{} + :nextAttribute foreach ($attribute in $script.Attributes) { + # If the attribute does not have a key and value, + if (-not ($attribute.Key -and $attribute.value)) { + continue nextAttribute # continue to the next attribute + } + + # If we do not already know about this key + if (-not $attributeData[$attribute.key]) { + # set it + $attributeData[$attribute.key] = $attribute.value + } else { + # otherwise, make our data a list and add the new value + $attributeData[$attribute.key] = @( + $attributeData[$attribute.key] + ) + $attribute.Value + } + } + + # Now we want to try to match. + try { + # If either the `FilePattern` or `ContentTypePattern` matches + # we consider this to be a potential writer. + + # Loop thru the file patterns + foreach ($pattern in $attributeData.FilePattern) { + # skip parts that do not match. + if ($this.Uri -notmatch $pattern) { continue } + # Output the matching method + $method + continue nextMethod # and continue to the next method + } + # Loop thru the content type patterns + foreach ($pattern in $attributeData.ContentTypePattern) { + # skip parts that do not match. + if ($this.ContentType -notmatch $pattern) { continue } + # Output the matching method + $method + continue nextMethod # and continue to the next method. + } + } catch { + Write-Debug "$_" + } +}) + +# Now that we know all of the methods, we need to put them in order. +$WriteMethods = @( + $WriteMethods | # We can do this with a dynamic sort. + Sort-Object { + $in = $_ + # Simply walk over each attribute + foreach ($attr in $in.Script.Attributes) { + # any named `Order` with an integer value + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + # will use that value + return ($attr.Value -as [int]) + } + } + # Without an order attribute, use zero + return 0 + }, Name # and perform a secondary sort by name. ) -$thisTypeMap = $this.TypeMap - -if ($TypeMap.Count -eq 0) { - $thisTypeMap.Clear() -} else { - foreach ($key in $TypeMap.Keys) { - $thisTypeMap[$key] = $TypeMap[$key] - } -} - +# Now that we've gotten our writers and sorted them +# cache them on the object. +$this | Add-Member NoteProperty '#Writers' $WriteMethods -Force +# and return the cached value. +return $this.'#Writers' + DefaultDisplay - TypeMap + Uri +ContentType +Reader - OP.Part + OpenPackage.Part PSStandardMembers @@ -5667,7 +5462,7 @@ Reader - OpenPackage.Part + System.IO.Packaging.PackagePart PSStandardMembers @@ -6144,229 +5939,12 @@ if (-not $InputObject -and .LINK https://toml.io/ .LINK - https://github.com/jborean93/PSToml -#> -[Reflection.AssemblyMetadata( - # This should automatically apply to .yaml files - 'FilePattern', - '\.toml$' -)] -param( - # An optional input object - # If provided, content will be read from this object. - # If not provided, content will be read from this part. - [Alias('Input')] - [PSObject]$InputObject = $null, - - # Any options used to read the data. - [Alias('Options')] - [Collections.IDictionary]$Option = [Ordered]@{} -) - - -if (-not $this.ReadText) { return } - -$convertFromTomlCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Toml', 'Cmdlet,Function') -$partString = $this.ReadText($InputObject, $Option) - -if (-not $convertFromTomlCommand -or -not $convertFromTomlCommand.Parameters.InputObject) { - Write-Warning "ConvertFrom-Toml not found, please install PSToml" - $partString -} else { - try { - $partString | & $convertFromTomlCommand -ErrorAction Stop - } catch { - Write-Warning "'$($thisPart.Uri)' was not valid toml: $_" - } -} - - - - ReadXml - - - - ReadXsd - - - - ReadXslt - - - - ReadYaml - - Write - - - - WriteClixml + ReadXml - - - WriteJson - - WritePowerShell + ReadXsd - WriteText + ReadXslt - WriteToml + ReadYaml - WriteXml + Write - WriteYaml + WriteClixml - - - Hash - - <# -.SYNOPSIS - Gets the part hash -.DESCRIPTION - Gets the part hash as a string -#> -$this.GetHash().Hash - - - - IsEponym - - <# -.SYNOPSIS - Determines if a part is an eponym -.DESCRIPTION - Eponyms are files whose name matches their directory. - - For example: - - `/foo/foo.ps1` is an eponym - `/foo/foo.md` is an eponym - `/foo/bar.ps1` is not an eponym - - If a package has an an identifier, - any files whose name matches this identifier will be considered an eponym. -.NOTES - Multiple eponyms may exist for a given path. - - Anything before the first period `.` will be considered the name of the file. -#> -param() -$part = $this - -$partSegments = @($part.Uri -split '/+' -ne '') - -if ($partSegments.Count -eq 1 -and $this.Package.Identifier) { - if ($partSegments[0] -replace '\..+$' -eq $this.Package.Identifier) { - return $true - } -} - -if ($partSegments.Count -ge 2) { - if ($partSegments[-2] -eq - ($partSegments[-1] -replace '\..+$') - ) { - return $true - } -} - -return $false - - - - Name - - <# -.SYNOPSIS - Gets the part name -.DESCRIPTION - Gets the part name. - - This is is anything before the first period (`.`). -#> -param() -@($this.Uri -split '/' -ne '')[-1] -replace '\..+?$' - - - - Palette - - <# -.SYNOPSIS - Gets an Open Package Part's palette -.DESCRIPTION - Gets an Open Package palette for a part -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param() - -if (-not $this.RelationshipExists -or - -not $this.GetRelationship) { - return -} - -if ($this.RelationshipExists('palette')) { - return $this.GetRelationship('palette').TargetUri -} - - - <# -.SYNOPSIS - Sets an Open Package Part's palette -.DESCRIPTION - Sets an Open Package palette for a part. -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param( -[Parameter(Mandatory)] -[ValidatePattern('.css$')] -[uri]$Palette -) -if (-not $this.RelationshipExists) { - return -} -if ($this.RelationshipExists('palette')) { - $this.DeleteRelationship('palette') - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} else { - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} - - - - - - Reader - - <# -.SYNOPSIS - Gets a Part's available Readers -.DESCRIPTION - Gets the different methods we can use to Read a part. -#> -param() - -# If we already have a cached list of Readers -if ($this.'#Readers') { - # return it. - return $this.'#Readers' -} - -# To get all of our methods, go over each method on this object -$ReadMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { - # and ignore any methods that do not start with `Read`. - if ($method.Name -notmatch 'Read.+?') { - continue - } - # If there is no script, ignore the method. - if (-not $method.Script) { continue } - $script = $method.Script - # If there are no attributes, ignore the method - if (-not $script.Attributes) { continue } - # Gather all of our attribute data - $attributeData = [Ordered]@{} - :nextAttribute foreach ($attribute in $script.Attributes) { - # If the attribute does not have a key and value, - if (-not ($attribute.Key -and $attribute.value)) { - continue nextAttribute # continue to the next attribute - } + $option.Depth = $FormatEnumerationLimit * 2 +} - # If we do not already know about this key - if (-not $attributeData[$attribute.key]) { - # set it - $attributeData[$attribute.key] = $attribute.value - } else { - # otherwise, make our data a list and add the new value - $attributeData[$attribute.key] = @( - $attributeData[$attribute.key] - ) + $attribute.Value - } - } +$this.WriteText([Management.Automation.PSSerializer]::Serialize($InputObject, $option.Depth), $option) - # Now we want to try to match. - try { - # If either the `FilePattern` or `ContentTypePattern` matches - # we consider this to be a potential Reader. - - # Loop thru the file patterns - foreach ($pattern in $attributeData.FilePattern) { - # skip parts that do not match. - if ($this.Uri -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method - } - # Loop thru the content type patterns - foreach ($pattern in $attributeData.ContentTypePattern) { - # skip parts that do not match. - if ($this.ContentType -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method. - } - } catch { - Write-Debug "$_" - } -}) -# Now that we know all of the methods, we need to put them in order. -$ReadMethods = @( - $ReadMethods | # We can do this with a dynamic sort. - Sort-Object { - $in = $_ - # Simply walk over each attribute - foreach ($attr in $in.Script.Attributes) { - # any named `Order` with an integer value - if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { - # will use that value - return ($attr.Value -as [int]) - } - } - # Without an order attribute, use zero - return 0 - }, Name # and perform a secondary sort by name. -) -# Now that we've gotten our Readers and sorted them -# cache them on the object. -$this | Add-Member NoteProperty '#Readers' $ReadMethods -Force -# and return the cached value. -return $this.'#Readers' - - - - Writer - + + + + WriteJson + + - GetHash + WritePowerShell - Read + WriteText - ReadCliXml + WriteToml - - - ReadCSharp - - ReadFormData + WriteXml - - - ReadJson - - ReadJsonL + WriteYaml - - ReadMarkdown - - - - ReadPowerShell - - - - ReadPowerShellData - - - - ReadText - - - - ReadToml - - + Write-Debug "$_" + } +}) + +# Now that we know all of the methods, we need to put them in order. +$WriteMethods = @( + $WriteMethods | # We can do this with a dynamic sort. + Sort-Object { + $in = $_ + # Simply walk over each attribute + foreach ($attr in $in.Script.Attributes) { + # any named `Order` with an integer value + if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { + # will use that value + return ($attr.Value -as [int]) + } + } + # Without an order attribute, use zero + return 0 + }, Name # and perform a secondary sort by name. +) + +# Now that we've gotten our writers and sorted them +# cache them on the object. +$this | Add-Member NoteProperty '#Writers' $WriteMethods -Force +# and return the cached value. +return $this.'#Writers' + + + + DefaultDisplay + Uri +ContentType +Reader + + + + + OpenPackage.Source + - ReadXml + At - - - ReadXsd - - - - ReadXslt - - ReadYaml + AtBlob - Write + AtProtocol - WriteClixml + AtRecord - WriteJson + AtType - WritePowerShell + Dictionary - WriteText + Directory - - - WriteToml - - WriteXml + Nuget - WriteYaml + Repository - - Hash - + + Tar + + + + Url + + + + Zip + + \ No newline at end of file From 3b8b5fe50af59b7619f2dc2b735451dd48c2f098 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:38:03 -0700 Subject: [PATCH 479/724] feat: `Get-OpenPackage` ( Fixes #2 ) Using OpenPackage.Source and renaming methods accordingly --- Commands/Get-OpenPackage.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 2dd25bc..780c66f 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -638,7 +638,7 @@ function Get-OpenPackage #region Open Package from At Uri if ($AtUri) { $NamedParameters['AtUri'] = $AtUri - InvokeOpMethod 'AtProto' $NamedParameters + InvokeOpMethod 'AtProtocol' $NamedParameters return } #endregion Open Package From At Uri From 5ba2eb24d3c2c447a9c434782e78777df4ede5b4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:41:21 -0700 Subject: [PATCH 480/724] feat: `OpenPackage.Source.AtProtocol` ( Fixes #125 ) Fixing inner references --- Types/OpenPackage.Source/AtProtocol.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Types/OpenPackage.Source/AtProtocol.ps1 b/Types/OpenPackage.Source/AtProtocol.ps1 index c613177..a72eafc 100644 --- a/Types/OpenPackage.Source/AtProtocol.ps1 +++ b/Types/OpenPackage.Source/AtProtocol.ps1 @@ -71,13 +71,13 @@ $Skip if (-not $package) { $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') - $package.pstypenames.insert(0, 'OP') - $package.pstypenames.insert(0, 'OpenPackage') Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force } if (-not $this) {$this = $package} +$sources = [PSCustomObject]@{PSTypeName='OpenPackage.Source'} + filter packAtProtoRecord { # Declare a package uri for the segment. # (make sure to switch did colons to something else, so that the files can unpack cleanly regardless of OS) @@ -124,13 +124,13 @@ foreach ($at in $AtUri) { # If we have a type and rkey, we are after a single record. if ($atMatch.type -and $atMatch.rkey) { - $atRecord = $this.GetAtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds) + $atRecord = $sources.GetAtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds) if (-not $atRecord) { continue } packAtProtoRecord } elseif ($atMatch.type -match '\.') { # If there are dots in the type, it is a collection - foreach ($atRecord in $this.GetAtType( + foreach ($atRecord in $sources.GetAtType( $atMatch.did, $atMatch.type, $BatchSize, $First, $Skip, $pds )) { # If the uri is not an at uri From 001501424a15f36350b9d94de87119d7207b4677 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 19:41:39 +0000 Subject: [PATCH 481/724] feat: `OpenPackage.Source.AtProtocol` ( Fixes #125 ) Fixing inner references --- OP.types.ps1xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 48f6107..343fa3a 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -7377,13 +7377,13 @@ $Skip if (-not $package) { $memoryStream = [IO.MemoryStream]::new() $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') - $package.pstypenames.insert(0, 'OP') - $package.pstypenames.insert(0, 'OpenPackage') Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force } if (-not $this) {$this = $package} +$sources = [PSCustomObject]@{PSTypeName='OpenPackage.Source'} + filter packAtProtoRecord { # Declare a package uri for the segment. # (make sure to switch did colons to something else, so that the files can unpack cleanly regardless of OS) @@ -7430,13 +7430,13 @@ foreach ($at in $AtUri) { # If we have a type and rkey, we are after a single record. if ($atMatch.type -and $atMatch.rkey) { - $atRecord = $this.GetAtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds) + $atRecord = $sources.GetAtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds) if (-not $atRecord) { continue } packAtProtoRecord } elseif ($atMatch.type -match '\.') { # If there are dots in the type, it is a collection - foreach ($atRecord in $this.GetAtType( + foreach ($atRecord in $sources.GetAtType( $atMatch.did, $atMatch.type, $BatchSize, $First, $Skip, $pds )) { # If the uri is not an at uri From 680d770910c2b05a448ab76dab9c59f229287d5d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 12:42:29 -0700 Subject: [PATCH 482/724] feat: `OpenPackage.Source.AtProtocol` ( Fixes #125 ) Fixing inner references (removing Get) --- Types/OpenPackage.Source/AtProtocol.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage.Source/AtProtocol.ps1 b/Types/OpenPackage.Source/AtProtocol.ps1 index a72eafc..fccc0b5 100644 --- a/Types/OpenPackage.Source/AtProtocol.ps1 +++ b/Types/OpenPackage.Source/AtProtocol.ps1 @@ -124,13 +124,13 @@ foreach ($at in $AtUri) { # If we have a type and rkey, we are after a single record. if ($atMatch.type -and $atMatch.rkey) { - $atRecord = $sources.GetAtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds) + $atRecord = $sources.AtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds) if (-not $atRecord) { continue } packAtProtoRecord } elseif ($atMatch.type -match '\.') { # If there are dots in the type, it is a collection - foreach ($atRecord in $sources.GetAtType( + foreach ($atRecord in $sources.AtType( $atMatch.did, $atMatch.type, $BatchSize, $First, $Skip, $pds )) { # If the uri is not an at uri @@ -144,7 +144,7 @@ foreach ($at in $AtUri) { else { try { - $atBlob = $this.GetAtBlob($matches.did, $matches.type) + $atBlob = $sources.AtBlob($matches.did, $matches.type) $atContentType = $atBlob.Headers.'Content-Type' if (-not $atContentType) { continue From 7b7c2fbd8f52a3a43d0b579c896b1c2a6493885c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 19:42:50 +0000 Subject: [PATCH 483/724] feat: `OpenPackage.Source.AtProtocol` ( Fixes #125 ) Fixing inner references (removing Get) --- OP.types.ps1xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 343fa3a..2e12e45 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -7430,13 +7430,13 @@ foreach ($at in $AtUri) { # If we have a type and rkey, we are after a single record. if ($atMatch.type -and $atMatch.rkey) { - $atRecord = $sources.GetAtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds) + $atRecord = $sources.AtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds) if (-not $atRecord) { continue } packAtProtoRecord } elseif ($atMatch.type -match '\.') { # If there are dots in the type, it is a collection - foreach ($atRecord in $sources.GetAtType( + foreach ($atRecord in $sources.AtType( $atMatch.did, $atMatch.type, $BatchSize, $First, $Skip, $pds )) { # If the uri is not an at uri @@ -7450,7 +7450,7 @@ foreach ($at in $AtUri) { else { try { - $atBlob = $this.GetAtBlob($matches.did, $matches.type) + $atBlob = $sources.AtBlob($matches.did, $matches.type) $atContentType = $atBlob.Headers.'Content-Type' if (-not $atContentType) { continue From c634b3b117d9b8e02e172b4aa6a8f8f4b8a8e18a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 13:14:18 -0700 Subject: [PATCH 484/724] feat: `OpenPackage.Part.ReadPowerShellData` ( Fixes #101 ) Allowing optional supported commands --- Types/OpenPackage.Part/ReadPowerShellData.ps1 | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/Types/OpenPackage.Part/ReadPowerShellData.ps1 b/Types/OpenPackage.Part/ReadPowerShellData.ps1 index c36152d..bcf1917 100644 --- a/Types/OpenPackage.Part/ReadPowerShellData.ps1 +++ b/Types/OpenPackage.Part/ReadPowerShellData.ps1 @@ -23,17 +23,37 @@ param( if (-not $this.ReadText) { return } -$datablock = [ScriptBlock]::Create("data {$($this.ReadText($InputObject, $Option)) }") +$supportedCommands = @( + if ($option.SupportedCommand) { + $option.SupportedCommand + } + if ($option.SupportedCommands) { + $option.SupportedCommands + } +) + +$datablock = [ScriptBlock]::Create("data $( + if ($supportedCommands) { + '-supportedCommand' + "'$($supportedCommands -replace "'","''" -join "','")'" + } +) {$( + $this.ReadText($InputObject, $Option) +)}") if ($datablock.Ast.EndBlock.Statements.Count -gt 1) { return } if ($datablock.Ast.EndBlock.Statements[0] -isnot [Management.Automation.Language.DataStatementAst] ) { return } -if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { - return +if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { + foreach ($commandMaybeAllowed in $datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { + if (-not $commandMaybeAllowed) { continue } + if ($supportedCommands -notcontains "$commandMaybeAllowed") { + Write-Warning "Unsupported command $commandMaybeAllowed" + return + } + } } & $datablock - - From 43e798e00330dec5279f3af7dc75ce33d9dc7097 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 20:14:45 +0000 Subject: [PATCH 485/724] feat: `OpenPackage.Part.ReadPowerShellData` ( Fixes #101 ) Allowing optional supported commands --- OP.types.ps1xml | 90 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 2e12e45..de36e05 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2606,21 +2606,41 @@ param( if (-not $this.ReadText) { return } -$datablock = [ScriptBlock]::Create("data {$($this.ReadText($InputObject, $Option)) }") +$supportedCommands = @( + if ($option.SupportedCommand) { + $option.SupportedCommand + } + if ($option.SupportedCommands) { + $option.SupportedCommands + } +) + +$datablock = [ScriptBlock]::Create("data $( + if ($supportedCommands) { + '-supportedCommand' + "'$($supportedCommands -replace "'","''" -join "','")'" + } +) {$( + $this.ReadText($InputObject, $Option) +)}") if ($datablock.Ast.EndBlock.Statements.Count -gt 1) { return } if ($datablock.Ast.EndBlock.Statements[0] -isnot [Management.Automation.Language.DataStatementAst] ) { return } -if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { - return +if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { + foreach ($commandMaybeAllowed in $datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { + if (-not $commandMaybeAllowed) { continue } + if ($supportedCommands -notcontains "$commandMaybeAllowed") { + Write-Warning "Unsupported command $commandMaybeAllowed" + return + } + } } & $datablock - - @@ -4226,21 +4246,41 @@ param( if (-not $this.ReadText) { return } -$datablock = [ScriptBlock]::Create("data {$($this.ReadText($InputObject, $Option)) }") +$supportedCommands = @( + if ($option.SupportedCommand) { + $option.SupportedCommand + } + if ($option.SupportedCommands) { + $option.SupportedCommands + } +) + +$datablock = [ScriptBlock]::Create("data $( + if ($supportedCommands) { + '-supportedCommand' + "'$($supportedCommands -replace "'","''" -join "','")'" + } +) {$( + $this.ReadText($InputObject, $Option) +)}") if ($datablock.Ast.EndBlock.Statements.Count -gt 1) { return } if ($datablock.Ast.EndBlock.Statements[0] -isnot [Management.Automation.Language.DataStatementAst] ) { return } -if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { - return +if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { + foreach ($commandMaybeAllowed in $datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { + if (-not $commandMaybeAllowed) { continue } + if ($supportedCommands -notcontains "$commandMaybeAllowed") { + Write-Warning "Unsupported command $commandMaybeAllowed" + return + } + } } & $datablock - - @@ -5846,21 +5886,41 @@ param( if (-not $this.ReadText) { return } -$datablock = [ScriptBlock]::Create("data {$($this.ReadText($InputObject, $Option)) }") +$supportedCommands = @( + if ($option.SupportedCommand) { + $option.SupportedCommand + } + if ($option.SupportedCommands) { + $option.SupportedCommands + } +) + +$datablock = [ScriptBlock]::Create("data $( + if ($supportedCommands) { + '-supportedCommand' + "'$($supportedCommands -replace "'","''" -join "','")'" + } +) {$( + $this.ReadText($InputObject, $Option) +)}") if ($datablock.Ast.EndBlock.Statements.Count -gt 1) { return } if ($datablock.Ast.EndBlock.Statements[0] -isnot [Management.Automation.Language.DataStatementAst] ) { return } -if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { - return +if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { + foreach ($commandMaybeAllowed in $datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { + if (-not $commandMaybeAllowed) { continue } + if ($supportedCommands -notcontains "$commandMaybeAllowed") { + Write-Warning "Unsupported command $commandMaybeAllowed" + return + } + } } & $datablock - - From 3f55b4e9bb63ec90efa57ebdf0fc7b59685d4732 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 15:27:14 -0700 Subject: [PATCH 486/724] feat: `OpenPackagke.View.MarkdownHtml` ( Fixes #163 ) --- Types/OpenPackage.View/MarkdownHtml.ps1 | 128 ++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 Types/OpenPackage.View/MarkdownHtml.ps1 diff --git a/Types/OpenPackage.View/MarkdownHtml.ps1 b/Types/OpenPackage.View/MarkdownHtml.ps1 new file mode 100644 index 0000000..ad6eca2 --- /dev/null +++ b/Types/OpenPackage.View/MarkdownHtml.ps1 @@ -0,0 +1,128 @@ +<# +.SYNOPSIS + Views Markdown as HTML +.DESCRIPTION + Views Markdown as HTML +.EXAMPLE + +#> +[OutputType('text/html')] +[PSTypeName('string')] +[PSTypeName('IO.Packaging.Package')] +[PSTypeName('IO.Packaging.PackagePart')] +param() + +# Quickly enumerate all input and arguments. +$allInput = @($input) + @($args) + +# If there is no pipeline builder +if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { + # warn and return + Write-Warning "Markdig not loaded (ConvertFrom-Markdown is not installed)" + return +} + +# Create the pipeline builder +$mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new() +$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build() + +# Make one quick pass over all input +$allInput = @( + foreach ($in in $allInput) { + # and expand any packages we find into their parts. + if ($in -is [IO.Packaging.Package]) { + $in.GetParts() + } else { + $in + } + } +) + +# Next make a neat little filter to decorate our output +# We can give PowerShell objects N arbitrary types +# including actual content types +filter text/html { + # Just check if our typenames do not contain our invocation name + if ($_.pstypenames -and + $_.pstypenames -notcontains $myInvocation.InvocationName) { + # and add that typename. + $_.pstypenames.add($myInvocation.InvocationName) + } + $_ # and pass thru the input. +} + +# Now, let's go over all input +:nextInput foreach ($in in $allInput) { + + # If the input is a string + if ($in -is [string]) { + # Just make the markdown html + # and stick it in an
    tag + "
    $( + [Markdig.Markdown]::ToHtml($in, $mdPipeline) + )
    " | text/html + # and continue to the next input. + continue nextInput + } + + # If the input is a package part, we can do more + if ( + $in -is [IO.Packaging.PackagePart] -and + # (if it is not a markdown file, we should ignore it). + $in.Uri -match '(?>\.md|\.markdown)$' + ) { + # Let's read our input real quick: + $partStream = $in.GetStream('Open', 'Read') + $reader = [IO.StreamReader]::new($partStream) + # Just store the markdown. + $markdown = $streamReader.ReadToEnd() + + # and clean up. + $reader.Close(), $reader.Dispose() + $partStream.Close(), $partStream.Dispose() + + # We stick the markdown into an article tag + $markdownHtml = "
    $markdown
    " + + # And we can use relationships to determine more. + $part = $in + + # If the part had a 'palette' relationship + $paletteUrl = + # If the part had a 'palette' relationship + if ($part.RelationshipExists('palette')) { + # use that target uri + $part.GetRelationship('palette').TargetUri + } elseif ($part.Package.RelationshipExists('palette')) { + # alternatively, if the package had a palette, use that. + $part.Package.GetRelationship('palette').TargetUri + } + + # Now, we can put the markdown into HTML. + @( + "" + # The title is easy. + "" + $([Web.HttpUtility]::HtmlEncode($part.Name)) + "" + # If there's a palette url + if ($paletteUrl) { + # link the stylesheet with that id. + "" + } + "" + "" + $markdownHtml + "" + "" + ) -join [Environment]::NewLine | + Add-Member NoteProperty Package $in.Package -Force -PassThru | + Add-Member NoteProperty PartUri $in.Uri -Force -PassThru | + Add-Member NoteProperty Part $in -Force -PassThru | + text/html + + # continue to the next input. + continue nextInput + } +} + From 22073c8146390a449249ea766d88234c0d40d777 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 15:38:17 -0700 Subject: [PATCH 487/724] feat: `Format-OpenPackage` ( Fixes #164 ) --- Commands/Format-OpenPackage.ps1 | 92 +++++++++++++++++++++++++++++++++ OP.psd1 | 5 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 Commands/Format-OpenPackage.ps1 diff --git a/Commands/Format-OpenPackage.ps1 b/Commands/Format-OpenPackage.ps1 new file mode 100644 index 0000000..63cf4b2 --- /dev/null +++ b/Commands/Format-OpenPackage.ps1 @@ -0,0 +1,92 @@ +function Format-OpenPackage { + <# + .SYNOPSIS + Formates Open Package + .DESCRIPTION + Formates Open Packages using any view. + #> + [CmdletBinding(PositionalBinding=$false)] + [Alias('Format-OP', 'fop', 'fOpenPackage')] + param( + # The name of the Formatter, or a command or script block used to Format. + # The Package and Option will be passed to the Formatter as positional parameters. + [Parameter(Mandatory,Position=0)] + [ArgumentCompleter({ + param ( $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters ) + $typeData = Get-TypeData -TypeName OpenPackage.View + + if (-not $wordToComplete) { + $typeData.Members.Keys + } else { + $typeData.Members.Keys -match ([Regex]::Escape($wordTocomplete)) + } + })] + [PSObject] + $View, + + # The Any Positional arguments for the view + [Parameter(Position=1,ValueFromRemainingArguments)] + [Alias('Argument','Arguments','Args')] + [PSObject[]] + $ArgumentList, + + # Any input objects. + [Parameter(ValueFromPipeline)] + [Alias('Package')] + [PSObject[]] + $InputObject, + + # Any options to pass to the View + [Alias('Options')] + [Collections.IDictionary] + $Option = [Ordered]@{} + ) + + # Gather all input + $allInput = @($input) + + if (-not $allInput -and $InputObject) { + $allInput += $InputObject + } + + # Get the typedata that describes Formatters + $typeData = Get-TypeData -TypeName OpenPackage.View + + # If the Formatter is a string + if ($View -is [string]) { + # check for a Formatter with that name + if ($typeData.Members[$View].Script) { + # if one is found, call it with the package and option + if ($allInput) { + $allInput | + . $typeData.Members[$View].Script @ArgumentList @Option + } + return + } else { + # Othewise, try to find a Formatter command + $ViewCommand = + $ExecutionContext.SessionState.InvokeCommand.GetCommand( + $View,'Cmdlet,Alias,Function' + ) + # If we found one, try to use it + if ($ViewCommand) { + $View = $ViewCommand + } else { + # Otherwise, warn that Formatter is unknown + Write-Warning "Unknown View $view" + # and break out of the loop. + return + } + } + } + + if ($allInput) { + $allInput | & $view @ArgumentList @Option + } else { + & $view @argumentList @option + } +} \ No newline at end of file diff --git a/OP.psd1 b/OP.psd1 index e81330a..d69ef3b 100644 --- a/OP.psd1 +++ b/OP.psd1 @@ -27,7 +27,7 @@ Author = 'James Brundage' CompanyName = 'Start-Automating' # Copyright statement for this module -Copyright = '2025 Start-Automating' +Copyright = '2025-2026 Start-Automating' # Description of the functionality provided by this module Description = 'An Overpowered little module for Open Packages' @@ -72,6 +72,7 @@ TypesToProcess = @('OP.types.ps1xml') FunctionsToExport = @( 'Copy-OpenPackage' 'Export-OpenPackage' + 'Format-OpenPackage' 'Get-OpenPackage' 'Install-OpenPackage' 'Join-OpenPackage' @@ -102,6 +103,8 @@ AliasesToExport = @( 'Export-OP','epop','epOpenPackage', 'Save-OpenPackage','Save-OP','svop', 'svOpenPackage' + + 'Format-OP', 'fop', 'fOpenPackage' 'Get-OP', 'OP', 'OpenPackage','gOpenPackage' From 76647c4a387e0347096faaf6dab3146ade6161a5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 22:38:39 +0000 Subject: [PATCH 488/724] feat: `Format-OpenPackage` ( Fixes #164 ) --- OP.format.ps1xml | 141 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 OP.format.ps1xml diff --git a/OP.format.ps1xml b/OP.format.ps1xml new file mode 100644 index 0000000..4f78c27 --- /dev/null +++ b/OP.format.ps1xml @@ -0,0 +1,141 @@ + + + + + text/html + + text/html + string + IO.Packaging.Package + IO.Packaging.PackagePart + + + + + + + + +# Quickly enumerate all input and arguments. +$allInput = @($input) + @($args) + +# If there is no pipeline builder +if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { + # warn and return + Write-Warning "Markdig not loaded (ConvertFrom-Markdown is not installed)" + return +} + +# Create the pipeline builder +$mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new() +$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build() + +# Make one quick pass over all input +$allInput = @( + foreach ($in in $allInput) { + # and expand any packages we find into their parts. + if ($in -is [IO.Packaging.Package]) { + $in.GetParts() + } else { + $in + } + } +) + +# Next make a neat little filter to decorate our output +# We can give PowerShell objects N arbitrary types +# including actual content types +filter text/html { + # Just check if our typenames do not contain our invocation name + if ($_.pstypenames -and + $_.pstypenames -notcontains $myInvocation.InvocationName) { + # and add that typename. + $_.pstypenames.add($myInvocation.InvocationName) + } + $_ # and pass thru the input. +} + +# Now, let's go over all input +:nextInput foreach ($in in $allInput) { + + # If the input is a string + if ($in -is [string]) { + # Just make the markdown html + # and stick it in an <article> tag + "<article>$( + [Markdig.Markdown]::ToHtml($in, $mdPipeline) + )</article>" | text/html + # and continue to the next input. + continue nextInput + } + + # If the input is a package part, we can do more + if ( + $in -is [IO.Packaging.PackagePart] -and + # (if it is not a markdown file, we should ignore it). + $in.Uri -match '(?>\.md|\.markdown)$' + ) { + # Let's read our input real quick: + $partStream = $in.GetStream('Open', 'Read') + $reader = [IO.StreamReader]::new($partStream) + # Just store the markdown. + $markdown = $streamReader.ReadToEnd() + + # and clean up. + $reader.Close(), $reader.Dispose() + $partStream.Close(), $partStream.Dispose() + + # We stick the markdown into an article tag + $markdownHtml = "<article>$markdown</article>" + + # And we can use relationships to determine more. + $part = $in + + # If the part had a 'palette' relationship + $paletteUrl = + # If the part had a 'palette' relationship + if ($part.RelationshipExists('palette')) { + # use that target uri + $part.GetRelationship('palette').TargetUri + } elseif ($part.Package.RelationshipExists('palette')) { + # alternatively, if the package had a palette, use that. + $part.Package.GetRelationship('palette').TargetUri + } + + # Now, we can put the markdown into HTML. + @( + "<html><head>" + # The title is easy. + "<title>" + $([Web.HttpUtility]::HtmlEncode($part.Name)) + "</title>" + # If there's a palette url + if ($paletteUrl) { + # link the stylesheet with that id. + "<link rel='stylesheet' id='palette' href='$($paletteUrl)' />" + } + "</head>" + "<body>" + $markdownHtml + "</body>" + "</html>" + ) -join [Environment]::NewLine | + Add-Member NoteProperty Package $in.Package -Force -PassThru | + Add-Member NoteProperty PartUri $in.Uri -Force -PassThru | + Add-Member NoteProperty Part $in -Force -PassThru | + text/html + + # continue to the next input. + continue nextInput + } +} + + + + + + + + + + \ No newline at end of file From b61027ccf09ac08273e4886f97ec9aa0cb46cd5c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 22:38:40 +0000 Subject: [PATCH 489/724] feat: `Format-OpenPackage` ( Fixes #164 ) --- OP.types.ps1xml | 139 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index de36e05..9a06942 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -8967,6 +8967,145 @@ foreach ($resolvedItem in Get-Item -Path $ZipFile) { $currentPackage } + + + + + + OpenPackage.View + + + MarkdownHtml + From d58d003a20cdcc787180b69d702d0f4d22293fea Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 15:41:47 -0700 Subject: [PATCH 490/724] feat: `Format-OpenPackage` ( Fixes #164 ) Not building formatting in Types and removing OP.format.ps1xml --- Build/OP.ezout.ps1 | 2 +- OP.format.ps1xml | 141 --------------------------------------------- 2 files changed, 1 insertion(+), 142 deletions(-) delete mode 100644 OP.format.ps1xml diff --git a/Build/OP.ezout.ps1 b/Build/OP.ezout.ps1 index 480ccb0..bd5f12f 100644 --- a/Build/OP.ezout.ps1 +++ b/Build/OP.ezout.ps1 @@ -7,7 +7,7 @@ Push-Location $myRoot $formatting = @( # Add your own Write-FormatView here, # or put them in a Formatting or Views directory - foreach ($potentialDirectory in 'Formatting','Views','Types') { + foreach ($potentialDirectory in 'Formatting','Views') { Join-Path $myRoot $potentialDirectory | Get-ChildItem -ea ignore | Import-FormatView -FilePath {$_.Fullname} diff --git a/OP.format.ps1xml b/OP.format.ps1xml deleted file mode 100644 index 4f78c27..0000000 --- a/OP.format.ps1xml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - text/html - - text/html - string - IO.Packaging.Package - IO.Packaging.PackagePart - - - - - - - - -# Quickly enumerate all input and arguments. -$allInput = @($input) + @($args) - -# If there is no pipeline builder -if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { - # warn and return - Write-Warning "Markdig not loaded (ConvertFrom-Markdown is not installed)" - return -} - -# Create the pipeline builder -$mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new() -$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build() - -# Make one quick pass over all input -$allInput = @( - foreach ($in in $allInput) { - # and expand any packages we find into their parts. - if ($in -is [IO.Packaging.Package]) { - $in.GetParts() - } else { - $in - } - } -) - -# Next make a neat little filter to decorate our output -# We can give PowerShell objects N arbitrary types -# including actual content types -filter text/html { - # Just check if our typenames do not contain our invocation name - if ($_.pstypenames -and - $_.pstypenames -notcontains $myInvocation.InvocationName) { - # and add that typename. - $_.pstypenames.add($myInvocation.InvocationName) - } - $_ # and pass thru the input. -} - -# Now, let's go over all input -:nextInput foreach ($in in $allInput) { - - # If the input is a string - if ($in -is [string]) { - # Just make the markdown html - # and stick it in an <article> tag - "<article>$( - [Markdig.Markdown]::ToHtml($in, $mdPipeline) - )</article>" | text/html - # and continue to the next input. - continue nextInput - } - - # If the input is a package part, we can do more - if ( - $in -is [IO.Packaging.PackagePart] -and - # (if it is not a markdown file, we should ignore it). - $in.Uri -match '(?>\.md|\.markdown)$' - ) { - # Let's read our input real quick: - $partStream = $in.GetStream('Open', 'Read') - $reader = [IO.StreamReader]::new($partStream) - # Just store the markdown. - $markdown = $streamReader.ReadToEnd() - - # and clean up. - $reader.Close(), $reader.Dispose() - $partStream.Close(), $partStream.Dispose() - - # We stick the markdown into an article tag - $markdownHtml = "<article>$markdown</article>" - - # And we can use relationships to determine more. - $part = $in - - # If the part had a 'palette' relationship - $paletteUrl = - # If the part had a 'palette' relationship - if ($part.RelationshipExists('palette')) { - # use that target uri - $part.GetRelationship('palette').TargetUri - } elseif ($part.Package.RelationshipExists('palette')) { - # alternatively, if the package had a palette, use that. - $part.Package.GetRelationship('palette').TargetUri - } - - # Now, we can put the markdown into HTML. - @( - "<html><head>" - # The title is easy. - "<title>" - $([Web.HttpUtility]::HtmlEncode($part.Name)) - "</title>" - # If there's a palette url - if ($paletteUrl) { - # link the stylesheet with that id. - "<link rel='stylesheet' id='palette' href='$($paletteUrl)' />" - } - "</head>" - "<body>" - $markdownHtml - "</body>" - "</html>" - ) -join [Environment]::NewLine | - Add-Member NoteProperty Package $in.Package -Force -PassThru | - Add-Member NoteProperty PartUri $in.Uri -Force -PassThru | - Add-Member NoteProperty Part $in -Force -PassThru | - text/html - - # continue to the next input. - continue nextInput - } -} - - - - - - - - - - \ No newline at end of file From dd4c1616dd18bcc987664b55aa9c13b8a61671ef Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 16:30:10 -0700 Subject: [PATCH 491/724] feat: `OpenPackage.View.Tree.html` ( Fixes #122 ) Renaming and slightly refactoring to support Format-OpenPackage --- .../Tree.html.ps1} | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) rename Types/{OpenPackage/ShowTreeHtml.ps1 => OpenPackage.View/Tree.html.ps1} (56%) diff --git a/Types/OpenPackage/ShowTreeHtml.ps1 b/Types/OpenPackage.View/Tree.html.ps1 similarity index 56% rename from Types/OpenPackage/ShowTreeHtml.ps1 rename to Types/OpenPackage.View/Tree.html.ps1 index 655bc34..ee6dcfb 100644 --- a/Types/OpenPackage/ShowTreeHtml.ps1 +++ b/Types/OpenPackage.View/Tree.html.ps1 @@ -4,11 +4,20 @@ .DESCRIPTION Shows a package tree beneath a point as HTML #> +[OutputType('text/html')] +[PSTypeName('IO.Packaging.Package')] param( +# Any input +[Parameter(ValueFromPipeline)] +[Alias('Package')] +[PSObject[]] +$InputObject, + # The file pattern [string] $FilePattern = '.', +# The CSS of the tree [Alias('CSS')] [string] $Style = @" @@ -17,7 +26,11 @@ $Style = @" "@ ) -$fileTree = $this.GetTree($FilePattern) + +$allInput = @($input) +if ((-not $allInput) -and $InputObject) { + $allInput += $InputObject +} filter toIndex { $in = $_ @@ -42,12 +55,27 @@ filter toIndex { "" } } - } -return @( - if ($Style) { - "" +foreach ($in in $allInput) { + if (-not $in.GetTree) { + continue } - $fileTree | toIndex -) -join [Environment]::NewLine + + $fileTree = $in.GetTree($FilePattern) + @( + if ($Style) { + "" + } + $fileTree | toIndex + ) -join [Environment]::NewLine | + Add-Member NoteProperty Package $in -Force -PassThru | + Add-Member NoteProperty FilePattern $in -Force -PassThru | + . { + process { + $_.pstypenames.add('text/html') + $_ + } + } +} + From 0603ecb4df7890952a0a990723b7d4ebbbc9f5a3 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 23:30:31 +0000 Subject: [PATCH 492/724] feat: `OpenPackage.View.Tree.html` ( Fixes #122 ) Renaming and slightly refactoring to support Format-OpenPackage --- OP.types.ps1xml | 146 +++++++++++++++++++++++++++++------------------- 1 file changed, 87 insertions(+), 59 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9a06942..0f34011 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -356,65 +356,6 @@ else { $partStream.Close() - - ShowTreeHtml - - ShowTreeText + + + Tree.html + From ef61c6a6c3c78bdbd2f2e095e87097868fb87823 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 16:32:34 -0700 Subject: [PATCH 493/724] feat: `OpenPackage.View.Markdown.html` ( Fixes #163 ) --- Types/OpenPackage.View/{MarkdownHtml.ps1 => Markdown.html.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/OpenPackage.View/{MarkdownHtml.ps1 => Markdown.html.ps1} (100%) diff --git a/Types/OpenPackage.View/MarkdownHtml.ps1 b/Types/OpenPackage.View/Markdown.html.ps1 similarity index 100% rename from Types/OpenPackage.View/MarkdownHtml.ps1 rename to Types/OpenPackage.View/Markdown.html.ps1 From 86703a17e1736462617fd58d795fe67f37c024a0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 24 Mar 2026 23:32:54 +0000 Subject: [PATCH 494/724] feat: `OpenPackage.View.Markdown.html` ( Fixes #163 ) --- OP.types.ps1xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0f34011..f4852ca 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -8916,7 +8916,7 @@ foreach ($resolvedItem in Get-Item -Path $ZipFile) { OpenPackage.View - MarkdownHtml + Markdown.html - - ShowTreeText - - Astro @@ -9137,6 +9068,98 @@ foreach ($in in $allInput) { } + + + + Tree.txt + From 483b25d8017200e85e10b8afb32fe534313e2c4a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 25 Mar 2026 12:36:49 -0700 Subject: [PATCH 503/724] feat: `Format-OpenPackage` ( Fixes #164 ) Cleaning up logic and only splatting positional parameters if present. --- Commands/Format-OpenPackage.ps1 | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Commands/Format-OpenPackage.ps1 b/Commands/Format-OpenPackage.ps1 index 63cf4b2..12020bd 100644 --- a/Commands/Format-OpenPackage.ps1 +++ b/Commands/Format-OpenPackage.ps1 @@ -61,11 +61,7 @@ function Format-OpenPackage { # check for a Formatter with that name if ($typeData.Members[$View].Script) { # if one is found, call it with the package and option - if ($allInput) { - $allInput | - . $typeData.Members[$View].Script @ArgumentList @Option - } - return + $view = $typeData.Members[$View].Script } else { # Othewise, try to find a Formatter command $ViewCommand = @@ -84,9 +80,25 @@ function Format-OpenPackage { } } + if ($view -isnot [ScriptBlock] -and + $view -isnot [Management.Automation.CommandInfo] + ) { + Write-Error "View must be a Name of view, ScriptBlock, or Command" + return + } + if ($allInput) { - $allInput | & $view @ArgumentList @Option + if ($ArgumentList) { + $allInput | & $view @ArgumentList @Option + } else { + $allInput | & $view @Option + } + } else { - & $view @argumentList @option + if ($ArgumentList) { + & $view @ArgumentList @Option + } else { + & $view @Option + } } } \ No newline at end of file From 9a0de220bcd0d3d0de985ee986a0f73d414e627f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 25 Mar 2026 15:36:20 -0700 Subject: [PATCH 504/724] feat: Open Package Action ( Fixes #155 ) Avoiding Invoke-Expression --- Build/GitHub/Actions/OPAction.ps1 | 13 ++++++++----- action.yml | 31 +++++++++++++++++++------------ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Build/GitHub/Actions/OPAction.ps1 b/Build/GitHub/Actions/OPAction.ps1 index 0d97eba..dfc5c5d 100644 --- a/Build/GitHub/Actions/OPAction.ps1 +++ b/Build/GitHub/Actions/OPAction.ps1 @@ -38,11 +38,13 @@ $ActionScript, [string] $GitHubToken = '{{ secrets.GITHUB_TOKEN }}', -# The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com. +# The user email associated with a git commit. +# If this is not provided, it will be set to the id+username@users.noreply.github.com. [string] $UserEmail, # The user name associated with a git commit. +# If this is not provided, it will be set to the $env:GITHUB_ACTOR [string] $UserName, @@ -133,11 +135,10 @@ function InitializeAction { # Configure git based on the $env:GITHUB_ACTOR if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } - if (-not $actorID) { $actorID = $env:GITHUB_ACTOR_ID } - + if (-not $actorID) { $actorID = $env:GITHUB_ACTOR_ID } if (-not $UserEmail) { $UserEmail = "$actorID+$UserName@users.noreply.github.com" } git config --global user.email $UserEmail - git config --global user.name $actorInfo.name + git config --global user.name $UserName # Pull down any changes git pull | Out-Host @@ -147,9 +148,11 @@ function InvokeActionModule { $myScriptStart = [DateTime]::Now $myScript = $ExecutionContext.SessionState.PSVariable.Get("Run").Value if ($myScript) { - Invoke-Expression -Command $myScript | + $myScript > ./_run.ps1 + . ./_run.ps1 | . ProcessOutput | Out-Host + Remove-Item ./_run.ps1 return } $myScriptTook = [Datetime]::Now - $myScriptStart diff --git a/action.yml b/action.yml index 00766c3..25c81c0 100644 --- a/action.yml +++ b/action.yml @@ -23,10 +23,14 @@ inputs: description: The github token to use for requests. UserEmail: required: false - description: The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com. + description: | + The user email associated with a git commit. + If this is not provided, it will be set to the id+username@users.noreply.github.com. UserName: required: false - description: The user name associated with a git commit. + description: | + The user name associated with a git commit. + If this is not provided, it will be set to the $env:GITHUB_ACTOR Push: required: false description: | @@ -42,14 +46,14 @@ runs: id: OPAction shell: pwsh env: - InstallModule: ${{inputs.InstallModule}} - Push: ${{inputs.Push}} ActionScript: ${{inputs.ActionScript}} - UserEmail: ${{inputs.UserEmail}} - Run: ${{inputs.Run}} - SkipScriptFile: ${{inputs.SkipScriptFile}} + Push: ${{inputs.Push}} UserName: ${{inputs.UserName}} GitHubToken: ${{inputs.GitHubToken}} + InstallModule: ${{inputs.InstallModule}} + Run: ${{inputs.Run}} + UserEmail: ${{inputs.UserEmail}} + SkipScriptFile: ${{inputs.SkipScriptFile}} run: | $Parameters = @{} $Parameters.Run = ${env:Run} @@ -110,11 +114,13 @@ runs: [string] $GitHubToken = '{{ secrets.GITHUB_TOKEN }}', - # The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com. + # The user email associated with a git commit. + # If this is not provided, it will be set to the id+username@users.noreply.github.com. [string] $UserEmail, # The user name associated with a git commit. + # If this is not provided, it will be set to the $env:GITHUB_ACTOR [string] $UserName, @@ -205,11 +211,10 @@ runs: # Configure git based on the $env:GITHUB_ACTOR if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } - if (-not $actorID) { $actorID = $env:GITHUB_ACTOR_ID } - + if (-not $actorID) { $actorID = $env:GITHUB_ACTOR_ID } if (-not $UserEmail) { $UserEmail = "$actorID+$UserName@users.noreply.github.com" } git config --global user.email $UserEmail - git config --global user.name $actorInfo.name + git config --global user.name $UserName # Pull down any changes git pull | Out-Host @@ -219,9 +224,11 @@ runs: $myScriptStart = [DateTime]::Now $myScript = $ExecutionContext.SessionState.PSVariable.Get("Run").Value if ($myScript) { - Invoke-Expression -Command $myScript | + $myScript > ./_run.ps1 + . ./_run.ps1 | . ProcessOutput | Out-Host + Remove-Item ./_run.ps1 return } $myScriptTook = [Datetime]::Now - $myScriptStart From 6eb1c638492deeeebc8bfe94b735ccedeb8b7a24 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 25 Mar 2026 16:38:41 -0700 Subject: [PATCH 505/724] feat: `OpenPackage.Part.Export` ( Fixes #165 ) --- Types/OpenPackage.Part/Export.ps1 | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Types/OpenPackage.Part/Export.ps1 diff --git a/Types/OpenPackage.Part/Export.ps1 b/Types/OpenPackage.Part/Export.ps1 new file mode 100644 index 0000000..c3f5d27 --- /dev/null +++ b/Types/OpenPackage.Part/Export.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Exports package parts +.DESCRIPTION + Exports a part from a package. + + This will write the part to a file on disk. +#> +param( +# The export path +[Parameter(Mandatory)] +[string] +$Path +) + +if (-not $this.Uri -and -not $this.GetStream) { + return +} + +# The location may already exist +$outputFiles = if (Test-Path $path) { + # If it does, get the location + foreach ($foundItem in Get-Item $path) { + # If it is a file, we are writing directly to it. + if ($foundItem -is [IO.FileInfo]) { + $foundItem + } elseif ($foundItem -is [IO.DirectoryInfo]) { + # If it is a directory, put the output in that directory, + $outputPath = Join-Path $foundItem.FullName @( + # use the last segment as the file name. + $this.Uri -split '/' -ne '' + )[-1] + New-Item -ItemType File -Path $outputPath -Force + } + } +} else { + # if it does not exist, create a new file. + New-Item -ItemType File -Path $Path +} + +# If we don't have any output files at this point, something is wrong +# so return. +if (-not $outputFiles) { return } +# Go over each output file +foreach ($outputFile in $outputFiles) { + # Open it for writing + $openedFile = $outputFile.OpenWrite() + if (-not $openedFile) { continue } + # zero out the length + $openedFile.SetLength(0) + # Open our stream for read + $partStream = $this.GetStream('Open', 'Read') + # copy it to the file + $partStream.CopyTo($openedFile) + # and close up. + $partStream.Close() + $partStream.Dispose() + $openedFile.Close() + $openedFile.Dispose() +} + + + From 22aef73453e355ed0a2ee85a080f8eb3986db64e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 25 Mar 2026 23:38:57 +0000 Subject: [PATCH 506/724] feat: `OpenPackage.Part.Export` ( Fixes #165 ) --- OP.types.ps1xml | 207 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index b98ac07..46de001 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2109,6 +2109,75 @@ if ($TypeMap.Count -eq 0) { + + Export + + GetHash + GetHash + GetHash + + Import + + Read + + Import + + Read + + Import + + Read - - - GetHash - - - - Import - - - - Read - - - - ReadCliXml - - - - ReadCSharp - - - - ReadFormData - - - - ReadJson - - - - ReadJsonL - - - - ReadMarkdown - - - - ReadPowerShell - - - - ReadPowerShellData - - - - ReadText - - - - ReadToml - - - - ReadXml - - - - ReadXsd - - - - ReadXslt - - - - ReadYaml - - - - Write - - - - WriteClixml - - - - WriteJson - - - - WritePowerShell - - - - WriteText - - - - WriteToml - - - - WriteXml - - - - WriteYaml - - - - Hash - - <# -.SYNOPSIS - Gets the part hash -.DESCRIPTION - Gets the part hash as a string -#> -$this.GetHash().Hash - - - - IsEponym - - <# -.SYNOPSIS - Determines if a part is an eponym -.DESCRIPTION - Eponyms are files whose name matches their directory. - - For example: - - `/foo/foo.ps1` is an eponym - `/foo/foo.md` is an eponym - `/foo/bar.ps1` is not an eponym - - If a package has an an identifier, - any files whose name matches this identifier will be considered an eponym. -.NOTES - Multiple eponyms may exist for a given path. - - Anything before the first period `.` will be considered the name of the file. -#> -param() -$part = $this - -$partSegments = @($part.Uri -split '/+' -ne '') - -if ($partSegments.Count -eq 1 -and $this.Package.Identifier) { - if ($partSegments[0] -replace '\..+$' -eq $this.Package.Identifier) { - return $true - } -} - -if ($partSegments.Count -ge 2) { - if ($partSegments[-2] -eq - ($partSegments[-1] -replace '\..+$') - ) { - return $true - } -} - -return $false - - - - Name - - <# -.SYNOPSIS - Gets the part name -.DESCRIPTION - Gets the part name. - - This is is anything before the first period (`.`). -#> -param() -@($this.Uri -split '/' -ne '')[-1] -replace '\..+?$' - - - - Palette - - <# -.SYNOPSIS - Gets an Open Package Part's palette -.DESCRIPTION - Gets an Open Package palette for a part -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param() - -if (-not $this.RelationshipExists -or - -not $this.GetRelationship) { - return -} - -if ($this.RelationshipExists('palette')) { - return $this.GetRelationship('palette').TargetUri -} - - - <# -.SYNOPSIS - Sets an Open Package Part's palette -.DESCRIPTION - Sets an Open Package palette for a part. -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param( -[Parameter(Mandatory)] -[ValidatePattern('.css$')] -[uri]$Palette -) -if (-not $this.RelationshipExists) { - return -} -if ($this.RelationshipExists('palette')) { - $this.DeleteRelationship('palette') - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} else { - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} - - - - - - Reader - - <# -.SYNOPSIS - Gets a Part's available Readers -.DESCRIPTION - Gets the different methods we can use to Read a part. -#> -param() - -# If we already have a cached list of Readers -if ($this.'#Readers') { - # return it. - return $this.'#Readers' -} - -# To get all of our methods, go over each method on this object -$ReadMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { - # and ignore any methods that do not start with `Read`. - if ($method.Name -notmatch 'Read.+?') { - continue - } - # If there is no script, ignore the method. - if (-not $method.Script) { continue } - $script = $method.Script - # If there are no attributes, ignore the method - if (-not $script.Attributes) { continue } - # Gather all of our attribute data - $attributeData = [Ordered]@{} - :nextAttribute foreach ($attribute in $script.Attributes) { - # If the attribute does not have a key and value, - if (-not ($attribute.Key -and $attribute.value)) { - continue nextAttribute # continue to the next attribute - } - - # If we do not already know about this key - if (-not $attributeData[$attribute.key]) { - # set it - $attributeData[$attribute.key] = $attribute.value - } else { - # otherwise, make our data a list and add the new value - $attributeData[$attribute.key] = @( - $attributeData[$attribute.key] - ) + $attribute.Value - } - } - - # Now we want to try to match. - try { - # If either the `FilePattern` or `ContentTypePattern` matches - # we consider this to be a potential Reader. - - # Loop thru the file patterns - foreach ($pattern in $attributeData.FilePattern) { - # skip parts that do not match. - if ($this.Uri -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method - } - # Loop thru the content type patterns - foreach ($pattern in $attributeData.ContentTypePattern) { - # skip parts that do not match. - if ($this.ContentType -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method. - } - } catch { - Write-Debug "$_" - } -}) - -# Now that we know all of the methods, we need to put them in order. -$ReadMethods = @( - $ReadMethods | # We can do this with a dynamic sort. - Sort-Object { - $in = $_ - # Simply walk over each attribute - foreach ($attr in $in.Script.Attributes) { - # any named `Order` with an integer value - if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { - # will use that value - return ($attr.Value -as [int]) - } - } - # Without an order attribute, use zero - return 0 - }, Name # and perform a secondary sort by name. -) - -# Now that we've gotten our Readers and sorted them -# cache them on the object. -$this | Add-Member NoteProperty '#Readers' $ReadMethods -Force -# and return the cached value. -return $this.'#Readers' - - - - Writer - - <# -.SYNOPSIS - Gets a Part's available writers -.DESCRIPTION - Gets the different methods we can use to Write to a part. -#> -param() - -# If we already have a cached list of writers -if ($this.'#Writers') { - # return it. - return $this.'#Writers' -} - -# To get all of our methods, go over each method on this object -$WriteMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { - # and ignore any methods that do not start with `Write`. - if ($method.Name -notmatch 'Write.+?') { - continue - } - # If there is no script, ignore the method. - if (-not $method.Script) { continue } - $script = $method.Script - # If there are no attributes, ignore the method - if (-not $script.Attributes) { continue } - # Gather all of our attribute data - $attributeData = [Ordered]@{} - :nextAttribute foreach ($attribute in $script.Attributes) { - # If the attribute does not have a key and value, - if (-not ($attribute.Key -and $attribute.value)) { - continue nextAttribute # continue to the next attribute - } - - # If we do not already know about this key - if (-not $attributeData[$attribute.key]) { - # set it - $attributeData[$attribute.key] = $attribute.value - } else { - # otherwise, make our data a list and add the new value - $attributeData[$attribute.key] = @( - $attributeData[$attribute.key] - ) + $attribute.Value - } - } - - # Now we want to try to match. - try { - # If either the `FilePattern` or `ContentTypePattern` matches - # we consider this to be a potential writer. - - # Loop thru the file patterns - foreach ($pattern in $attributeData.FilePattern) { - # skip parts that do not match. - if ($this.Uri -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method - } - # Loop thru the content type patterns - foreach ($pattern in $attributeData.ContentTypePattern) { - # skip parts that do not match. - if ($this.ContentType -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method. - } - } catch { - Write-Debug "$_" - } -}) - -# Now that we know all of the methods, we need to put them in order. -$WriteMethods = @( - $WriteMethods | # We can do this with a dynamic sort. - Sort-Object { - $in = $_ - # Simply walk over each attribute - foreach ($attr in $in.Script.Attributes) { - # any named `Order` with an integer value - if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { - # will use that value - return ($attr.Value -as [int]) - } - } - # Without an order attribute, use zero - return 0 - }, Name # and perform a secondary sort by name. -) - -# Now that we've gotten our writers and sorted them -# cache them on the object. -$this | Add-Member NoteProperty '#Writers' $WriteMethods -Force -# and return the cached value. -return $this.'#Writers' - - - - DefaultDisplay - Uri -ContentType -Reader - - - - - OpenPackage.Part - - - PSStandardMembers - - - DefaultDisplayPropertySet - - Uri - ContentType - Reader - - - - - - Export - - - - GetHash - - - - Import - - - - Read - - - - ReadCliXml - - - - ReadCSharp - - - - ReadFormData - - - - ReadJson - - - - ReadJsonL - - - - ReadMarkdown - - - - ReadPowerShell - - - - ReadPowerShellData - - - - ReadText - - - - ReadToml - - - - ReadXml - - - - ReadXsd - - - - ReadXslt - - - - ReadYaml - - - - Write - - - - WriteClixml - - - - WriteJson - - - - WritePowerShell - - - - WriteText - - - - WriteToml - - - - WriteXml - - - - WriteYaml - - - - Hash - - <# -.SYNOPSIS - Gets the part hash -.DESCRIPTION - Gets the part hash as a string -#> -$this.GetHash().Hash - - - - IsEponym - - <# -.SYNOPSIS - Determines if a part is an eponym -.DESCRIPTION - Eponyms are files whose name matches their directory. - - For example: - - `/foo/foo.ps1` is an eponym - `/foo/foo.md` is an eponym - `/foo/bar.ps1` is not an eponym - - If a package has an an identifier, - any files whose name matches this identifier will be considered an eponym. -.NOTES - Multiple eponyms may exist for a given path. - - Anything before the first period `.` will be considered the name of the file. -#> -param() -$part = $this - -$partSegments = @($part.Uri -split '/+' -ne '') - -if ($partSegments.Count -eq 1 -and $this.Package.Identifier) { - if ($partSegments[0] -replace '\..+$' -eq $this.Package.Identifier) { - return $true - } -} - -if ($partSegments.Count -ge 2) { - if ($partSegments[-2] -eq - ($partSegments[-1] -replace '\..+$') - ) { - return $true - } -} - -return $false - - - - Name - - <# -.SYNOPSIS - Gets the part name -.DESCRIPTION - Gets the part name. - - This is is anything before the first period (`.`). -#> -param() -@($this.Uri -split '/' -ne '')[-1] -replace '\..+?$' - - - - Palette - - <# -.SYNOPSIS - Gets an Open Package Part's palette -.DESCRIPTION - Gets an Open Package palette for a part -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param() - -if (-not $this.RelationshipExists -or - -not $this.GetRelationship) { - return -} - -if ($this.RelationshipExists('palette')) { - return $this.GetRelationship('palette').TargetUri -} - - - <# -.SYNOPSIS - Sets an Open Package Part's palette -.DESCRIPTION - Sets an Open Package palette for a part. -.NOTES - A palette is a relationship between a package and a stylesheet. -#> -param( -[Parameter(Mandatory)] -[ValidatePattern('.css$')] -[uri]$Palette -) -if (-not $this.RelationshipExists) { - return -} -if ($this.RelationshipExists('palette')) { - $this.DeleteRelationship('palette') - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} else { - $this.CreateRelationship($Palette, 'external', 'stylesheet', 'palette') -} - - - - - - Reader - - <# -.SYNOPSIS - Gets a Part's available Readers -.DESCRIPTION - Gets the different methods we can use to Read a part. -#> -param() - -# If we already have a cached list of Readers -if ($this.'#Readers') { - # return it. - return $this.'#Readers' -} - -# To get all of our methods, go over each method on this object -$ReadMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { - # and ignore any methods that do not start with `Read`. - if ($method.Name -notmatch 'Read.+?') { - continue - } - # If there is no script, ignore the method. - if (-not $method.Script) { continue } - $script = $method.Script - # If there are no attributes, ignore the method - if (-not $script.Attributes) { continue } - # Gather all of our attribute data - $attributeData = [Ordered]@{} - :nextAttribute foreach ($attribute in $script.Attributes) { - # If the attribute does not have a key and value, - if (-not ($attribute.Key -and $attribute.value)) { - continue nextAttribute # continue to the next attribute - } - - # If we do not already know about this key - if (-not $attributeData[$attribute.key]) { - # set it - $attributeData[$attribute.key] = $attribute.value - } else { - # otherwise, make our data a list and add the new value - $attributeData[$attribute.key] = @( - $attributeData[$attribute.key] - ) + $attribute.Value - } - } - - # Now we want to try to match. - try { - # If either the `FilePattern` or `ContentTypePattern` matches - # we consider this to be a potential Reader. - - # Loop thru the file patterns - foreach ($pattern in $attributeData.FilePattern) { - # skip parts that do not match. - if ($this.Uri -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method - } - # Loop thru the content type patterns - foreach ($pattern in $attributeData.ContentTypePattern) { - # skip parts that do not match. - if ($this.ContentType -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method. - } - } catch { - Write-Debug "$_" - } -}) - -# Now that we know all of the methods, we need to put them in order. -$ReadMethods = @( - $ReadMethods | # We can do this with a dynamic sort. - Sort-Object { - $in = $_ - # Simply walk over each attribute - foreach ($attr in $in.Script.Attributes) { - # any named `Order` with an integer value - if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { - # will use that value - return ($attr.Value -as [int]) - } - } - # Without an order attribute, use zero - return 0 - }, Name # and perform a secondary sort by name. -) - -# Now that we've gotten our Readers and sorted them -# cache them on the object. -$this | Add-Member NoteProperty '#Readers' $ReadMethods -Force -# and return the cached value. -return $this.'#Readers' - - - - Writer - - <# -.SYNOPSIS - Gets a Part's available writers -.DESCRIPTION - Gets the different methods we can use to Write to a part. -#> -param() - -# If we already have a cached list of writers -if ($this.'#Writers') { - # return it. - return $this.'#Writers' -} - -# To get all of our methods, go over each method on this object -$WriteMethods = @(:nextMethod foreach ($method in $this.PSObject.Methods) { - # and ignore any methods that do not start with `Write`. - if ($method.Name -notmatch 'Write.+?') { - continue - } - # If there is no script, ignore the method. - if (-not $method.Script) { continue } - $script = $method.Script - # If there are no attributes, ignore the method - if (-not $script.Attributes) { continue } - # Gather all of our attribute data - $attributeData = [Ordered]@{} - :nextAttribute foreach ($attribute in $script.Attributes) { - # If the attribute does not have a key and value, - if (-not ($attribute.Key -and $attribute.value)) { - continue nextAttribute # continue to the next attribute - } - - # If we do not already know about this key - if (-not $attributeData[$attribute.key]) { - # set it - $attributeData[$attribute.key] = $attribute.value - } else { - # otherwise, make our data a list and add the new value - $attributeData[$attribute.key] = @( - $attributeData[$attribute.key] - ) + $attribute.Value - } - } - - # Now we want to try to match. - try { - # If either the `FilePattern` or `ContentTypePattern` matches - # we consider this to be a potential writer. - - # Loop thru the file patterns - foreach ($pattern in $attributeData.FilePattern) { - # skip parts that do not match. - if ($this.Uri -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method - } - # Loop thru the content type patterns - foreach ($pattern in $attributeData.ContentTypePattern) { - # skip parts that do not match. - if ($this.ContentType -notmatch $pattern) { continue } - # Output the matching method - $method - continue nextMethod # and continue to the next method. - } - } catch { - Write-Debug "$_" - } -}) - -# Now that we know all of the methods, we need to put them in order. -$WriteMethods = @( - $WriteMethods | # We can do this with a dynamic sort. - Sort-Object { - $in = $_ - # Simply walk over each attribute - foreach ($attr in $in.Script.Attributes) { - # any named `Order` with an integer value - if ($attr.Key -eq 'Order' -and $attr.Value -as [int]) { - # will use that value - return ($attr.Value -as [int]) - } - } - # Without an order attribute, use zero - return 0 - }, Name # and perform a secondary sort by name. -) - -# Now that we've gotten our writers and sorted them -# cache them on the object. -$this | Add-Member NoteProperty '#Writers' $WriteMethods -Force -# and return the cached value. -return $this.'#Writers' - - - - DefaultDisplay - Uri -ContentType -Reader - - - System.IO.Packaging.PackagePart From 6013a2c4812225e53a52355967b3ca80f74a06ca Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 25 Mar 2026 19:00:27 -0700 Subject: [PATCH 511/724] docs: `OpenPackage.md` ( Fixes #158 ) --- OpenPackage.md | 103 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 OpenPackage.md diff --git a/OpenPackage.md b/OpenPackage.md new file mode 100644 index 0000000..804a78a --- /dev/null +++ b/OpenPackage.md @@ -0,0 +1,103 @@ +# Open Package + +Anything can become a package. + +## Open Packaging Conventions + +The [Open Package Conventions](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) are an Open Protocol for Open Packages. + +They were first published in 2006, in [ECMA-376](https://ecma-international.org/publications-and-standards/standards/ecma-376/) + +Like many file formats, Open Packages are zip files in a trenchcoat. + +Any Open Packaging Convention file can be renamed to .zip and extracted as-is. + +Unlike many archives, Open Packages have some distinct advantages: + +1. They contain parts _and_ content types +2. They contain core metadata +3. They can contain relationships + +These advantages are huge! + +They allow Open Packages to be an [Open Platform](OpenPlatform.md) built on [Open Protocols](OpenProtocol.md). + +### Open Package Parts + +Any time we make a web request, we're effectively asking for a file. Open Packages call this a "part" + +Any time a server sends a response, it's providing a file's content. + +In order for a browser to treat this correctly, a server sets the Content-Type header. + +By containing both the file and the content type, Open Packages are a natural choice for describing simple servers. + +This makes Open Packages a natural choice for making an Open Platform for applications: + +Just create a part in a package and you've got yourself an app. + +## Package Metadata + +Open Packages also contain metadata. + +Each Open Package has a small set of "core" metadata that any package may possess: + +In .NET, this information is described in the[`.PackageProperties`](https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties?view=windowsdesktop-10.0&wt.mc_id=MVP_321542) of an Open Package. + +The core package properties are: + +* `Category` +* `ContentStatus` +* `ContentType` +* `Created` +* `Creator` +* `Description` +* `Identifier` +* `Keywords` +* `Language` +* `LastModifiedBy` +* `LastPrinted` +* `Modified` +* `Revision` +* `Subject` +* `Title` +* `Version` + +These properties can make it easier to discover and inspect packages. + +It also makes it pretty trivial to make a package store: + +All we really need is the identifier and the version (though the Description and Creator are also nice to have) + +Packages can also contain as much additional metadata as they want inside of files + +### File Metadata + +Any package can include additional metadata in the package in an easy-to-read file format. + +For some examples: + +* Any Nuget package will have a `.nuspec` which describes this data and more in an XML file. +* Any PowerShell module will have a `.psd1` which describes the module. +* Any JavaScript package will have a `package.json` which describes the package contents + +Most package creation tools are effectively just verifying some metadata exists and then creating a package. + +Having an [Open Platform](OpenPlatform.md) for Open Packages makes this much easier. + +## Package Relationships + +Open Packages can also have any number of relationships. + +A relationship has a few components: + +* TargetUri (what this relates to) +* TargetMode (`Internal` or `External`) +* RelationshipType (what kind of relationship) +* Identifier (the relationship id) + +Relationships can exist on the package and on any part in the package. + +Relationships allow package to describe how they relate to themselves and the rest of the world. + +This lets a package describe it's relationships with [Open Protocols](OpenProtocol.md) \ No newline at end of file From 6f9df8217b8fcb05eed570df9cdbdbd021c58a4f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 25 Mar 2026 19:02:09 -0700 Subject: [PATCH 512/724] docs: `OpenPlatform.md` ( Fixes #159 ) --- OpenPlatform.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 OpenPlatform.md diff --git a/OpenPlatform.md b/OpenPlatform.md new file mode 100644 index 0000000..bbd6fe9 --- /dev/null +++ b/OpenPlatform.md @@ -0,0 +1,43 @@ + +# Open Platform + +OP is an open platform. + +It is a wonderful tool for working with [Open Packages](OpenPackage.md). + +Anything can be a package. + +Each package is a self-contained file tree, and a server waiting to happen. + +This makes Open Packages an incredibly simple and open file format for applications. + +## Web Apps + +Open Packages are made of parts and content types. + +This makes them ready to run as servers. + +To make a "Hello World" web app with OP, we can just + +~~~PowerShell +Start-OP @{"index.html" = "

    Hello World

    "} +~~~ + +This will create a package containing index.html, and start a server. + +## Local Apps + +Almost any existing application can be run with this platform: + +Simply package it up and install away. + +For example, if we want to install the latest version of PowerShell, we can run: + +~~~PowerShell +# Installs PowerShell as a dotnet tool +Install-OpenPackage https://www.nuget.org/packages/PowerShell +# (note: this package does not include arm64 binaries) +~~~ + +On Unix / Linux machines, you will need to run `chmod +x` on any binaries or scripts for them to run. + From f7d27e7d9ee26ed7904301d0062a591af26295dd Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 25 Mar 2026 19:03:07 -0700 Subject: [PATCH 513/724] docs: `OpenProtocol.md` ( Fixes #160 ) --- OpenProtocol.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 OpenProtocol.md diff --git a/OpenProtocol.md b/OpenProtocol.md new file mode 100644 index 0000000..7b1952c --- /dev/null +++ b/OpenProtocol.md @@ -0,0 +1,64 @@ +# Open Protocol + +Open Packages are an Open Protocol + +Anything can be a package. + +There is no strict requirement on what makes an open package. + +To store a package, it has to adhere to the [Open Package Conventions](https://en.wikipedia.org/wiki/Open_Packaging_Conventions). + +What the package contains is up to you! + +The protocol is open. + +That's the point. + +_Anything can be a package_. + +## Open Protocols + +Open Package is built upon Open Protocols. + +If a protocol can be read and written, we want to support it. + +We can get Open Packages from any number of Open Protocols: + +* Any URL +* Any At Protocol URI +* Any ZIP +* Any tar or tar.gz +* Any dictionary (recursively) + +Packages can written with any type of file, and many file formats are implicitly readable. + +For example, we can easily read any of these files as objects: + +* `.cs` +* `.clixml` +* `.json` +* `.ps1` +* `.psd1` +* `.svg` +* `.toml` +* `.xml` +* `.xsl` +* `.xsd` +* `.yaml` + +Packages can easily be exported to disk + +We would also welcome contributions to add additional file format support. + + + + + + + + + + + + + From 8f083521b12ded537cc01e9c6226a47027fa3c5d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 25 Mar 2026 19:07:11 -0700 Subject: [PATCH 514/724] docs: `OptimalPerformance.md` ( Fixes #167 ) --- OptimalPerformance.md | 119 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 OptimalPerformance.md diff --git a/OptimalPerformance.md b/OptimalPerformance.md new file mode 100644 index 0000000..40c7866 --- /dev/null +++ b/OptimalPerformance.md @@ -0,0 +1,119 @@ +# Optimal Performance + +There are many advantages to archives. + +Open Packages have several performance characteristics worth understanding. + +## Reduced Disk Impact + +An Open Package is an archive that can be loaded into memory. + +When we use OP to Open Packages, we are reading the files into memory. + +Please note the plural. + +We have an entire filesystem in memory! + +This means that every operation on that filesystem is also in memory. + +And this means that the disk is involved much less often. + +This is especially helpful for home applications and physical servers. + +Disks take more time to access than memory. + +When dealing with lots of files, this tends to be especially painful. + +Each read of the disk could be anywhere, and seek times vary. + +By using packages, we eliminate our disk impact. Instead, we impact memory. + +## Reduced Memory Impact + +Our reduced disk impact makes our reduced memory impact even more priceless. + +Normally, if we were mapping files to memory, +we'd expect to see a memory impact roughly equal to the file size. + +You _might_ get clever, and use some light compression. +At this point you are simply making your own package, anyway. + +Because we are storing an _archive_ in memory, +we are getting compression for free. + +The data is all implicitly compressed and decompressed as we read and write content. + +This is quite nice! + +It also offers some interesting benefits. + +## Consistent References + +Once a package is loaded into memory, +every change to that package is to the exact same object. + +This means that packages are easy to pass around in memory. + +Multiple thread jobs can also access the same package. + +Therefore, changes to a loaded package are nearly instant. + +The change does not need to propagate, because _it is the same object_. + +This is also true for package parts. Once the object exists, we are not recreating it, we are simply sharing a reference to it. + +And that reference is consistent. + +This enables the core functionality of Open Package: + +## Extensible Instances + +We can easily extend objects in PowerShell. + +This happens in three ways: + +1. We can use `Add-Member` to add information to an instance +2. We can use `Update-TypeData` to add information about a type +3. We can load a `.types.ps1xml`, which contains type data. + +OP is built by extending the types .NET uses to load packages. + +Primarily: + +* `System.IO.Packaging.Package` +* `System.IO.Packaging.PackagePart` + +Each instance may also `Add-Member` to it's heart's content. + +If you want to make your own methods for working with a package, just `Add-Member` + +Speaking of adding things, let's talk layers + +## Flexible Layering + +We can use packages in layers. + +This is a lot like how containers work: +Each layer in a container is effectively a filesystem. + +The difference is that, in memory, we can add layers as we need, in any order. + +We can allow some packages to be writeable, and others to not be. + +We can serve one layer that provides an experience around any files it can fetch. + +This can allow us to build applications as layers, instead of making everything a monolith. + +We can also swap out layers. + +For an example, image a photo viewer. + +All it needs to do is render images, but there's lot of ways to do this. + +We can easily make a photo viewer as it's own package, +load it with our package containing photos, +and have ourselves a gallery! + +# Open Possibility + +As you can see \ No newline at end of file From 8444ed307eba7dc18922afa0fe3a1ddc3bccdb5b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 25 Mar 2026 19:43:07 -0700 Subject: [PATCH 515/724] feat: `OpenPackage.Part.ReadMarkdown` ( Fixes #105 ) Stringifying return to html --- Types/OpenPackage.Part/ReadMarkdown.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/ReadMarkdown.ps1 b/Types/OpenPackage.Part/ReadMarkdown.ps1 index 7db6439..83e3c15 100644 --- a/Types/OpenPackage.Part/ReadMarkdown.ps1 +++ b/Types/OpenPackage.Part/ReadMarkdown.ps1 @@ -45,7 +45,8 @@ try { PSTypeName = 'text/markdown' Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) Markdown = "$partString" - } + } | + Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru } catch { Write-Warning "'$($this.Uri)' was not valid markdown: $_" } From 0fd25be7d89dbe8ebd298d3336a540995915aa34 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 02:43:26 +0000 Subject: [PATCH 516/724] feat: `OpenPackage.Part.ReadMarkdown` ( Fixes #105 ) Stringifying return to html --- OP.types.ps1xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 08f22ad..6a83ddb 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2554,7 +2554,8 @@ try { PSTypeName = 'text/markdown' Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) Markdown = "$partString" - } + } | + Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru } catch { Write-Warning "'$($this.Uri)' was not valid markdown: $_" } From c55e251bc7a7ccf8fbf1788861e248579735dc4e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 11:57:31 -0700 Subject: [PATCH 517/724] feat: `OpenPackage.Part.ReadText` ( Fixes #97 ) Adding more text file patterns. --- Types/OpenPackage.Part/ReadText.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Part/ReadText.ps1 b/Types/OpenPackage.Part/ReadText.ps1 index d1cfebf..a9f79bb 100644 --- a/Types/OpenPackage.Part/ReadText.ps1 +++ b/Types/OpenPackage.Part/ReadText.ps1 @@ -5,9 +5,9 @@ Reads Package Part Content as Text #> [Reflection.AssemblyMetadata( - # This should automatically apply to .txt files, modelfiles, and dockerfiles. + # This should automatically apply to .txt files, sql files, modelfiles, and dockerfiles. 'FilePattern', - '(?>[/\.]Dockerfile|[/\.]Modelfile|\.txt)$' + '(?>Dockerfile|Modelfile|c|h|cpp|cs|js|md|sql|txt)$' )] [Reflection.AssemblyMetadata( # This should automatically apply to any text/ content types From 18ef5aa6c49936cf92c1ee07ade98850986e1113 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 18:57:53 +0000 Subject: [PATCH 518/724] feat: `OpenPackage.Part.ReadText` ( Fixes #97 ) Adding more text file patterns. --- OP.types.ps1xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 6a83ddb..d6dcf9e 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2670,9 +2670,9 @@ if ($datablock.Ast.EndBlock.Statements[0].CommandsAllowed) { Reads Package Part Content as Text #> [Reflection.AssemblyMetadata( - # This should automatically apply to .txt files, modelfiles, and dockerfiles. + # This should automatically apply to .txt files, sql files, modelfiles, and dockerfiles. 'FilePattern', - '(?>[/\.]Dockerfile|[/\.]Modelfile|\.txt)$' + '(?>Dockerfile|Modelfile|c|h|cpp|cs|js|md|sql|txt)$' )] [Reflection.AssemblyMetadata( # This should automatically apply to any text/ content types From 69e8f3dedd772726533d812484183c2aeeb948a7 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 12:00:25 -0700 Subject: [PATCH 519/724] feat: `OpenPackage.Part.WriteJsonL` ( Fixes #169 ) --- Types/OpenPackage.Part/WriteJsonL.ps1 | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Types/OpenPackage.Part/WriteJsonL.ps1 diff --git a/Types/OpenPackage.Part/WriteJsonL.ps1 b/Types/OpenPackage.Part/WriteJsonL.ps1 new file mode 100644 index 0000000..1c11781 --- /dev/null +++ b/Types/OpenPackage.Part/WriteJsonL.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + Writes Part Content as Json Lines +.DESCRIPTION + Writes Open Package Part Content as Json Lines +#> +[Reflection.AssemblyMetadata( + 'FilePattern', + '\.(?>cast|jsonl|jsonnd)?$' +)] +param( +# The object to write. +[Alias('Input','Content','Text')] +[PSObject] +$InputObject, + +<# + +Any options used to write the object + +Supported Options: + +|Option|Description| +|-|-| +|Depth|The serialization depth| +|Encoding|The text encoding| +|Stream|Optional destination stream| + +#> +[Collections.IDictionary] +$Option = [Ordered]@{} +) + +# If this object does not have a write text method, return. +if (-not $this.WriteText) { throw 'No `.WriteText()`'; return } + +# If no depth was set, +if (-not $option.Depth) { + # use double the format enumeration limit (by default, 8) + $option.Depth = $FormatEnumerationLimit * 2 +} + +# If we have a .Package and .PartUri property +if ($InputObject.Package -is [IO.Packaging.Package] -and + $InputObject.PartUri -is [uri]) { + # avoid putting them in the object + $text = @(foreach ($in in $InputObject | + Select-Object -Property * -ExcludeProperty 'Package', 'PartUri') { + ConvertTo-Json -InputObject $in -Depth $Option.Depth -Compress + }) -join [Environment]::NewLine +} else { + # Convert any other objects to json. + $text = @(foreach ($in in $InputObject) { + ConvertTo-Json -InputObject $in -Depth $Option.Depth -Compress + }) -join [Environment]::NewLine + +} + +# Then, write the text +$this.WriteText($text, $Option) \ No newline at end of file From 2bc3011af6e69574b9f9dbbd5b0c5229b05f7217 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 19:00:51 +0000 Subject: [PATCH 520/724] feat: `OpenPackage.Part.WriteJsonL` ( Fixes #169 ) --- OP.types.ps1xml | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d6dcf9e..cd939be 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3135,6 +3135,71 @@ if ($InputObject.Package -is [IO.Packaging.Package] -and $text = ConvertTo-Json -InputObject $InputObject -Depth $Option.Depth } +# Then, write the text +$this.WriteText($text, $Option) + + + + WriteJsonL + From 592a50d06b4406ef1100fc178c97e63d9b7ff49e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 12:18:42 -0700 Subject: [PATCH 521/724] feat: `OpenPackage.Part.ReadJson` ( Fixes #99 ) Improving performance --- Types/OpenPackage.Part/ReadJson.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/ReadJson.ps1 b/Types/OpenPackage.Part/ReadJson.ps1 index 53dba58..538aa4e 100644 --- a/Types/OpenPackage.Part/ReadJson.ps1 +++ b/Types/OpenPackage.Part/ReadJson.ps1 @@ -17,8 +17,19 @@ param( [Alias('Options')] [Collections.IDictionary]$Option = [Ordered]@{} ) + if (-not $this.ReadText) { return } + $partText = $this.ReadText($InputObject, $Option) -ConvertFrom-Json -InputObject $partText + +if (-not $partText) { return } + +# This is faster than ConvertFrom-Json +$ConvertFromJson = [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson +if (-not $ConvertFromJson) { return } + +$ConvertFromJson.Invoke( + $partText, $option.hashtable, $option.Depth, [ref]$null +) From 4ccfb238326862f6346f1046a40a7976daa32245 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 19:19:02 +0000 Subject: [PATCH 522/724] feat: `OpenPackage.Part.ReadJson` ( Fixes #99 ) Improving performance --- OP.types.ps1xml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index cd939be..18c1070 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2461,9 +2461,20 @@ param( [Alias('Options')] [Collections.IDictionary]$Option = [Ordered]@{} ) + if (-not $this.ReadText) { return } + $partText = $this.ReadText($InputObject, $Option) -ConvertFrom-Json -InputObject $partText + +if (-not $partText) { return } + +# This is faster than ConvertFrom-Json +$ConvertFromJson = [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson +if (-not $ConvertFromJson) { return } + +$ConvertFromJson.Invoke( + $partText, $option.hashtable, $option.Depth, [ref]$null +) From 857a911b5e702d96f8d706ee82130fd718c31432 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 12:24:02 -0700 Subject: [PATCH 523/724] feat: `OpenPackage.Part.ReadJsonL` ( Fixes #104 ) Improving performance --- Types/OpenPackage.Part/ReadJsonL.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/ReadJsonL.ps1 b/Types/OpenPackage.Part/ReadJsonL.ps1 index ff4cc7c..cb294fd 100644 --- a/Types/OpenPackage.Part/ReadJsonL.ps1 +++ b/Types/OpenPackage.Part/ReadJsonL.ps1 @@ -24,6 +24,14 @@ param( ) if (-not $this.ReadText) { return } $partText = $this.ReadText($InputObject, $Option) -$partText -split '(?>\r\n|\n)' | ConvertFrom-Json +# This is faster than the cmdlet ConvertFrom-Json +$ConvertFromJson = [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson +if (-not $ConvertFromJson) { return } + +foreach ($line in $partText -split '(?>\r\n|\n)') { + $ConvertFromJson.Invoke( + $line, $option.hashtable, $option.Depth, [ref]$null + ) +} From d9a5b3c204140d9b4dc755bbb1f462a095aa8d5d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 19:24:19 +0000 Subject: [PATCH 524/724] feat: `OpenPackage.Part.ReadJsonL` ( Fixes #104 ) Improving performance --- OP.types.ps1xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 18c1070..af7a4b2 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2509,7 +2509,15 @@ param( ) if (-not $this.ReadText) { return } $partText = $this.ReadText($InputObject, $Option) -$partText -split '(?>\r\n|\n)' | ConvertFrom-Json +# This is faster than the cmdlet ConvertFrom-Json +$ConvertFromJson = [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson +if (-not $ConvertFromJson) { return } + +foreach ($line in $partText -split '(?>\r\n|\n)') { + $ConvertFromJson.Invoke( + $line, $option.hashtable, $option.Depth, [ref]$null + ) +} From 673967fc5d3782035bf2f7925a6946da55edba14 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 12:25:12 -0700 Subject: [PATCH 525/724] feat: `OpenPackage.Part.Get/Set` ( Fixes #95, Fixes #143 ) Aliasing Get to Read and Set to Write --- Types/OpenPackage.Part/Alias.psd1 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Types/OpenPackage.Part/Alias.psd1 diff --git a/Types/OpenPackage.Part/Alias.psd1 b/Types/OpenPackage.Part/Alias.psd1 new file mode 100644 index 0000000..31eeb36 --- /dev/null +++ b/Types/OpenPackage.Part/Alias.psd1 @@ -0,0 +1,4 @@ +@{ + Get = "Read" + Set = "Write" +} \ No newline at end of file From c64c30d63324f7962ffa4abc8f7f649dacf43f4d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 19:25:33 +0000 Subject: [PATCH 526/724] feat: `OpenPackage.Part.Get/Set` ( Fixes #95, Fixes #143 ) Aliasing Get to Read and Set to Write --- OP.types.ps1xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index af7a4b2..ebda84b 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2109,6 +2109,14 @@ if ($TypeMap.Count -eq 0) {
    + + Get + Read + + + Set + Write + Export + + Culture + + <# +.SYNOPSIS + Gets the culture of the part. +.DESCRIPTION + Gets the `[CultureInfo]` associated with a package part. +#> +param() +# Get all the culture names +$allCultures = [cultureinfo]::GetCultures('all').name + +# Split the uri into segments +foreach ($segment in $this.Uri -split '/' -ne '') { + # if any segment is a culture name + if ($segment -in $allCultures) { + $segment -as [cultureinfo] + # it applies to that segment. + } +} + + + Hash From c2d2d2ff0c022bcd3c02cbb56a0a20b48c76aaea Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 26 Mar 2026 19:37:00 -0700 Subject: [PATCH 534/724] feat: `Install-OpenPackage` ( Fixes #52 ) Removing -NoBackup --- Commands/Install-OpenPackage.ps1 | 55 ++++++-------------------------- 1 file changed, 9 insertions(+), 46 deletions(-) diff --git a/Commands/Install-OpenPackage.ps1 b/Commands/Install-OpenPackage.ps1 index 97d6d11..dfa6634 100644 --- a/Commands/Install-OpenPackage.ps1 +++ b/Commands/Install-OpenPackage.ps1 @@ -88,11 +88,7 @@ function Install-OpenPackage [Parameter(ValueFromPipeline)] [Alias('Package')] [PSObject] - $InputObject, - - # If set, will not install the archive into backup paths. - [switch] - $NoBackup, + $InputObject, # If set, will overwrite existing files. [switch] @@ -241,43 +237,7 @@ function Install-OpenPackage } # Get all of the package parts - $inputParts = @(Select-OpenPackage @selectSplat) - - if ( - # If -NoBackup was passed, - (-not $NoBackup) -or - # or we are explicitly installing into a path - $PSBoundParameters['DestinationPath'] - ) { - # copy the open package - $copyOpenPackage = @(Copy-OpenPackage @selectSplat) - - # create a `.zip` - $zipCopy = $copyOpenPackage | - Export-OpenPackage -DestinationPath ( - $DestinationPath | - Split-Path | - Join-Path -ChildPath "$($package.PackageProperties.Identifier).zip" - ) -Force - - # If -PassThru was passed - if ($passThru) { - $zipCopy # output that zip - } - - # create an `.op` - $opCopy = $copyOpenPackage | - Export-OpenPackage -DestinationPath ( - $DestinationPath | - Split-Path | - Join-Path -ChildPath "$($package.PackageProperties.Identifier).op" - ) -Force - - # If -PassThru was passed - if ($PassThru) { - $opCopy # output that `op` - } - } + $inputParts = @(Select-OpenPackage @selectSplat) # Now let's prepare our progress bars $total = $inputParts.Length @@ -320,10 +280,13 @@ function Install-OpenPackage # We will warn when we're done, # but don't -Force the point by warning each time. # Add it to the list of existing files - $existingFiles += Get-Item -LiteralPath $partDestination - if ($passThru) { - $PSCmdlet.WriteObject($existingFiles[-1]) - } + $existingFile = [IO.FileInfo]"$partDestination" + if ($existingFile) { + $existingFiles += $existingFile + if ($passThru) { + $PSCmdlet.WriteObject($existingFiles[-1]) + } + } continue nextPart # (and continue to the next part). } New-Item -ItemType File -Path $partDestination -Force From 328285bc6e30002277e16aff159aba848b4afbc1 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Mar 2026 18:34:46 -0700 Subject: [PATCH 535/724] feat: `OpenPackage.get_Version` ( Fixes #17 ) Only automatically assigning the first version encountered for each package type. Also, adding nuget support. --- Types/OpenPackage/get_Version.ps1 | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage/get_Version.ps1 b/Types/OpenPackage/get_Version.ps1 index 40fce54..ae2bd44 100644 --- a/Types/OpenPackage/get_Version.ps1 +++ b/Types/OpenPackage/get_Version.ps1 @@ -4,7 +4,11 @@ .DESCRIPTION Gets the OpenPackage `Version` property. - If this has not been explicitly set, looks for potential version informatin + If this has not been explicitly set, looks for potential version information in: + + * PowerShell Manifests + * package.json + * nuspec files .LINK https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 #> @@ -14,14 +18,22 @@ if ($this.PackageProperties.Version) { return $this.PackageProperties.Version } -$moduleManifest = $this.PowerShellManifest +$moduleManifest = @($this.PowerShellManifest)[0] if ($moduleManifest) { $this.PackageProperties.Version = $moduleManifest.ModuleVersion return $this.PackageProperties.Version } -$packageJson = $this.'Package.json' +$packageJson = @($this.'Package.json')[0] if ($packageJson -and $packageJson.version) { $this.PackageProperties.Version = $packageJson.version return $this.PackageProperties.Version +} + +$nuSpec = @($this.nuSpec)[0] +if ($nuSpec -and $nuSpec.package.metadata.version) { + $this.PackageProperties.Version = + $nuSpec.package.metadata.version + + return $this.PackageProperties.Version } \ No newline at end of file From 78534c77b1973a4bd12f42266a14ac6e8345ac60 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Mar 2026 01:35:06 +0000 Subject: [PATCH 536/724] feat: `OpenPackage.get_Version` ( Fixes #17 ) Only automatically assigning the first version encountered for each package type. Also, adding nuget support. --- OP.types.ps1xml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 7b5efb5..ca823b8 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1829,7 +1829,11 @@ return $this.GetTree("/_") .DESCRIPTION Gets the OpenPackage `Version` property. - If this has not been explicitly set, looks for potential version informatin + If this has not been explicitly set, looks for potential version information in: + + * PowerShell Manifests + * package.json + * nuspec files .LINK https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties.version?wt.mc_id=MVP_321542 #> @@ -1839,17 +1843,25 @@ if ($this.PackageProperties.Version) { return $this.PackageProperties.Version } -$moduleManifest = $this.PowerShellManifest +$moduleManifest = @($this.PowerShellManifest)[0] if ($moduleManifest) { $this.PackageProperties.Version = $moduleManifest.ModuleVersion return $this.PackageProperties.Version } -$packageJson = $this.'Package.json' +$packageJson = @($this.'Package.json')[0] if ($packageJson -and $packageJson.version) { $this.PackageProperties.Version = $packageJson.version return $this.PackageProperties.Version } + +$nuSpec = @($this.nuSpec)[0] +if ($nuSpec -and $nuSpec.package.metadata.version) { + $this.PackageProperties.Version = + $nuSpec.package.metadata.version + + return $this.PackageProperties.Version +} <# From 9f6f0cc49af5d12ad253873609ee901773809d96 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Mar 2026 18:40:47 -0700 Subject: [PATCH 537/724] feat: `OpenPackage.Part.Read` ( Fixes #95 ) Only adding Package and PartUri if it would not override existing data --- Types/OpenPackage.Part/Read.ps1 | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage.Part/Read.ps1 b/Types/OpenPackage.Part/Read.ps1 index d35020f..977588b 100644 --- a/Types/OpenPackage.Part/Read.ps1 +++ b/Types/OpenPackage.Part/Read.ps1 @@ -16,10 +16,20 @@ param( [Collections.IDictionary]$Option = [Ordered]@{} ) +# We want items returned from .Read to know their package and part +# so delcare a filter for that. filter addPackageAndPart { - $_ | - Add-Member NoteProperty Package $this.Package -Force -PassThru | - Add-Member NoteProperty PartUri $this.Uri -Force -PassThru + $in = $_ + # Make sure we do not overwrite any information + if ($in -and -not $in.package) { + $in | + Add-Member NoteProperty Package $this.Package -Force + } + if ($in -and -not $in.partUri) { + $in | + Add-Member NoteProperty PartUri $this.Uri -Force + } + $_ } $orderedMethods = @($this.Reader) From 8d07e3b0a768551fc49cdccb25c83b15ee7dc243 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Mar 2026 01:41:07 +0000 Subject: [PATCH 538/724] feat: `OpenPackage.Part.Read` ( Fixes #95 ) Only adding Package and PartUri if it would not override existing data --- OP.types.ps1xml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index ca823b8..d6b8b89 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2324,10 +2324,20 @@ param( [Collections.IDictionary]$Option = [Ordered]@{} ) +# We want items returned from .Read to know their package and part +# so delcare a filter for that. filter addPackageAndPart { - $_ | - Add-Member NoteProperty Package $this.Package -Force -PassThru | - Add-Member NoteProperty PartUri $this.Uri -Force -PassThru + $in = $_ + # Make sure we do not overwrite any information + if ($in -and -not $in.package) { + $in | + Add-Member NoteProperty Package $this.Package -Force + } + if ($in -and -not $in.partUri) { + $in | + Add-Member NoteProperty PartUri $this.Uri -Force + } + $_ } $orderedMethods = @($this.Reader) From 1a1df06966d706032528f446617f50076e6c3f7a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Mar 2026 22:13:55 -0700 Subject: [PATCH 539/724] feat: `OpenPackage.Relate` ( Fixes #173 ) --- Types/OpenPackage/Relate.ps1 | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 Types/OpenPackage/Relate.ps1 diff --git a/Types/OpenPackage/Relate.ps1 b/Types/OpenPackage/Relate.ps1 new file mode 100644 index 0000000..16d5b0f --- /dev/null +++ b/Types/OpenPackage/Relate.ps1 @@ -0,0 +1,52 @@ +<# +.SYNOPSIS + Adds Relationships to a Package +.DESCRIPTION + Simplifies adding relationships to a package. + + All relationships are external. + + If a relationship type is not provided, will default to "unknown" + + If no id is provided, will default to the relationship type. + + If relations with that type already exists, will append a counter to the id. +#> +param( +# The relationship uri +[Parameter(Mandatory)] +[uri]$uri, +# The relationship type. +# If this is not provided, will default to `unknown` +[string]$type, +# The relationship id +# If not provided, will default to the type. +# If any relationships of the type already exist, will append a counter to the id. +[string]$id +) + +# Return if we cannot have relationships. +if (-not $this.GetRelationships) { return } +if (-not $this.CreateRelationship) { return } +# Default the type to `unknown` +if (-not $type) { $type = 'unknown'} +# and default the `$id` to the `$type`. +if (-not $id) { $id = $type } + +# Get our relations +$relations = @( + foreach ($relation in $this.GetRelationships()) { + if ($relation.RelationshipType -eq $type) { + $relation + } + } +) + +# If we have no relations, +if (-not $relations) { + # hard relate. + $this.CreateRelationship($uri, 'External', $type, $id) +} else { + # Otherwise, create another relationship of the same type. + $this.CreateRelationship($uri, 'External', $type, "$($id)$($relations.Count)") +} \ No newline at end of file From 1deb2b744523d224d2cea92ab15dbae9c14b1c6f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Mar 2026 05:14:15 +0000 Subject: [PATCH 540/724] feat: `OpenPackage.Relate` ( Fixes #173 ) --- OP.types.ps1xml | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d6b8b89..0838ddc 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -273,6 +273,63 @@ if (-not $this.GetParts) { return } $ContentPattern.Matches($partText) | Add-Member NoteProperty Uri $part.Uri -Force -PassThru | Add-Member NoteProperty Package $this +} + + + + Relate + From 8daf4b365c8a4ce3afdbfccaee3853e5f5ad1d24 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Mar 2026 22:14:56 -0700 Subject: [PATCH 541/724] feat: `OpenPackage.Part.Relate` ( Fixes #174 ) --- Types/OpenPackage.Part/Relate.ps1 | 56 +++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Types/OpenPackage.Part/Relate.ps1 diff --git a/Types/OpenPackage.Part/Relate.ps1 b/Types/OpenPackage.Part/Relate.ps1 new file mode 100644 index 0000000..9e9fbbb --- /dev/null +++ b/Types/OpenPackage.Part/Relate.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS + Adds Relationships to a package part +.DESCRIPTION + Simplifies adding relationships to a package part. + + All relationships are external. + + If a relationship type is not provided, will default to "unknown" + + If no id is provided, will default to the relationship type. + + If relations with that type already exists, will append a counter to the id. +#> +param( +# The relationship uri +[Parameter(Mandatory)] +[uri]$uri, +# The relationship type. +# If this is not provided, will default to `unknown` +[string]$type, +# The relationship id +# If not provided, will default to the type. +# If any relationships of the type already exist, will append a counter to the id. +[string]$id +) + +# Return if we cannot have relationships. +if (-not $this.GetRelationships) { return } +if (-not $this.CreateRelationship) { return } +# Default the type to `unknown` +if (-not $type) { $type = 'unknown'} +# and default the `$id` to the `$type`. +if (-not $id) { $id = $type } + +# Get our relations +$relations = @( + foreach ($relation in $this.GetRelationships()) { + if ($relation.RelationshipType -eq $type) { + $relation + } + } +) + +# If we have no relations, +if (-not $relations) { + # hard relate. + $this.CreateRelationship( + $uri, 'External', $type, $id + ) +} else { + # Otherwise, create another relationship of the same type. + $this.CreateRelationship( + $uri, 'External', $type, "$($id)$($relations.Count)" + ) +} \ No newline at end of file From 34a09b19f37bf57c2b9dd28689aa85d515443078 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Mar 2026 05:15:14 +0000 Subject: [PATCH 542/724] feat: `OpenPackage.Part.Relate` ( Fixes #174 ) --- OP.types.ps1xml | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0838ddc..25dd4ed 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -3092,6 +3092,67 @@ if (-not $convertFromYamlCommand -or -not $convertFromYamlCommand.Parameters.Inp } catch { Write-Warning "'$($thisPart.Uri)' was not valid yaml: $_" } +} + + + + Relate + From c22402d907425584641e57d734635c03a2d3e19d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Mar 2026 23:33:38 -0700 Subject: [PATCH 543/724] feat: `Get-OpenPackage -IncludeSite` ( Fixes #2, Fixes #115, Fixes #116 ) Also adding relationships to repository and directory --- Commands/Get-OpenPackage.ps1 | 17 +++++++-- Types/OpenPackage.Source/Directory.ps1 | 49 ++++++++++++++++++------- Types/OpenPackage.Source/Repository.ps1 | 21 +++++++++-- 3 files changed, 66 insertions(+), 21 deletions(-) diff --git a/Commands/Get-OpenPackage.ps1 b/Commands/Get-OpenPackage.ps1 index 780c66f..1ab81f7 100644 --- a/Commands/Get-OpenPackage.ps1 +++ b/Commands/Get-OpenPackage.ps1 @@ -87,7 +87,8 @@ function Get-OpenPackage #> [CmdletBinding(PositionalBinding=$false,DefaultParameterSetName='Any',SupportsPaging)] [Alias( - 'Get-OP', 'OP', 'OpenPackage','gOpenPackage' + 'Get-OP', 'OP', 'OpenPackage','gOpenPackage', + 'Open-OpenPackage', 'Open-OP', 'opop', 'opOpenPackage' )] param( # Any unnamed arguments to the command. @@ -229,14 +230,22 @@ function Get-OpenPackage $IncludeHidden, # If set, will include the `.git` directory contents if found. + # By default, this content will be excluded. [Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] [switch] $IncludeGit, # If set, will include any content found in `/node_modules`. + # By default, this content will be excluded. [Alias('IncludeNodeModules')] [switch] - $IncludeNodeModule + $IncludeNodeModule, + + # If set, will include any content found in `/_site`. + # By default, this content will be excluded. + [Alias('IncludeWebsite')] + [switch] + $IncludeSite ) begin { @@ -279,8 +288,10 @@ function Get-OpenPackage } } + $ExecutionContext.SessionState.PSVariable.Remove('function:func') + try { - & $func @bindableParameters + & $OpTypeData.Members[$Name].Script @bindableParameters } catch { $PSCmdlet.WriteError($_) } diff --git a/Types/OpenPackage.Source/Directory.ps1 b/Types/OpenPackage.Source/Directory.ps1 index 94d2dab..4b4025d 100644 --- a/Types/OpenPackage.Source/Directory.ps1 +++ b/Types/OpenPackage.Source/Directory.ps1 @@ -48,15 +48,23 @@ $Force, $IncludeHidden, # If set, will include the `.git` directory contents if found. +# By default, this content will be excluded. [Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] [switch] $IncludeGit, # If set, will include any content found in `/node_modules`. +# By default, this content will be excluded. [Alias('IncludeNodeModules')] [switch] $IncludeNodeModule, +# If set, will include any content found in `/_site`. +# By default, this content will be excluded. +[Alias('IncludeWebsite')] +[switch] +$IncludeSite, + # The current package [IO.Packaging.Package] $Package @@ -114,18 +122,25 @@ foreach ($resolvedItem in $resolvedItems) { )) ) { # get it's first remote - @(& $gitApp '-C' $resolvedItem.FullName remote)[0] | + $gitRemote = @(& $gitApp '-C' $resolvedItem.FullName remote)[0] | ForEach-Object { & $gitApp '-C' $resolvedItem.FullName remote get-url $_ - } | - Foreach-Object { - if ($package.RelationshipExists('repository')) { - $package.DeleteRelationship('repository') - } - $null = $package.CreateRelationship($_, 'External', 'git', 'repository') } + + $relation = $package.Relate($gitRemote,'git','repository') + Write-Verbose "Related $( + $relation.TargetUri + ) as [$($relation.RelationshipType)]$($relation.id)" } + $sourceDirectoryUri = + "file://$($resolvedItem.FullName -replace '[\\/]', '/')" + + $relation = $package.Relate($sourceDirectoryUri,'source', 'directory') + Write-Verbose "Related $( + $relation.TargetUri + ) as [$($relation.RelationshipType)]$($relation.id)" + # So declare an oldest created file and newest write time. $oldestCreationTime = [DateTime]::Now @@ -143,6 +158,9 @@ foreach ($resolvedItem in $resolvedItems) { if (-not $IncludeNodeModule -and $file -match '[\\/]node_modules[\\/]') { continue } + if (-not $IncludeSite -and $file -match '[\\/]_site[\\/]') { + continue + } if ($exclude) { foreach ($exclusion in $Exclude) { if ($file.FullName -like $exclusion) { @@ -184,21 +202,23 @@ foreach ($resolvedItem in $resolvedItems) { # write a message to verbose indicating we are skipping the file. Write-Verbose "Skipping blank file $($file.FullName)" continue - } - - # encode our URI, + } + + # encode our URI, $encodedUri = [IO.Packaging.PackUriHelper]::CreatePartUri($relativeUri) + + # make it a root relative uri. $relativeUri = '/' + ($encodedUri -replace '^/') # Determine the right content type for the extension $fileContentType = $typeMap[$file.Extension] # and fall back to text/plain if (-not $fileContentType) { $fileContentType = 'text/plain'} - + # Then update our creation times / last write times as needed. if ($file.CreationTime -lt $oldestCreationTime) { $oldestCreationTime = $file.CreationTime - } + } if ($file.LastWriteTime -gt $lastWriteTime) { $lastWriteTime = $file.LastWriteTime } @@ -246,10 +266,11 @@ foreach ($resolvedItem in $resolvedItems) { # If we could not create the part, continue if (-not $newPart) { continue } + $newStream = $newPart.GetStream() $newStream.Write($fileBytes, 0, $fileBytes.Length) - $newStream.Close() - $null = $newStream.DisposeAsync() + $newStream.Close() + $null = $newStream.DisposeAsync() } Pop-Location diff --git a/Types/OpenPackage.Source/Repository.ps1 b/Types/OpenPackage.Source/Repository.ps1 index b7c3a36..201a92b 100644 --- a/Types/OpenPackage.Source/Repository.ps1 +++ b/Types/OpenPackage.Source/Repository.ps1 @@ -8,7 +8,7 @@ param( # A Repository to package. # This can be the root of a repo or a link to a portion of the tree. # If a portion of the tree is provided, will perform a sparse clone of the repository -[Parameter(Mandatory,ValueFromPipelineByPropertyName)] +[Parameter(ValueFromPipelineByPropertyName)] [Alias('clone_url')] [string] $Repository, @@ -64,15 +64,28 @@ $Force, $IncludeHidden, # If set, will include the `.git` directory contents if found. +# By default, this content will be excluded. +[Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] [switch] $IncludeGit, # If set, will include any content found in `/node_modules`. +# By default, this content will be excluded. [Alias('IncludeNodeModules')] [switch] -$IncludeNodeModule +$IncludeNodeModule, + +# If set, will include any content found in `/_site`. +# By default, this content will be excluded. +[Alias('IncludeWebsite')] +[switch] +$IncludeSite ) +if (-not $repository) { + throw "No repository" +} + $gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git', 'Application') if (-not $gitApp) { throw "No git" @@ -183,7 +196,7 @@ if (-not (Test-Path $repoDirectory)) { $gitProgress.Completed = $true Write-Progress @gitProgress # and call ourselves with the repoDirectory - Get-OpenPackage -FilePath $repoDirectory @namedParameters + Get-OpenPackage -FilePath $pwd @namedParameters Pop-Location } else { # If no sparse filters were provided, just clone @@ -196,7 +209,7 @@ if (-not (Test-Path $repoDirectory)) { if ($?) { Push-Location $repoDirectory # and call ourself - Get-OpenPackage -FilePath $repoDirectory @namedParameters + Get-OpenPackage -FilePath $pwd @namedParameters Pop-Location } } From 26edfe3acd49119c0bac9bf33390592e100dc687 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Mar 2026 06:33:56 +0000 Subject: [PATCH 544/724] feat: `Get-OpenPackage -IncludeSite` ( Fixes #2, Fixes #115, Fixes #116 ) Also adding relationships to repository and directory --- OP.types.ps1xml | 70 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 25dd4ed..fadd334 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4929,15 +4929,23 @@ $Force, $IncludeHidden, # If set, will include the `.git` directory contents if found. +# By default, this content will be excluded. [Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] [switch] $IncludeGit, # If set, will include any content found in `/node_modules`. +# By default, this content will be excluded. [Alias('IncludeNodeModules')] [switch] $IncludeNodeModule, +# If set, will include any content found in `/_site`. +# By default, this content will be excluded. +[Alias('IncludeWebsite')] +[switch] +$IncludeSite, + # The current package [IO.Packaging.Package] $Package @@ -4995,18 +5003,25 @@ foreach ($resolvedItem in $resolvedItems) { )) ) { # get it's first remote - @(& $gitApp '-C' $resolvedItem.FullName remote)[0] | + $gitRemote = @(& $gitApp '-C' $resolvedItem.FullName remote)[0] | ForEach-Object { & $gitApp '-C' $resolvedItem.FullName remote get-url $_ - } | - Foreach-Object { - if ($package.RelationshipExists('repository')) { - $package.DeleteRelationship('repository') - } - $null = $package.CreateRelationship($_, 'External', 'git', 'repository') } + + $relation = $package.Relate($gitRemote,'git','repository') + Write-Verbose "Related $( + $relation.TargetUri + ) as [$($relation.RelationshipType)]$($relation.id)" } + $sourceDirectoryUri = + "file://$($resolvedItem.FullName -replace '[\\/]', '/')" + + $relation = $package.Relate($sourceDirectoryUri,'source', 'directory') + Write-Verbose "Related $( + $relation.TargetUri + ) as [$($relation.RelationshipType)]$($relation.id)" + # So declare an oldest created file and newest write time. $oldestCreationTime = [DateTime]::Now @@ -5024,6 +5039,9 @@ foreach ($resolvedItem in $resolvedItems) { if (-not $IncludeNodeModule -and $file -match '[\\/]node_modules[\\/]') { continue } + if (-not $IncludeSite -and $file -match '[\\/]_site[\\/]') { + continue + } if ($exclude) { foreach ($exclusion in $Exclude) { if ($file.FullName -like $exclusion) { @@ -5065,21 +5083,23 @@ foreach ($resolvedItem in $resolvedItems) { # write a message to verbose indicating we are skipping the file. Write-Verbose "Skipping blank file $($file.FullName)" continue - } - - # encode our URI, + } + + # encode our URI, $encodedUri = [IO.Packaging.PackUriHelper]::CreatePartUri($relativeUri) + + # make it a root relative uri. $relativeUri = '/' + ($encodedUri -replace '^/') # Determine the right content type for the extension $fileContentType = $typeMap[$file.Extension] # and fall back to text/plain if (-not $fileContentType) { $fileContentType = 'text/plain'} - + # Then update our creation times / last write times as needed. if ($file.CreationTime -lt $oldestCreationTime) { $oldestCreationTime = $file.CreationTime - } + } if ($file.LastWriteTime -gt $lastWriteTime) { $lastWriteTime = $file.LastWriteTime } @@ -5127,10 +5147,11 @@ foreach ($resolvedItem in $resolvedItems) { # If we could not create the part, continue if (-not $newPart) { continue } + $newStream = $newPart.GetStream() $newStream.Write($fileBytes, 0, $fileBytes.Length) - $newStream.Close() - $null = $newStream.DisposeAsync() + $newStream.Close() + $null = $newStream.DisposeAsync() } Pop-Location @@ -5214,7 +5235,7 @@ param( # A Repository to package. # This can be the root of a repo or a link to a portion of the tree. # If a portion of the tree is provided, will perform a sparse clone of the repository -[Parameter(Mandatory,ValueFromPipelineByPropertyName)] +[Parameter(ValueFromPipelineByPropertyName)] [Alias('clone_url')] [string] $Repository, @@ -5270,15 +5291,28 @@ $Force, $IncludeHidden, # If set, will include the `.git` directory contents if found. +# By default, this content will be excluded. +[Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] [switch] $IncludeGit, # If set, will include any content found in `/node_modules`. +# By default, this content will be excluded. [Alias('IncludeNodeModules')] [switch] -$IncludeNodeModule +$IncludeNodeModule, + +# If set, will include any content found in `/_site`. +# By default, this content will be excluded. +[Alias('IncludeWebsite')] +[switch] +$IncludeSite ) +if (-not $repository) { + throw "No repository" +} + $gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git', 'Application') if (-not $gitApp) { throw "No git" @@ -5389,7 +5423,7 @@ if (-not (Test-Path $repoDirectory)) { $gitProgress.Completed = $true Write-Progress @gitProgress # and call ourselves with the repoDirectory - Get-OpenPackage -FilePath $repoDirectory @namedParameters + Get-OpenPackage -FilePath $pwd @namedParameters Pop-Location } else { # If no sparse filters were provided, just clone @@ -5402,7 +5436,7 @@ if (-not (Test-Path $repoDirectory)) { if ($?) { Push-Location $repoDirectory # and call ourself - Get-OpenPackage -FilePath $repoDirectory @namedParameters + Get-OpenPackage -FilePath $pwd @namedParameters Pop-Location } } From eafa96ae78b50bceaa26e46fd90336d7799b50b9 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 27 Mar 2026 23:37:42 -0700 Subject: [PATCH 545/724] feat: `Get-OpenPackage` Open aliasing ( Fixes #2 ) Allowing using Open as an alternate verb, for lexical compatibility --- OP.psd1 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/OP.psd1 b/OP.psd1 index 04c598a..1064bf7 100644 --- a/OP.psd1 +++ b/OP.psd1 @@ -99,6 +99,7 @@ VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. AliasesToExport = @( 'Close-OP', 'csop','csOpenPackage' + 'Copy-OP','cpop','cpOpenPackage' 'Expand-OP', 'enop','enOpenPackage' @@ -109,6 +110,7 @@ AliasesToExport = @( 'Format-OP', 'fop', 'fOpenPackage' 'Get-OP', 'OP', 'OpenPackage','gOpenPackage' + 'Open-OpenPackage', 'Open-OP', 'opop', 'opOpenPackage' 'Install-OP', 'inop', 'inOpenPackage', 'Expand-OpenPackage','Expand-OP', 'enop','enOpenPackage' @@ -149,7 +151,17 @@ PrivateData = @{ PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('OpenPackages','Zip','Nuget','PowerShellGallery','Git','AtProto','WebServer','Overpowered') + Tags = @('OpenPackage', + 'Zip', + 'Nuget', + 'PowerShellGallery', + 'Git', + 'AtProto', + 'WebServer', + 'OpenPlatform', + 'OpenProtocol', + 'Overpowered' + ) # A URL to the license for this module. LicenseUri = 'https://github.com/PoshWeb/OP/tree/main/LICENSE' From 8b5e0055e0eeb4eab90330f4fd3f46583b897bfd Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Mar 2026 16:22:18 -0700 Subject: [PATCH 546/724] feat: `OpenPackage.View.Tree.md` ( Fixes #177 ) --- Types/OpenPackage.View/Tree.md.ps1 | 111 +++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 Types/OpenPackage.View/Tree.md.ps1 diff --git a/Types/OpenPackage.View/Tree.md.ps1 b/Types/OpenPackage.View/Tree.md.ps1 new file mode 100644 index 0000000..5efc4f2 --- /dev/null +++ b/Types/OpenPackage.View/Tree.md.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS + Shows a markdown package tree +.DESCRIPTION + Shows a package tree as markdown. +.NOTES + Will ignore any input that is not a package +#> +[OutputType('text/plain')] +param( +# Any input. +# If this is not a package, or does not contain a package, the input will be ignored. +[Parameter(ValueFromPipeline)] +[Alias('Package')] +[PSObject] +$InputObject, + +# The file pattern. All files that match this pattern will be urn. +[string] +$FilePattern = '.' +) + +$allInput = @($input) +if ((-not $allInput) -and $InputObject) { + $allInput += $InputObject +} + + +filter toIndex { + $in = $_ + + if ($in -is [Collections.IDictionary]) { + $in.GetEnumerator() | . toIndex + } elseif ($in.Key -is [string]) { + $prefix = "$(' ' * $depth * 2)* " + $depth++ + if ($in.Value -is [Collections.IDictionary]) { + if ( + $in.Value.Keys -match 'index\..+?$' -and + $in.Value.Values.Uri + ) { + $prefix + "[$($in.Key)]($( + $rootUrl + + @( + @($in.Value.Values.Uri) -match 'index\..+?$' + )[0] -replace 'index\..+?$' + ))" + } else { + $prefix + $in.Key + } + + $in.Value | . toIndex + } elseif ($in.Value.Uri) { + $prefix + "[$($in.Key)]($($rootUrl)$($in.Value.Uri))" + } elseif ($in.Value.FullName) { + $prefix + "[$($in.Key)]($($in.Value.FullName))" + } else { + $prefix + $in.Key + } + $depth-- + } + +} + +foreach ($in in $allInput) { + $fileTree = $null + $rootUrl = '' + if ($in.GetTree) { + $fileTree = $in.GetTree($FilePattern) + } + if ($in.Package.GetTree) { + $fileTree = $in.Package.GetTree($FilePattern) + } + if ($in.Url) { + $rootUrl = $in.Url + } + + if ($in -is [IO.DirectoryInfo]) { + $filelist = @(Get-ChildItem -Path $in -File -Recurse) + $fileTree = [Ordered]@{} + foreach ($fileInfo in $filelist) { + $relativePath = $fileInfo.FullName.Substring($in.FullName.Length + 1) + $pointer = $fileTree + $hierarchy = @($relativePath -split '[\\/]') + for ($index = 0; $index -lt ($hierarchy.Length - 1); $index++) { + $subdirectory = $hierarchy[$index] + if (-not $pointer[$subdirectory]) { + $pointer[$subdirectory] = [Ordered]@{} + } + $pointer = $pointer[$subdirectory] + } + $pointer[$hierarchy[-1]] = $fileInfo + } + } + + if (-not $fileTree.Count) { + continue + } + @( + $depth = 0 + $fileTree | toIndex + ) -join [Environment]::NewLine | + Add-Member NoteProperty Package $in -Force -PassThru | + Add-Member NoteProperty FilePattern $in -Force -PassThru | + . { + process { + $_.pstypenames.add('text/markdown') + $_ + } + } +} \ No newline at end of file From 85df5d183b1ca0d9fc46408fd9531d3555b9118d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 28 Mar 2026 23:22:35 +0000 Subject: [PATCH 547/724] feat: `OpenPackage.View.Tree.md` ( Fixes #177 ) --- OP.types.ps1xml | 116 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index fadd334..ebbf249 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -6226,6 +6226,122 @@ foreach ($in in $allInput) { + + Tree.md + + Tree.txt + Markdown.html From c2f94701d52ca627bf6674ba7ba90401fdd73bd4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 18:51:42 -0700 Subject: [PATCH 558/724] feat: `Publish-OpenPackage` ( Fixes #172 ) --- Commands/Publish-OpenPackage.ps1 | 143 +++++++++++++++++++++++++++++++ OP.psd1 | 5 +- 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 Commands/Publish-OpenPackage.ps1 diff --git a/Commands/Publish-OpenPackage.ps1 b/Commands/Publish-OpenPackage.ps1 new file mode 100644 index 0000000..d699d80 --- /dev/null +++ b/Commands/Publish-OpenPackage.ps1 @@ -0,0 +1,143 @@ +function Publish-OpenPackage { + <# + .SYNOPSIS + Publishes Open Package + .DESCRIPTION + Publishes Open Packages using any `OpenPackage.Publisher` or command. + #> + [CmdletBinding(PositionalBinding=$false,SupportsShouldProcess)] + [Alias('Publish-OP', 'pbop', 'pbOpenPackage')] + param( + # The name of the Publisher, or a command or script block used to Publish. + [Parameter(Mandatory,Position=0)] + [ArgumentCompleter({ + param ( $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters ) + $typeData = Get-TypeData -TypeName OpenPackage.Publisher + + if (-not $wordToComplete) { + $typeData.Members.Keys + } else { + $typeData.Members.Keys -match ([Regex]::Escape($wordTocomplete)) + } + })] + [PSObject] + $Publisher, + + # The Any Positional arguments for the view + [Parameter(Position=1,ValueFromRemainingArguments)] + [Alias('Argument','Arguments','Args')] + [PSObject[]] + $ArgumentList, + + # Any input objects. + [Parameter(ValueFromPipeline)] + [Alias('Package')] + [PSObject[]] + $InputObject, + + # Any options to pass to the View + [Alias('Options','Parameter','Parameters')] + [Collections.IDictionary] + $Option = [Ordered]@{} + ) + + # Gather all input + $allInput = @($input) + + if (-not $allInput -and $InputObject) { + $allInput += $InputObject + } + + # Get the typedata that describes Publishers + $typeData = Get-TypeData -TypeName OpenPackage.Publisher + + # If the Publisher is a string + if ($Publisher -is [string]) { + # check for a Publisher with that name + if ($typeData.Members[$Publisher].Script) { + # if one is found, call it with the package and option + $Publisher = $typeData.Members[$Publisher].Script + } else { + # Othewise, try to find a Publisher command + $PublisherCommand = + $ExecutionContext.SessionState.InvokeCommand.GetCommand( + $Publisher,'Cmdlet,Alias,Function' + ) + # If we found one, try to use it + if ($PublisherCommand) { + $Publisher = $PublisherCommand + } else { + # Otherwise, warn that Publisher is unknown + Write-Warning "Unknown Publisher $Publisher" + # and break out of the loop. + return + } + } + } + + if ($Publisher -isnot [ScriptBlock] -and + $Publisher -isnot [Management.Automation.CommandInfo] + ) { + Write-Error "Publisher must be a name of a OpenPackage.Publisher method, ScriptBlock, or Command" + return + } + + # Get our command metadata. + $commandMetaData = + if ($Publisher -is [ScriptBlock]) { + # If the Publisher is a scriptblock + # make a temporary function + $function:Publisher = $Publisher + # get its metadata + $ExecutionContext.SessionState.InvokeCommand.GetCommand('Publisher', 'Function') -as + [Management.Automation.CommandMetaData] + # and remove the temporary function + Remove-Item 'function:Publisher' + } else { + # otherwise, just cast to command metadata + $commandName = $Publisher.Name + Publisher -as [Management.Automation.CommandMetaData] + } + + # Once we have commandmetadata, we can find parameter names. + $validParameterNames = @( + $commandMetaData.Parameters.Keys + $commandMetaData.Parameters.Values.Aliases + ) + + # We want to be forgiving with input, so copy the options + $commandParameters = [Ordered]@{} + $Option + if ($WhatIfPreference) { $commandParameters.WhatIf = $true} + if ($PSBoundParameters['Confirm']) { $commandParameters.Confirm = $PSBoundParameters['Confirm'] } + # Check each key + foreach ($key in @($commandParameters.Keys)) { + # If we have valid parameter names + if ($validParameterNames -and + # and this isn't one of them, + $validParameterNames -notcontains $key + ) { + # write a warning + Write-Warning "Option $key not supported by $($commandName)" + # and remove the key. + $commandParameters.Remove($key) + } + } + + if ($allInput) { + if ($ArgumentList) { + $allInput | & $Publisher @ArgumentList @commandParameters + } else { + $allInput | & $Publisher @commandParameters + } + } else { + if ($ArgumentList) { + & $Publisher @ArgumentList @commandParameters + } else { + & $Publisher @commandParameters + } + } +} \ No newline at end of file diff --git a/OP.psd1 b/OP.psd1 index 1064bf7..c8c0de2 100644 --- a/OP.psd1 +++ b/OP.psd1 @@ -74,11 +74,12 @@ FunctionsToExport = @( 'Copy-OpenPackage' 'Export-OpenPackage' 'Format-OpenPackage' - 'Get-OpenPackage' + 'Get-OpenPackage' 'Install-OpenPackage' 'Join-OpenPackage' 'Lock-OpenPackage' 'New-OpenPackage' + 'Publish-OpenPackage' 'Read-OpenPackage' 'Remove-OpenPackage' 'Select-OpenPackage' @@ -120,6 +121,8 @@ AliasesToExport = @( 'Lock-OP', 'lkop','lkOpenPackage' 'New-OP','nop', 'nOpenPackage' + + 'Publish-OP', 'pbop', 'pbOpenPackage' 'Read-OP', 'rdop', 'rdOpenPackage' From 0121b87ca672a78585012c6bd19c645e8f67677e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 19:10:38 -0700 Subject: [PATCH 559/724] feat: `OpenPackage.Publisher.Site` ( Fixes #180 ) --- Types/OpenPackage.Publisher/Site.ps1 | 74 ++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 Types/OpenPackage.Publisher/Site.ps1 diff --git a/Types/OpenPackage.Publisher/Site.ps1 b/Types/OpenPackage.Publisher/Site.ps1 new file mode 100644 index 0000000..83278b2 --- /dev/null +++ b/Types/OpenPackage.Publisher/Site.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + Publishes a static site +.DESCRIPTION + Publishes a package as a static site. + + This installs the package to the -DestinationPath (default `./_site`) + + Then, this formats all markdown in the package, + and exports each markdown file into an index.html +#> +[CmdletBinding(PositionalBinding=$false,SupportsShouldProcess)] +param( +# The destination path of the website. +[Parameter(Position=0)] +[string] +$DestinationPath = './_site', + +# Any input object. This should be one or more packages. +# If no input is provided, will archive the current directory and publish a page. +[Parameter(ValueFromPipeline)] +[Alias('Package')] +[PSObject[]] +$InputObject +) + +# Quickly enumerate all input and arguments. +$allInput = @($input) +if (-not $allInput -and $InputObject) { + $allInput += $InputObject +} + +# If there is no input +if (-not $allInput) { + # exclude our destination from the input + $excludeWildCard = $DestinationPath -replace './', '*' -replace '$', '*' + # and make a package from the current directory. + $allInput = @(Get-OpenPackage -FilePath . -Exclude $excludeWildCard) +} + +# Still no input? Return. +if (-not $allInput) { return } + +# To build a static site out of a package, we just need to install it +$installParameters = [Ordered]@{ + DestinationPath = $DestinationPath + Force = $true + WarningAction = 'SilentlyContinue' + WarningVariable = 'warnings' +} + +# All of the content from that site is the base layer +$allInput | + Install-OpenPackage @installParameters -PassThru -Force + +# Next up, we can take any markdown in the package and make it an index.html +$allInput | + # First up, format it as markdown. + # This will apply styles and frame the content. + Format-OpenPackage -View Markdown.html | + Foreach-Object { + $md = $_ + $markdownHtmlPath = + if ($md.PartUri -match '/index\.(?>md|markdown)$') { + Join-Path $DestinationPath ( + $md.PartUri -replace '\.(?>md|markdown)$', '.html' + ) + } else { + Join-Path $DestinationPath ( + $md.PartUri -replace '\.(?>md|markdown)$', '/index.html' + ) + } + New-Item -ItemType File -Value "$md" -Path $markdownHtmlPath -Force + } \ No newline at end of file From 112ca0b8ef682db773423dd6cae8e63f576edafb Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 31 Mar 2026 02:10:56 +0000 Subject: [PATCH 560/724] feat: `OpenPackage.Publisher.Site` ( Fixes #180 ) --- OP.types.ps1xml | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index a07523d..39ef60e 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4193,6 +4193,90 @@ Reader
    + + OpenPackage.Publisher + + + Site + + + + OpenPackage.Source From d0457147451f2726f77b59ca499fa3e673986342 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 19:16:31 -0700 Subject: [PATCH 561/724] feat: Publishing draft in workflow ( re #155, re #172, re #180 ) --- .github/workflows/BuildOP.yml | 5 +++++ Build/GitHub/Jobs/BuildOP.psd1 | 12 ++++++++++-- OP.op.ps1 | 6 ++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/BuildOP.yml b/.github/workflows/BuildOP.yml index af86672..723abad 100644 --- a/.github/workflows/BuildOP.yml +++ b/.github/workflows/BuildOP.yml @@ -505,6 +505,11 @@ jobs: with: name: op-artifact path: OP.zip + - uses: actions/upload-artifact@main + id: artifact-upload-step + with: + name: op-site + path: _site env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} diff --git a/Build/GitHub/Jobs/BuildOP.psd1 b/Build/GitHub/Jobs/BuildOP.psd1 index a269ee6..f981c15 100644 --- a/Build/GitHub/Jobs/BuildOP.psd1 +++ b/Build/GitHub/Jobs/BuildOP.psd1 @@ -5,14 +5,14 @@ @{ name = 'Check out repository' uses = 'actions/checkout@main' - }, + } 'RunEZOut' @{ name = 'Run Action (on branch)' if = '${{github.ref_name != ''main''}}' uses = './' id = 'OPAction' - }, + } @{ uses = 'actions/upload-artifact@main' 'id' = 'artifact-upload-step' @@ -21,5 +21,13 @@ path = 'OP.zip' } } + @{ + uses = 'actions/upload-artifact@main' + 'id' = 'artifact-upload-step' + 'with' = @{ + name = 'op-site' + path = '_site' + } + } ) } \ No newline at end of file diff --git a/OP.op.ps1 b/OP.op.ps1 index 04b7697..92d0883 100644 --- a/OP.op.ps1 +++ b/OP.op.ps1 @@ -1,3 +1,9 @@ $imported = Import-Module ./OP.psd1 -Force -PassThru + $op = $imported | Get-OpenPackage + +$op.Palette = 'https://cdn.jsdelivr.net/gh/2bitdesigns/4bitcss@latest/css/Konsolas.css +' +$publishedFiles = $op | Publish-OpenPackage -Publisher Site + $op | Export-OpenPackage -DestinationPath "$($imported.name).zip" From 1d3786bfc531b81b1117835d231a1f2bfee1c14e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 19:21:01 -0700 Subject: [PATCH 562/724] feat: Publishing draft in workflow ( re #155, re #172, re #180 ) Fixing upload step --- .github/workflows/BuildOP.yml | 11 ++++------- Build/GitHub/Jobs/BuildOP.psd1 | 15 +++++---------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/.github/workflows/BuildOP.yml b/.github/workflows/BuildOP.yml index 723abad..1a780f1 100644 --- a/.github/workflows/BuildOP.yml +++ b/.github/workflows/BuildOP.yml @@ -503,13 +503,10 @@ jobs: - uses: actions/upload-artifact@main id: artifact-upload-step with: - name: op-artifact - path: OP.zip - - uses: actions/upload-artifact@main - id: artifact-upload-step - with: - name: op-site - path: _site + name: op-test + path: | + OP.zip + _site env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} diff --git a/Build/GitHub/Jobs/BuildOP.psd1 b/Build/GitHub/Jobs/BuildOP.psd1 index f981c15..1907326 100644 --- a/Build/GitHub/Jobs/BuildOP.psd1 +++ b/Build/GitHub/Jobs/BuildOP.psd1 @@ -17,16 +17,11 @@ uses = 'actions/upload-artifact@main' 'id' = 'artifact-upload-step' 'with' = @{ - name = 'op-artifact' - path = 'OP.zip' - } - } - @{ - uses = 'actions/upload-artifact@main' - 'id' = 'artifact-upload-step' - 'with' = @{ - name = 'op-site' - path = '_site' + name = 'op-test' + path = @' +OP.zip +_site +'@ } } ) From 24df6d3154d964a0f7436cc24b18a24aae515fce Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 19:50:40 -0700 Subject: [PATCH 563/724] docs: `OpenPackage.ContentTypeMap.README` --- Types/OpenPackage.ContentTypeMap/README.md | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 Types/OpenPackage.ContentTypeMap/README.md diff --git a/Types/OpenPackage.ContentTypeMap/README.md b/Types/OpenPackage.ContentTypeMap/README.md new file mode 100644 index 0000000..15a0e5b --- /dev/null +++ b/Types/OpenPackage.ContentTypeMap/README.md @@ -0,0 +1,43 @@ +## OpenPackage.ContentTypeMap + +Packages can contain anything, but they do not always use consistent content types. + +In order to properly classify content, we need to map any file name into the right content type. + +While there is a wonderfully long list of content types at the +[IANA](https://www.iana.org/assignments/media-types/media-types.xhtml), it does not contain any file extensions. + +[MDN has a list of common types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types). + +Sadly, this is far from complete. + +So, in order to make it easy to work with any type of content, OP contains this pseudo type, which gives us a customizable type map. + +## How This Works + +Various package commands will accept a typemap parameter, and default it to this object. + +Those commands will then use this typemap whenever they need to determine the content type of a file. + +For `Get-OpenPackage`, this will inform the content type used to pack the file. + +For `Start-OpenPackage`, this will inform the content type used to serve the part content. + +### OpenPackage.ContentTypeMap.DefaultTypeMap + +The Default Type Map is a PowerShell data file containing a mapping of known extensions to content types. + +### OpenPackage.ContentTypeMap.get_TypeMap + +When first requested, this creates a copy of the DefaultTypeMap. + +Subsequent requests will return this copy. + +### OpenPackage.ContentTypeMap.set_TypeMap + +When provided a dictionary mapping extension to content type, will change the content type map. + + + + + From 6aff9085dad7f14c5ebceb84b3cb8b7b154e70d6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 31 Mar 2026 02:51:25 +0000 Subject: [PATCH 564/724] docs: `OpenPackage.ContentTypeMap.README` --- OP.types.ps1xml | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 39ef60e..34f6420 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2219,6 +2219,53 @@ if ($TypeMap.Count -eq 0) { DefaultDisplay TypeMap + + README + ## OpenPackage.ContentTypeMap + +Packages can contain anything, but they do not always use consistent content types. + +In order to properly classify content, we need to map any file name into the right content type. + +While there is a wonderfully long list of content types at the +[IANA](https://www.iana.org/assignments/media-types/media-types.xhtml), it does not contain any file extensions. + +[MDN has a list of common types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types). + +Sadly, this is far from complete. + +So, in order to make it easy to work with any type of content, OP contains this pseudo type, which gives us a customizable type map. + +## How This Works + +Various package commands will accept a typemap parameter, and default it to this object. + +Those commands will then use this typemap whenever they need to determine the content type of a file. + +For `Get-OpenPackage`, this will inform the content type used to pack the file. + +For `Start-OpenPackage`, this will inform the content type used to serve the part content. + +### OpenPackage.ContentTypeMap.DefaultTypeMap + +The Default Type Map is a PowerShell data file containing a mapping of known extensions to content types. + +### OpenPackage.ContentTypeMap.get_TypeMap + +When first requested, this creates a copy of the DefaultTypeMap. + +Subsequent requests will return this copy. + +### OpenPackage.ContentTypeMap.set_TypeMap + +When provided a dictionary mapping extension to content type, will change the content type map. + + + + + + + From ce2bf0d6a6fdd5c046cae68a86c2043a26bd2c19 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 20:00:17 -0700 Subject: [PATCH 565/724] feat: `OpenPackage.Publisher.Site` ( Fixes #180 ) Treating READMEs differently, checking if index.html already exists --- Types/OpenPackage.Publisher/Site.ps1 | 35 +++++++++++++++++++--------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/Types/OpenPackage.Publisher/Site.ps1 b/Types/OpenPackage.Publisher/Site.ps1 index 83278b2..f6e3dbb 100644 --- a/Types/OpenPackage.Publisher/Site.ps1 +++ b/Types/OpenPackage.Publisher/Site.ps1 @@ -3,11 +3,15 @@ Publishes a static site .DESCRIPTION Publishes a package as a static site. - +.NOTES This installs the package to the -DestinationPath (default `./_site`) Then, this formats all markdown in the package, and exports each markdown file into an index.html + (if the file did not already exist). + + If the markdown file was named README, + will try to place an index in the same directory. #> [CmdletBinding(PositionalBinding=$false,SupportsShouldProcess)] param( @@ -61,14 +65,23 @@ $allInput | Foreach-Object { $md = $_ $markdownHtmlPath = - if ($md.PartUri -match '/index\.(?>md|markdown)$') { - Join-Path $DestinationPath ( - $md.PartUri -replace '\.(?>md|markdown)$', '.html' - ) - } else { - Join-Path $DestinationPath ( - $md.PartUri -replace '\.(?>md|markdown)$', '/index.html' - ) - } - New-Item -ItemType File -Value "$md" -Path $markdownHtmlPath -Force + if ($md.PartUri -match '/index\.(?>md|markdown)$') { + Join-Path $DestinationPath ( + $md.PartUri -replace '\.(?>md|markdown)$', '.html' + ) + } + elseif ($md.PartUri -match '/README\.(?>md|markdown)$') { + Join-Path $DestinationPath ( + $md.PartUri -replace '/README\.(?>md|markdown)$', '/index.html' + ) + } + else { + Join-Path $DestinationPath ( + $md.PartUri -replace '\.(?>md|markdown)$', '/index.html' + ) + } + + if (-not (Test-Path $markdownHtmlPath)) { + New-Item -ItemType File -Value "$md" -Path $markdownHtmlPath -Force + } } \ No newline at end of file From 2854302fbe8b5faec44fc90cddde759e9a8e060b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 31 Mar 2026 03:00:34 +0000 Subject: [PATCH 566/724] feat: `OpenPackage.Publisher.Site` ( Fixes #180 ) Treating READMEs differently, checking if index.html already exists --- OP.types.ps1xml | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 34f6420..200d5b3 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4251,11 +4251,15 @@ Reader Publishes a static site .DESCRIPTION Publishes a package as a static site. - +.NOTES This installs the package to the -DestinationPath (default `./_site`) Then, this formats all markdown in the package, and exports each markdown file into an index.html + (if the file did not already exist). + + If the markdown file was named README, + will try to place an index in the same directory. #> [CmdletBinding(PositionalBinding=$false,SupportsShouldProcess)] param( @@ -4309,16 +4313,25 @@ $allInput | Foreach-Object { $md = $_ $markdownHtmlPath = - if ($md.PartUri -match '/index\.(?>md|markdown)$') { - Join-Path $DestinationPath ( - $md.PartUri -replace '\.(?>md|markdown)$', '.html' - ) - } else { - Join-Path $DestinationPath ( - $md.PartUri -replace '\.(?>md|markdown)$', '/index.html' - ) - } - New-Item -ItemType File -Value "$md" -Path $markdownHtmlPath -Force + if ($md.PartUri -match '/index\.(?>md|markdown)$') { + Join-Path $DestinationPath ( + $md.PartUri -replace '\.(?>md|markdown)$', '.html' + ) + } + elseif ($md.PartUri -match '/README\.(?>md|markdown)$') { + Join-Path $DestinationPath ( + $md.PartUri -replace '/README\.(?>md|markdown)$', '/index.html' + ) + } + else { + Join-Path $DestinationPath ( + $md.PartUri -replace '\.(?>md|markdown)$', '/index.html' + ) + } + + if (-not (Test-Path $markdownHtmlPath)) { + New-Item -ItemType File -Value "$md" -Path $markdownHtmlPath -Force + } } From c5082700e07e3ec73d0a0da226376e881f977192 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 20:10:02 -0700 Subject: [PATCH 567/724] docs: `OpenPackage.Publisher.README` --- Types/OpenPackage.Publisher/README.md | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Types/OpenPackage.Publisher/README.md diff --git a/Types/OpenPackage.Publisher/README.md b/Types/OpenPackage.Publisher/README.md new file mode 100644 index 0000000..f761acc --- /dev/null +++ b/Types/OpenPackage.Publisher/README.md @@ -0,0 +1,35 @@ +# Open Pubishing with Open Packages + +Packages are just a bunch of files. + +On a technical level, publishing is just publishing some files someplace (often in the some way). + +There are many things we can publish, and many places we can publish them. + +`Publish-OpenPackage` is used to publish open packages. + +It accepts a publisher, any arguments, any options, and any input. + +If the publisher is a command or a script block, it will be called directly. + +If the publisher is the name of one of the methods in the type data of `OpenPackage.Publisher`, + +that method will be called. + +Any invalid named parameters will be removed prior to calling the publisher. + + +## OpenPackage.Publisher + +`OpenPackage.Publisher` is contains the built-in publishers. + +More will be added with time. + +### OpenPackage.Publisher.Site + +`OpenPackage.Publisher.Site` publishes packages as a static site. + +This will install a package to a `-DestionationPath` (default `./_site`). + +It will also generate an index.html for each markdown file in the package, if one does not already exist. + From 9b63e3d60119f22bfcb38318b29bfbc2a3ad3b8b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 31 Mar 2026 03:10:23 +0000 Subject: [PATCH 568/724] docs: `OpenPackage.Publisher.README` --- OP.types.ps1xml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 200d5b3..6c2cdaf 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4335,6 +4335,45 @@ $allInput | } + + README + # Open Pubishing with Open Packages + +Packages are just a bunch of files. + +On a technical level, publishing is just publishing some files someplace (often in the some way). + +There are many things we can publish, and many places we can publish them. + +`Publish-OpenPackage` is used to publish open packages. + +It accepts a publisher, any arguments, any options, and any input. + +If the publisher is a command or a script block, it will be called directly. + +If the publisher is the name of one of the methods in the type data of `OpenPackage.Publisher`, + +that method will be called. + +Any invalid named parameters will be removed prior to calling the publisher. + + +## OpenPackage.Publisher + +`OpenPackage.Publisher` is contains the built-in publishers. + +More will be added with time. + +### OpenPackage.Publisher.Site + +`OpenPackage.Publisher.Site` publishes packages as a static site. + +This will install a package to a `-DestionationPath` (default `./_site`). + +It will also generate an index.html for each markdown file in the package, if one does not already exist. + + + From 6be3ca36aab26b8427e89a47c80880c812dc3727 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 20:10:57 -0700 Subject: [PATCH 569/724] docs: `OpenPackage.Publisher.README` --- Types/OpenPackage.Publisher/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Types/OpenPackage.Publisher/README.md b/Types/OpenPackage.Publisher/README.md index f761acc..293ce4f 100644 --- a/Types/OpenPackage.Publisher/README.md +++ b/Types/OpenPackage.Publisher/README.md @@ -29,7 +29,7 @@ More will be added with time. `OpenPackage.Publisher.Site` publishes packages as a static site. -This will install a package to a `-DestionationPath` (default `./_site`). +This will install a package to a `-DestinationPath` (default `./_site`). It will also generate an index.html for each markdown file in the package, if one does not already exist. From 7d352ff29943a4502e2fe90125303867d0fa3947 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 20:23:20 -0700 Subject: [PATCH 570/724] feat: `OpenPackage.README.md` ( Fixes #81 ) Renaming property to include extension (and open up space for a README.md) --- Types/OpenPackage/{get_README.ps1 => get_README.md.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/OpenPackage/{get_README.ps1 => get_README.md.ps1} (100%) diff --git a/Types/OpenPackage/get_README.ps1 b/Types/OpenPackage/get_README.md.ps1 similarity index 100% rename from Types/OpenPackage/get_README.ps1 rename to Types/OpenPackage/get_README.md.ps1 From 4e680a14ac0081b621cb06278938ba5bda5cd750 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 31 Mar 2026 03:23:43 +0000 Subject: [PATCH 571/724] feat: `OpenPackage.README.md` ( Fixes #81 ) Renaming property to include extension (and open up space for a README.md) --- OP.types.ps1xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 6c2cdaf..9cae260 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1697,7 +1697,7 @@ $this.GetContent( - README + README.md <# .SYNOPSIS @@ -4368,7 +4368,7 @@ More will be added with time. `OpenPackage.Publisher.Site` publishes packages as a static site. -This will install a package to a `-DestionationPath` (default `./_site`). +This will install a package to a `-DestinationPath` (default `./_site`). It will also generate an index.html for each markdown file in the package, if one does not already exist. From 14341b62469266b1873e4870ddbc88b572c964db Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 20:34:07 -0700 Subject: [PATCH 572/724] docs: `OpenPackage.README` --- Types/OpenPackage/README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Types/OpenPackage/README.md diff --git a/Types/OpenPackage/README.md b/Types/OpenPackage/README.md new file mode 100644 index 0000000..07ef583 --- /dev/null +++ b/Types/OpenPackage/README.md @@ -0,0 +1,28 @@ +# Open Package + +Anything can become a package. + +## Open Packaging Conventions + +The [Open Package Conventions](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) are an Open Protocol for Open Packages. + +They were first published in 2006, in [ECMA-376](https://ecma-international.org/publications-and-standards/standards/ecma-376/) + +Like many file formats, Open Packages are zip files in a trenchcoat. + +In PowerShell we can work with Open Packages using .NET type [`System.IO.Packaging.Package`](https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties?view=windowsdesktop-10.0&wt.mc_id=MVP_321542) + +In PowerShell, we can also extend type data using either Update-TypeData or a .types.ps1xml file. + +This is how OP works. + +It extends what you can do with OpenPackages in PowerShell, uses Open Protocols to read and write to packages. + +There are _quite_ a lot of things you can do with an Open Package, and we will add more with time. + +To explore what you can do with an open package, use the PowerShell command `Get-Member`: + +~~~PowerShell +# Get an empty package and get its methods and properties. +Get-OpenPackage | Get-Member +~~~ \ No newline at end of file From 240807a57b5955ffdb028fbb21967ddca6b87a49 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 31 Mar 2026 03:34:27 +0000 Subject: [PATCH 573/724] docs: `OpenPackage.README` --- OP.types.ps1xml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 9cae260..618bad5 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2035,6 +2035,37 @@ foreach ($part in $this.GetParts()) { Identifier FileList + + README + # Open Package + +Anything can become a package. + +## Open Packaging Conventions + +The [Open Package Conventions](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) are an Open Protocol for Open Packages. + +They were first published in 2006, in [ECMA-376](https://ecma-international.org/publications-and-standards/standards/ecma-376/) + +Like many file formats, Open Packages are zip files in a trenchcoat. + +In PowerShell we can work with Open Packages using .NET type [`System.IO.Packaging.Package`](https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging.packageproperties?view=windowsdesktop-10.0&wt.mc_id=MVP_321542) + +In PowerShell, we can also extend type data using either Update-TypeData or a .types.ps1xml file. + +This is how OP works. + +It extends what you can do with OpenPackages in PowerShell, uses Open Protocols to read and write to packages. + +There are _quite_ a lot of things you can do with an Open Package, and we will add more with time. + +To explore what you can do with an open package, use the PowerShell command `Get-Member`: + +~~~PowerShell +# Get an empty package and get its methods and properties. +Get-OpenPackage | Get-Member +~~~ + From a458bb4ff71d03afa93bff4cb94eb34b0108a433 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 20:51:49 -0700 Subject: [PATCH 574/724] feat: `OpenPackage.get_Creator` ( Fixes #19 ) Automatically setting from metadata --- Types/OpenPackage/get_Creator.ps1 | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage/get_Creator.ps1 b/Types/OpenPackage/get_Creator.ps1 index abfc739..ea596ce 100644 --- a/Types/OpenPackage/get_Creator.ps1 +++ b/Types/OpenPackage/get_Creator.ps1 @@ -8,4 +8,31 @@ #> param() -$this.PackageProperties.Creator \ No newline at end of file +if ($this.PackageProperties.Creator) { + return $this.PackageProperties.Creator +} + +$moduleManifest = @($this.PowerShellManifest)[0] +if ($moduleManifest) { + $this.PackageProperties.Creator = $moduleManifest.Author + return $this.PackageProperties.Creator +} + +$packageJson = @($this.'Package.json')[0] +if ($packageJson -and $packageJson.author) { + $this.PackageProperties.Creator = + if ($packageJson.author -is [string]) { + $packageJson.author + } else { + $packageJson.author | ConvertTo-Json -Depth 3 + } + return $this.PackageProperties.Creator +} + +$nuSpec = @($this.nuSpec)[0] +if ($nuSpec -and $nuSpec.package.metadata.authors) { + $this.PackageProperties.Creator = + $nuSpec.package.metadata.authors + + return $this.PackageProperties.Creator +} \ No newline at end of file From 2e1adcbd3cd7bd5e16edaad35a3e5d266dd61349 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 31 Mar 2026 03:52:09 +0000 Subject: [PATCH 575/724] feat: `OpenPackage.get_Creator` ( Fixes #19 ) Automatically setting from metadata --- OP.types.ps1xml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 618bad5..83c0f79 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -769,7 +769,34 @@ $this.PackageProperties.Created = $Created #> param() -$this.PackageProperties.Creator +if ($this.PackageProperties.Creator) { + return $this.PackageProperties.Creator +} + +$moduleManifest = @($this.PowerShellManifest)[0] +if ($moduleManifest) { + $this.PackageProperties.Creator = $moduleManifest.Author + return $this.PackageProperties.Creator +} + +$packageJson = @($this.'Package.json')[0] +if ($packageJson -and $packageJson.author) { + $this.PackageProperties.Creator = + if ($packageJson.author -is [string]) { + $packageJson.author + } else { + $packageJson.author | ConvertTo-Json -Depth 3 + } + return $this.PackageProperties.Creator +} + +$nuSpec = @($this.nuSpec)[0] +if ($nuSpec -and $nuSpec.package.metadata.authors) { + $this.PackageProperties.Creator = + $nuSpec.package.metadata.authors + + return $this.PackageProperties.Creator +} <# From 41fcb7e60bc1ab2e1fa094f6dfd723424a36dc59 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 21:41:02 -0700 Subject: [PATCH 576/724] docs: `OpenPackage.Source.README` --- Types/OpenPackage.Source/README.md | 116 +++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 Types/OpenPackage.Source/README.md diff --git a/Types/OpenPackage.Source/README.md b/Types/OpenPackage.Source/README.md new file mode 100644 index 0000000..94061c0 --- /dev/null +++ b/Types/OpenPackage.Source/README.md @@ -0,0 +1,116 @@ +# Open Package Sources + +Anything can become a package. + +However, different things become packages in different ways. + +`OpenPackage.Source` describes the supported open package sources. + +You should be able to use any `OpenPackage.Source` script to get packages, or data that can be put into a package. + +Current sources: + +## OpenPackage.Source + +OpenPackage.Source is the pseudo type that represents sources. + +Each ScriptMethod in the type can be used directly, and called from it's file, located in `./Types/OpenPackage.Source` + +`OpenPackage.Source` methods are most commonly called thru Get-OpenPackage (it is getting packages from a source) + +OpenPackage sources may call Get-OpenPackage recursively. + +If they do, they should include any parameters to `Get-OpenPackage` that are in all parameter sets. + +### At + +At enables `@` syntax, and supports getting content either from At Protocol or from any domain. + +For example: + +~~~PowerShell +op @( + '@MrPowerShell.com/site.standard.document' + '@MrPowerShell.com/site.standard.publication' + '@MrPowerShell.com/MrPowerShell.png' + '@MrPowerShell.com/MrPowerShell.svg' +) +~~~ + +This will: + +* Get [standard site documents](https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=mrpowershell.com&collection=site.standard.document&limit=100) from at protocol +* Get [standard site publications](https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=mrpowershell.com&collection=site.standard.publication&limit=100) from at protocol * Get [a png logo](https://MrPowerShell.com/MrPowerShell.png) from [MrPowerShell.com](https://MrPowerShell.com) +* Get [a svg logo](https://MrPowerShell.com/MrPowerShell.svg) from [MrPowerShell.com](https://MrPowerShell.com) + +If no domain is specified, at syntax will default to looking for a user or organization on GitHub.com. + +### AtBlob + +AtBlob is used to retreive public blobs from At Protocol. It does not store the blob in a package. + +### AtProtocol + +`AtProtocol` creates packages from At Protocol. It calls either AtBlob, AtRecord, or AtType and stores them in a package. + +### AtRecord + +`AtRecord` gets a single At Protocol record. It does not store the record in a package. + +### AtType + +`AtType` gets at records of a given type. It does not store the records in a package. + +### Dictionary + +`Dictionary` creates a package from a dictionary. + +Each key is a file or directory name. + +Each value can be file content or a dictionary. + +## Directory + +`Directory` creates a package from a directory + +Each file will be copied into the package. + +## Nuget + +`Nuget` gets a package from a Nuget repository. + +Since Nuget packages are already open packages, this directly returns the content. + +## Repository + +`Repository` gets a package from a git repository. + +This will attempt to clone this repository, and supports sparse filtering of the repository contents. + +Once the repository is cloned, it will be read in as a `Directory`. + +## Tar + +`tar` gets a package from a `.tar` or `.tar.gz` file. + +This requires either the tar application or the `System.Formats.Tar` assembly. + +## Url + +`url` gets a package from one or more urls. + +Any url is acceptable. + +Some urls may be processed by another source. + +For example: + +* Github urls will be processed with `Repository` +* `at://` urls will be processed with `AtProtocol` +* [Nuget](https://nuget.org/), [PowerShell Gallery](https://PowerShellGallery.com), and [Chocolatey](https://community.chocolatey.org/) packages will processed with `nuget` + +## Zip + +`zip` gets packages from a zip file. + +If the file is not already an open package, extracts the zip and creates an open package from the zip file. \ No newline at end of file From b3f880b787a221b87adbefd34d3e30bd5d81b1f5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 31 Mar 2026 04:41:18 +0000 Subject: [PATCH 577/724] docs: `OpenPackage.Source.README` --- OP.types.ps1xml | 119 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 83c0f79..101fc5e 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -6296,6 +6296,125 @@ foreach ($resolvedItem in Get-Item -Path $ZipFile) { + + README + # Open Package Sources + +Anything can become a package. + +However, different things become packages in different ways. + +`OpenPackage.Source` describes the supported open package sources. + +You should be able to use any `OpenPackage.Source` script to get packages, or data that can be put into a package. + +Current sources: + +## OpenPackage.Source + +OpenPackage.Source is the pseudo type that represents sources. + +Each ScriptMethod in the type can be used directly, and called from it's file, located in `./Types/OpenPackage.Source` + +`OpenPackage.Source` methods are most commonly called thru Get-OpenPackage (it is getting packages from a source) + +OpenPackage sources may call Get-OpenPackage recursively. + +If they do, they should include any parameters to `Get-OpenPackage` that are in all parameter sets. + +### At + +At enables `@` syntax, and supports getting content either from At Protocol or from any domain. + +For example: + +~~~PowerShell +op @( + '@MrPowerShell.com/site.standard.document' + '@MrPowerShell.com/site.standard.publication' + '@MrPowerShell.com/MrPowerShell.png' + '@MrPowerShell.com/MrPowerShell.svg' +) +~~~ + +This will: + +* Get [standard site documents](https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=mrpowershell.com&collection=site.standard.document&limit=100) from at protocol +* Get [standard site publications](https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=mrpowershell.com&collection=site.standard.publication&limit=100) from at protocol * Get [a png logo](https://MrPowerShell.com/MrPowerShell.png) from [MrPowerShell.com](https://MrPowerShell.com) +* Get [a svg logo](https://MrPowerShell.com/MrPowerShell.svg) from [MrPowerShell.com](https://MrPowerShell.com) + +If no domain is specified, at syntax will default to looking for a user or organization on GitHub.com. + +### AtBlob + +AtBlob is used to retreive public blobs from At Protocol. It does not store the blob in a package. + +### AtProtocol + +`AtProtocol` creates packages from At Protocol. It calls either AtBlob, AtRecord, or AtType and stores them in a package. + +### AtRecord + +`AtRecord` gets a single At Protocol record. It does not store the record in a package. + +### AtType + +`AtType` gets at records of a given type. It does not store the records in a package. + +### Dictionary + +`Dictionary` creates a package from a dictionary. + +Each key is a file or directory name. + +Each value can be file content or a dictionary. + +## Directory + +`Directory` creates a package from a directory + +Each file will be copied into the package. + +## Nuget + +`Nuget` gets a package from a Nuget repository. + +Since Nuget packages are already open packages, this directly returns the content. + +## Repository + +`Repository` gets a package from a git repository. + +This will attempt to clone this repository, and supports sparse filtering of the repository contents. + +Once the repository is cloned, it will be read in as a `Directory`. + +## Tar + +`tar` gets a package from a `.tar` or `.tar.gz` file. + +This requires either the tar application or the `System.Formats.Tar` assembly. + +## Url + +`url` gets a package from one or more urls. + +Any url is acceptable. + +Some urls may be processed by another source. + +For example: + +* Github urls will be processed with `Repository` +* `at://` urls will be processed with `AtProtocol` +* [Nuget](https://nuget.org/), [PowerShell Gallery](https://PowerShellGallery.com), and [Chocolatey](https://community.chocolatey.org/) packages will processed with `nuget` + +## Zip + +`zip` gets packages from a zip file. + +If the file is not already an open package, extracts the zip and creates an open package from the zip file. + From cbf892fa3a43f311c33e393fd61c32c23bf77ee6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 30 Mar 2026 22:43:32 -0700 Subject: [PATCH 578/724] feat: `OpenPackage.View.org.poshweb.op` ( Fixes #179 ) --- Types/OpenPackage.View/org.poshweb.op.ps1 | 111 ++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 Types/OpenPackage.View/org.poshweb.op.ps1 diff --git a/Types/OpenPackage.View/org.poshweb.op.ps1 b/Types/OpenPackage.View/org.poshweb.op.ps1 new file mode 100644 index 0000000..f79b65a --- /dev/null +++ b/Types/OpenPackage.View/org.poshweb.op.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS + Views a package as an `org.poshweb.op` +.DESCRIPTION + Views a package as an `org.poshweb.op`. + + This provides some summary metadata about an open package, + and contains basic information about files in the package. +.INPUTS + System.IO.Packaging.Package +#> +[Reflection.AssemblyMetadata('schema', @' +{ + "$schema": "https://json-schema.org/schema", + "$id": "https://op.poshweb.org/schemas/org.poshweb.op", + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "The package id" + }, + "creator": { + "type": "string", + "description": "The package creator" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "files": { + "type": "array", + "items": { + "type": "object", + "required": ["algorithm", "contentType", "hash", "path"], + "properties": { + "algorithm": { + "type": "string", + "description": "The hash algorithm" + }, + "hash": { + "type": "string", + "description": "The hash value" + }, + "contentType": { + "type": "string", + "description": "The content type" + }, + "path": { + "type": "string", + "description": "The path of the hashed content" + } + } + } + }, + "version": { + "type": "string", + "description": "The package version" + }, + "url": { + "type": "string", + "format": "url", + "description": "The package url" + } + } +} +'@ +)] +param() + +$allInputAndArgs = @($input) + @($args) + +$mySchema = foreach ($attribute in $MyInvocation.MyCommand.ScriptBlock.Attributes) { + if ($attribute.Key -in 'schema', 'jsonschema','json-schema') { + $attribute.Value | ConvertFrom-Json + break + } +} + +foreach ($in in $allInputAndArgs) { + if ($in -isnot [IO.Packaging.Package]) { + continue + } + + [PSCustomObject]@{ + PSTypeName = 'org.poshweb.op' + '$schema' = "$($mySchema.'$schema')" + '$id' = "$($mySchema.'$id')" + '$type' = 'org.poshweb.op' + id = "$($in.Identifier)" + description = "$($in.description)" + version = "$($in.Version)" + files = @( + foreach ($part in $in.GetParts()) { + $part.GetHash() | + Select-Object @{ + name='path';expression={$_.path} + }, @{ + name='contentType';Expression={$part.ContentType} + }, @{ + name='hash';expression={$_.hash.ToLower()} + }, @{ + name='algorithm';expression={$_.algorithm.ToLower()} + } + } + ) + tags = @($in.Keywords -split '\s+') + } +} \ No newline at end of file From 3bc7c6d1e6cba9061eb62f70e66716e61dfa5c12 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 31 Mar 2026 05:43:59 +0000 Subject: [PATCH 579/724] feat: `OpenPackage.View.org.poshweb.op` ( Fixes #179 ) --- OP.types.ps1xml | 116 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 101fc5e..6ba1422 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -6663,6 +6663,122 @@ filter text/html { + + org.poshweb.op + + Tree.html + Site + + + org.poshweb.op + + + Eleventy + + org.poshweb.op + + Markdown + + org.poshweb.op From 272abcab7d4ee740d89b6bbae6943c8a0d2e02af Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Thu, 2 Apr 2026 17:19:14 -0700 Subject: [PATCH 597/724] feat: `OpenPackage.Source.Repository` ( Fixes #115 ) Using env:OpenPackagePath to store repositories --- Types/OpenPackage.Source/Repository.ps1 | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Source/Repository.ps1 b/Types/OpenPackage.Source/Repository.ps1 index 201a92b..3fce96b 100644 --- a/Types/OpenPackage.Source/Repository.ps1 +++ b/Types/OpenPackage.Source/Repository.ps1 @@ -91,7 +91,19 @@ if (-not $gitApp) { throw "No git" } -$myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage +if (-not $env:OpenPackagePath) { + throw "No Open Package Path" +} + + + +$myAppData = $env:OpenPackagePath -split $( + if ($IsLinux -or $IsMacOS) { + ':' + } else { + ';' + } +) $namedParameters = [Ordered]@{} @@ -129,8 +141,16 @@ if (-not $owner -or -not $repositoryName) { return } +$repoDir = if ( + ($Repository -as [uri]).DnsSafeHost +) { + ($Repository -as [uri]).DnsSafeHost +} else { + 'repos' +} + # Get the path to our repos -$reposPaths = Join-Path $myAppData 'repos' +$reposPaths = Join-Path $myAppData $repoDir if (-not (Test-Path $reposPaths)) { # (create it if it did not exist) $null = New-Item -ItemType Directory $reposPaths -Force From c976a10f1b02ba10abbea2a9434166110ca2fd16 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 3 Apr 2026 14:27:44 -0700 Subject: [PATCH 598/724] feat: `OpenPackage.Source.Npm` ( Fixes #186 ) --- Types/OpenPackage.Source/Npm.ps1 | 186 +++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 Types/OpenPackage.Source/Npm.ps1 diff --git a/Types/OpenPackage.Source/Npm.ps1 b/Types/OpenPackage.Source/Npm.ps1 new file mode 100644 index 0000000..fa21d53 --- /dev/null +++ b/Types/OpenPackage.Source/Npm.ps1 @@ -0,0 +1,186 @@ +<# +.SYNOPSIS + Gets a node package +.DESCRIPTION + Gets a node package as an open package +#> +param( +# A node package. +# This can be the name of the package in npm or a package link from `npmjs.com` or `npmx.dev` +[Parameter(ValueFromPipelineByPropertyName)] +[Alias('nodepackages', 'nodepack')] +[string[]] +$NodePackage, + +# A list of file wildcards to include. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Include, + +# A list of file wildcards to exclude. +[Parameter(ValueFromPipelineByPropertyName)] +[SupportsWildcards()] +[string[]] +$Exclude, + +# The base path within the package. +# Content should be added beneath this base path. +[string] +$BasePath = '/', + +# A content type map. +# This maps extensions and URIs to a content type. +[Collections.IDictionary] +$TypeMap = $( + ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap +), + +# The compression option. +[IO.Packaging.CompressionOption] +[Alias('CompressionLevel')] +$CompressionOption = 'Superfast', + +# If set, will force the redownload of various resources and remove existing files or directories +[switch] +$Force, + +# If set, will include hidden files and folders, except for files beneath `.git` +[Alias('IncludeDotFiles')] +[switch] +$IncludeHidden, + +# If set, will include the `.git` directory contents if found. +# By default, this content will be excluded. +[Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] +[switch] +$IncludeGit, + +# If set, will include any content found in `/node_modules`. +# By default, this content will be excluded. +[Alias('IncludeNodeModules')] +[switch] +$IncludeNodeModule, + +# If set, will include any content found in `/_site`. +# By default, this content will be excluded. +[Alias('IncludeWebsite')] +[switch] +$IncludeSite, + +[string[]] +$NodeRepositoryDomain = @( + 'npmjs.com' + 'npmx.dev' + 'www.npmjs.com' + 'www.npmx.dev' +) +) + +# Collect all named parameters to this, +# so we can pass most of them to the next step. +$namedParameters = [Ordered]@{} + +foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) { + $var = $ExecutionContext.SessionState.PSVariable.Get($key) + if (-not [string]::IsNullOrEmpty($var.value)) { + $namedParameters[$key] = $var.value + } +} + +# Remove any node package specific parameters. +$namedParameters.Remove('NodePackage') +$namedParameters.Remove('NodeRepositoryDomain') + +# We want to +if (-not $env:OpenPackagePath) {throw "No Open Package Path"} + +# Get our first OpenPackage package path +$myAppData = @($env:OpenPackagePath -split $( + if ($IsLinux -or $IsMacOS) { + ':' + } else { + ';' + } +))[0] + +# and put all npm packages beneathh this location. +$nodePackRoot = Join-Path $myAppData "node_packages" + +# If we do not have this directory already, create it +if (-not (Test-Path $nodePackRoot)) { + $null = New-Item -ItemType Directory -Path $nodePackRoot -Force +} + +# Push into this location +Push-Location $nodePackRoot + +# Go over each package we want to get. +foreach ($nodePack in $nodePackage) { + # Packages can be in direct named form, or in a url + $nodePackUri = $nodePack -as [uri] + # If the url was absolute + if ($nodePackUri.IsAbsoluteUri) { + # check it against possible domain names. + if ($nodePackUri.DnsSafeHost -notin $NodeRepositoryDomain) { + Write-Error "Can only use an absolute url from $NodeRepositoryDomain" + continue + } + # If the url was versioned + if ($nodePackUri.Segments -match 'v/') { + # split it into two parts + $beforeV, $afterV = $nodePackUri.Segments -join '' -split 'v/' + # and put things in the format npm wants. + $nodePack = "$($beforeV -replace '^/' -replace '^/package' -replace '/$')@$afterV" + } else { + # Otherwise, split off the package segment of the url + $nodePack = + $nodePackUri.Segments[0..3] -ne '/' -join + '' -replace '^package/' -replace '/$' + } + } + # `npm pack` will download a package as .tar.gz + # (with a number of messages on standard error) + $npmOutput = @(npm pack $nodePack *>&1) + # some of these may actually be errors, so create a collection. + $npmErrors = @() + # and make sure we track if the package has been downloaded + # (for it will actually provide the name twice) + $packageDownloaded = $false + # Go over each output. + :gotPackage foreach ($npmOut in $npmOutput) { + # If any of the output is an error record + if ($npmOut -is [Management.Automation.ErrorRecord]) { + # add it to our list + $npmErrors += $npmOut + } + # If the output is not .tgz + if ($npmOut -notmatch '\S+\.tgz$') { + continue # continue + } + # Get the file path from our match + $npmTarFile = $matches.0 + # If the file does not exist + if (-not (Test-Path $npmTarFile)) { + # output our errors + $npmErrors + } else { + # Otherwise, get the tar file as an open package. + $nodePackage = Get-OpenPackage -FilePath "./$npmTarFile" @namedParameters + # If the output was a package + if ($nodePackage -is [IO.Packaging.Package]) { + # mark that we've downloaded it, + $packageDownloaded = $true + $nodePackage # emit the package, + break gotPackage # and take a break. + } + } + } + # If the packge was not downloaded, output errors. + if (-not $packageDownloaded) { + $npmErrors + } +} +Pop-Location + + From 8ab64db94c41c4ac8c19f99249a6fc4a2ba306d9 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 3 Apr 2026 21:28:03 +0000 Subject: [PATCH 599/724] feat: `OpenPackage.Source.Npm` ( Fixes #186 ) --- OP.types.ps1xml | 216 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 214 insertions(+), 2 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index c026051..f1a94d7 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5749,6 +5749,198 @@ foreach ($resolvedItem in $resolvedItems) { + + + + Npm + @@ -5901,7 +6093,19 @@ if (-not $gitApp) { throw "No git" } -$myAppData = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) openPackage +if (-not $env:OpenPackagePath) { + throw "No Open Package Path" +} + + + +$myAppData = $env:OpenPackagePath -split $( + if ($IsLinux -or $IsMacOS) { + ':' + } else { + ';' + } +) $namedParameters = [Ordered]@{} @@ -5939,8 +6143,16 @@ if (-not $owner -or -not $repositoryName) { return } +$repoDir = if ( + ($Repository -as [uri]).DnsSafeHost +) { + ($Repository -as [uri]).DnsSafeHost +} else { + 'repos' +} + # Get the path to our repos -$reposPaths = Join-Path $myAppData 'repos' +$reposPaths = Join-Path $myAppData $repoDir if (-not (Test-Path $reposPaths)) { # (create it if it did not exist) $null = New-Item -ItemType Directory $reposPaths -Force From 62c916015d5207386a6b2f6858677c3ca53b9a2f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 3 Apr 2026 22:59:03 -0700 Subject: [PATCH 600/724] feat: `OpenPackage.Source.Npm` ( Fixes #186 ) Resolving app earlier --- Types/OpenPackage.Source/Npm.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Source/Npm.ps1 b/Types/OpenPackage.Source/Npm.ps1 index fa21d53..43fcc96 100644 --- a/Types/OpenPackage.Source/Npm.ps1 +++ b/Types/OpenPackage.Source/Npm.ps1 @@ -77,6 +77,9 @@ $NodeRepositoryDomain = @( ) ) +$npmApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('npm', 'Application') +if (-not $npmApp) { throw "npm not installed or in path"} + # Collect all named parameters to this, # so we can pass most of them to the next step. $namedParameters = [Ordered]@{} @@ -141,7 +144,7 @@ foreach ($nodePack in $nodePackage) { } # `npm pack` will download a package as .tar.gz # (with a number of messages on standard error) - $npmOutput = @(npm pack $nodePack *>&1) + $npmOutput = @(& $npmApp pack $nodePack *>&1) # some of these may actually be errors, so create a collection. $npmErrors = @() # and make sure we track if the package has been downloaded From 4cecfba576260bcaaf644b6c693392f015945c51 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 4 Apr 2026 05:59:20 +0000 Subject: [PATCH 601/724] feat: `OpenPackage.Source.Npm` ( Fixes #186 ) Resolving app earlier --- OP.types.ps1xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f1a94d7..fd125c2 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5833,6 +5833,9 @@ $NodeRepositoryDomain = @( ) ) +$npmApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('npm', 'Application') +if (-not $npmApp) { throw "npm not installed or in path"} + # Collect all named parameters to this, # so we can pass most of them to the next step. $namedParameters = [Ordered]@{} @@ -5897,7 +5900,7 @@ foreach ($nodePack in $nodePackage) { } # `npm pack` will download a package as .tar.gz # (with a number of messages on standard error) - $npmOutput = @(npm pack $nodePack *>&1) + $npmOutput = @(& $npmApp pack $nodePack *>&1) # some of these may actually be errors, so create a collection. $npmErrors = @() # and make sure we track if the package has been downloaded From 01898750adf659f912ecc33902d415221a843e0b Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 3 Apr 2026 23:55:59 -0700 Subject: [PATCH 602/724] feat: `OpenPackage.Source.Node` ( Fixes #186 ) Renaming for clarity --- Types/OpenPackage.Source/{Npm.ps1 => Node.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Types/OpenPackage.Source/{Npm.ps1 => Node.ps1} (100%) diff --git a/Types/OpenPackage.Source/Npm.ps1 b/Types/OpenPackage.Source/Node.ps1 similarity index 100% rename from Types/OpenPackage.Source/Npm.ps1 rename to Types/OpenPackage.Source/Node.ps1 From 4d22cb69166a1711df7cc0dd3cbf8dc58cdba330 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 4 Apr 2026 06:56:20 +0000 Subject: [PATCH 603/724] feat: `OpenPackage.Source.Node` ( Fixes #186 ) Renaming for clarity --- OP.types.ps1xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index fd125c2..1251fc4 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5752,7 +5752,7 @@ foreach ($resolvedItem in $resolvedItems) { - Npm + Node + + Python + + Repository From f58c7f2169a2be99e5cc10fb9db435731c0a8b76 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 5 Apr 2026 23:02:15 -0700 Subject: [PATCH 639/724] feat: `OpenPackage.Part.get_Related` ( Fixes #190 ) Getting package relationships as well --- Types/OpenPackage.Part/get_Related.ps1 | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Types/OpenPackage.Part/get_Related.ps1 b/Types/OpenPackage.Part/get_Related.ps1 index cbd4c36..8141254 100644 --- a/Types/OpenPackage.Part/get_Related.ps1 +++ b/Types/OpenPackage.Part/get_Related.ps1 @@ -2,11 +2,18 @@ .SYNOPSIS Get Related Part information .DESCRIPTION - Get Package Part Relationships + Get Package Part and Package Relationships .NOTES - This is a shorthand for `.GetRelationships()` + This will`.GetRelationships()` from the current part, + then `.GetRelationships()` from the package. #> if (-not $this.GetRelationships) { return } -@($this.GetRelationships()) + +@( + $this.GetRelationships() + if ($this.Package -is [IO.Packaging.Package]) { + $this.Package.GetRelationships() + } +) From f3d45d7c9339cbcaef49f0df7f4109bca6b3b189 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 6 Apr 2026 06:02:41 +0000 Subject: [PATCH 640/724] feat: `OpenPackage.Part.get_Related` ( Fixes #190 ) Getting package relationships as well --- OP.types.ps1xml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 98bf7ff..d423f51 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4270,14 +4270,21 @@ return $this.'#Readers' .SYNOPSIS Get Related Part information .DESCRIPTION - Get Package Part Relationships + Get Package Part and Package Relationships .NOTES - This is a shorthand for `.GetRelationships()` + This will`.GetRelationships()` from the current part, + then `.GetRelationships()` from the package. #> if (-not $this.GetRelationships) { return } -@($this.GetRelationships()) + +@( + $this.GetRelationships() + if ($this.Package -is [IO.Packaging.Package]) { + $this.Package.GetRelationships() + } +) From 9769cb75e6f62186e0a3ed278cf0698fd2858957 Mon Sep 17 00:00:00 2001 From: James Brundage <+@noreply.github.com> Date: Mon, 6 Apr 2026 21:47:39 -0700 Subject: [PATCH 641/724] feat: `OpenPackage.View.at.markpub.markdown` ( Fixes #178 ) Adding front matter --- .../OpenPackage.View/at.markpub.markdown.ps1 | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/Types/OpenPackage.View/at.markpub.markdown.ps1 b/Types/OpenPackage.View/at.markpub.markdown.ps1 index 7d1c939..c31e5ac 100644 --- a/Types/OpenPackage.View/at.markpub.markdown.ps1 +++ b/Types/OpenPackage.View/at.markpub.markdown.ps1 @@ -14,6 +14,8 @@ #> param() +$allInput = @($input) + @($args) + # Make one quick pass over all input $allInput = @( foreach ($in in $allInput) { @@ -24,7 +26,7 @@ $allInput = @( elseif ($in.Package -is [IO.Packaging.Package]) { $in.Package.GetParts() } - else { + elseif ($in) { $in } } @@ -33,24 +35,42 @@ $allInput = @( # Declare a filter filter at.markpub.markdown { + $in = $_ + + $frontMatter = [Ordered]@{} + + if ($in.Package -and $in.PartUri) { + $inPart = $in.Package.GetPart($in.PartUri) + $frontMatter['name'] = $inPart.Name -replace '[\-_]', ' ' + $frontMatter['path'] = + $in.PartUri -replace '(?>/README|/index)?\.(?>md|markdown)$' + } + [PSCustomObject]@{ PSTypeName = 'at.markpub.markdown' '$type' = 'at.markpub.markdown' 'text' = [PSCustomObject]@{ PSTypeName = 'at.markpub.text' '$type' = 'at.markpub.text' - 'markdown' = $_ + 'markdown' = + if ($in -is [string]) { + $in + } elseif ($in.markdown) { + $in.markdown + } else { + '' + } } + 'frontMatter' = [PSCustomObject]$frontMatter } } # Now, let's go over all input -:nextInput foreach ($in in $allInput) { +:nextInput foreach ($in in $allInput) { # If the input is a string if ($in -is [string]) { - # Just take the markdown - # and put it in an at.markpub.markdown object - + # Just take the markdown, + # put it in an at.markpub.markdown object, # and continue to the next input. $in | at.markpub.markdown continue nextInput @@ -60,8 +80,7 @@ filter at.markpub.markdown { $in continue nextInput } - - + # If the input is a package part, we can do more if ( $in -is [IO.Packaging.PackagePart] -and @@ -75,9 +94,8 @@ filter at.markpub.markdown { continue nextInput } # Read our input and make it `at.markpub.markdown` - $in.Read().markdown | + $in.Read() | at.markpub.markdown - # continue to the next input. continue nextInput } From e32a1ffe63f337663c0b971df06a1ebaf5a656ec Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 7 Apr 2026 04:48:35 +0000 Subject: [PATCH 642/724] feat: `OpenPackage.View.at.markpub.markdown` ( Fixes #178 ) Adding front matter --- OP.types.ps1xml | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index d423f51..581ce4c 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -7235,6 +7235,8 @@ If the file is not already an open package, extracts the zip and creates an open #> param() +$allInput = @($input) + @($args) + # Make one quick pass over all input $allInput = @( foreach ($in in $allInput) { @@ -7245,7 +7247,7 @@ $allInput = @( elseif ($in.Package -is [IO.Packaging.Package]) { $in.Package.GetParts() } - else { + elseif ($in) { $in } } @@ -7254,24 +7256,42 @@ $allInput = @( # Declare a filter filter at.markpub.markdown { + $in = $_ + + $frontMatter = [Ordered]@{} + + if ($in.Package -and $in.PartUri) { + $inPart = $in.Package.GetPart($in.PartUri) + $frontMatter['name'] = $inPart.Name -replace '[\-_]', ' ' + $frontMatter['path'] = + $in.PartUri -replace '(?>/README|/index)?\.(?>md|markdown)$' + } + [PSCustomObject]@{ PSTypeName = 'at.markpub.markdown' '$type' = 'at.markpub.markdown' 'text' = [PSCustomObject]@{ PSTypeName = 'at.markpub.text' '$type' = 'at.markpub.text' - 'markdown' = $_ + 'markdown' = + if ($in -is [string]) { + $in + } elseif ($in.markdown) { + $in.markdown + } else { + '' + } } + 'frontMatter' = [PSCustomObject]$frontMatter } } # Now, let's go over all input -:nextInput foreach ($in in $allInput) { +:nextInput foreach ($in in $allInput) { # If the input is a string if ($in -is [string]) { - # Just take the markdown - # and put it in an at.markpub.markdown object - + # Just take the markdown, + # put it in an at.markpub.markdown object, # and continue to the next input. $in | at.markpub.markdown continue nextInput @@ -7281,8 +7301,7 @@ filter at.markpub.markdown { $in continue nextInput } - - + # If the input is a package part, we can do more if ( $in -is [IO.Packaging.PackagePart] -and @@ -7296,9 +7315,8 @@ filter at.markpub.markdown { continue nextInput } # Read our input and make it `at.markpub.markdown` - $in.Read().markdown | + $in.Read() | at.markpub.markdown - # continue to the next input. continue nextInput } From b862243d0bd4dbe9a30232b919c1e9164b794c04 Mon Sep 17 00:00:00 2001 From: James Brundage <+@noreply.github.com> Date: Tue, 7 Apr 2026 00:10:41 -0700 Subject: [PATCH 643/724] feat: `OpenPackage.Part.get_Metadata` ( Fixes #191 ) --- Types/OpenPackage.Part/get_Metadata.ps1 | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 Types/OpenPackage.Part/get_Metadata.ps1 diff --git a/Types/OpenPackage.Part/get_Metadata.ps1 b/Types/OpenPackage.Part/get_Metadata.ps1 new file mode 100644 index 0000000..4be53fb --- /dev/null +++ b/Types/OpenPackage.Part/get_Metadata.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS + Gets Part Metadata +.DESCRIPTION + Gets Part Metadata from parts with similar names +.EXAMPLE + $package.GetPart("/foo").Metadata +.NOTES + Parts can get metadata from many places. + + This will get metadata in a cascade. + + First, it will get any metadata found in a related data file: + + For example, `/foo/bar.md` could have metadata in: + + * `/foo/bar.md.json` + * `/foo/bar.md.psd1` + * `/foo/bar.md.toml` + * `/foo/bar.md.xml` + * `/foo/bar.md.yaml` + + * `/foo.json` + * `/foo.psd1` + * `/foo.toml` + * `/foo.xml` + * `/foo.yaml` + +#> +param() + +# For the moment, we will limit the types of files we can process as metadata. +$dataFilePattern = '\.(?>json|psd1|toml|xml|ya?ml)$' + +# First, match any directly related files. +$directlyRelated = "^$([Regex]::Escape("$($this.Uri)"))$dataFilePattern" + +# Check each part in the package +foreach ($part in $this.Package.Parts) { + # if it is directly related, and readable, read it. + if ($part.Uri -match $directlyRelated -and $part.Reader) { + try { + $part.Read() + } catch { + Write-Warning "Error Reading $($part.Uri): $_" + } + } +} + +# Now let's go up thru the list of segments +$segments = @($this.Uri -split '/' -ne '') +# Go backwards to forwards (bottom-most path to topmost path) +for ($segmentNumber = $segments.Count - 1; $segmentNumber -ge 0; $segmentNumber--) { + # If we're at the topmost path and have an identifier + $relatedToParent = if ($segmentNumber -eq 0 -and $this.Package.Identifier) { + # Look for files related to the identifier. + "/$([Regex]::Escape($this.Package.Identifier))$dataFilePattern" + } elseif ($segmentNumber -gt 0) { + # If we're at any greater index, join all segments together + "/$([Regex]::Escape( + @( + $segments[0..$segmentNumber]; + # and repeat the last segment. + $segments[$segmentNumber] + ) -join '/') + ))$dataFilePattern$" + } + + # Now go thru all of the parts in the package + foreach ($part in $this.Package.Parts) { + # and find any related to this level of parent. + if ($part.Uri -match $relatedToParent -and $part.Reader) { + try { + # and read the metadata. + $part.Read() + } catch { + Write-Warning "Error Reading $($part.Uri): $_" + } + } + } +} \ No newline at end of file From d9adb1c055e50522b322366e31ed39079101a00f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 7 Apr 2026 07:11:44 +0000 Subject: [PATCH 644/724] feat: `OpenPackage.Part.get_Metadata` ( Fixes #191 ) --- OP.types.ps1xml | 86 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 581ce4c..cc3d6a4 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4100,6 +4100,92 @@ if ($partSegments.Count -ge 2) { return $false + + Metadata + + <# +.SYNOPSIS + Gets Part Metadata +.DESCRIPTION + Gets Part Metadata from parts with similar names +.EXAMPLE + $package.GetPart("/foo").Metadata +.NOTES + Parts can get metadata from many places. + + This will get metadata in a cascade. + + First, it will get any metadata found in a related data file: + + For example, `/foo/bar.md` could have metadata in: + + * `/foo/bar.md.json` + * `/foo/bar.md.psd1` + * `/foo/bar.md.toml` + * `/foo/bar.md.xml` + * `/foo/bar.md.yaml` + + * `/foo.json` + * `/foo.psd1` + * `/foo.toml` + * `/foo.xml` + * `/foo.yaml` + +#> +param() + +# For the moment, we will limit the types of files we can process as metadata. +$dataFilePattern = '\.(?>json|psd1|toml|xml|ya?ml)$' + +# First, match any directly related files. +$directlyRelated = "^$([Regex]::Escape("$($this.Uri)"))$dataFilePattern" + +# Check each part in the package +foreach ($part in $this.Package.Parts) { + # if it is directly related, and readable, read it. + if ($part.Uri -match $directlyRelated -and $part.Reader) { + try { + $part.Read() + } catch { + Write-Warning "Error Reading $($part.Uri): $_" + } + } +} + +# Now let's go up thru the list of segments +$segments = @($this.Uri -split '/' -ne '') +# Go backwards to forwards (bottom-most path to topmost path) +for ($segmentNumber = $segments.Count - 1; $segmentNumber -ge 0; $segmentNumber--) { + # If we're at the topmost path and have an identifier + $relatedToParent = if ($segmentNumber -eq 0 -and $this.Package.Identifier) { + # Look for files related to the identifier. + "/$([Regex]::Escape($this.Package.Identifier))$dataFilePattern" + } elseif ($segmentNumber -gt 0) { + # If we're at any greater index, join all segments together + "/$([Regex]::Escape( + @( + $segments[0..$segmentNumber]; + # and repeat the last segment. + $segments[$segmentNumber] + ) -join '/') + ))$dataFilePattern$" + } + + # Now go thru all of the parts in the package + foreach ($part in $this.Package.Parts) { + # and find any related to this level of parent. + if ($part.Uri -match $relatedToParent -and $part.Reader) { + try { + # and read the metadata. + $part.Read() + } catch { + Write-Warning "Error Reading $($part.Uri): $_" + } + } + } +} + + Name From 1b0879b1673c39f204a27a916d84e0c3a76b2e31 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 7 Apr 2026 12:54:59 -0700 Subject: [PATCH 645/724] feat: `OpenPackage.Part.get_Metadata` ( Fixes #191 ) Fixing regex --- Types/OpenPackage.Part/get_Metadata.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Part/get_Metadata.ps1 b/Types/OpenPackage.Part/get_Metadata.ps1 index 4be53fb..7935fa0 100644 --- a/Types/OpenPackage.Part/get_Metadata.ps1 +++ b/Types/OpenPackage.Part/get_Metadata.ps1 @@ -38,7 +38,8 @@ $directlyRelated = "^$([Regex]::Escape("$($this.Uri)"))$dataFilePattern" # Check each part in the package foreach ($part in $this.Package.Parts) { # if it is directly related, and readable, read it. - if ($part.Uri -match $directlyRelated -and $part.Reader) { + if ($part.Uri -notmatch $directlyRelated) { continue } + if ($part.Reader) { try { $part.Read() } catch { @@ -62,7 +63,7 @@ for ($segmentNumber = $segments.Count - 1; $segmentNumber -ge 0; $segmentNumber- $segments[0..$segmentNumber]; # and repeat the last segment. $segments[$segmentNumber] - ) -join '/') + ) -join '/' ))$dataFilePattern$" } From 5490870655cdaf4ec65307149d2f925135e3e388 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 7 Apr 2026 19:55:18 +0000 Subject: [PATCH 646/724] feat: `OpenPackage.Part.get_Metadata` ( Fixes #191 ) Fixing regex --- OP.types.ps1xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index cc3d6a4..28da76c 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4143,7 +4143,8 @@ $directlyRelated = "^$([Regex]::Escape("$($this.Uri)"))$dataFilePattern" # Check each part in the package foreach ($part in $this.Package.Parts) { # if it is directly related, and readable, read it. - if ($part.Uri -match $directlyRelated -and $part.Reader) { + if ($part.Uri -notmatch $directlyRelated) { continue } + if ($part.Reader) { try { $part.Read() } catch { @@ -4167,7 +4168,7 @@ for ($segmentNumber = $segments.Count - 1; $segmentNumber -ge 0; $segmentNumber- $segments[0..$segmentNumber]; # and repeat the last segment. $segments[$segmentNumber] - ) -join '/') + ) -join '/' ))$dataFilePattern$" } From a3467e482343cebf1df4856fde19edd28dc6730c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 7 Apr 2026 13:09:22 -0700 Subject: [PATCH 647/724] feat: `OpenPackage.Part.get_PowerShellFunctionAst` ( Fixes #192 ) --- Types/OpenPackage/get_PowerShellFunctionAst.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Types/OpenPackage/get_PowerShellFunctionAst.ps1 diff --git a/Types/OpenPackage/get_PowerShellFunctionAst.ps1 b/Types/OpenPackage/get_PowerShellFunctionAst.ps1 new file mode 100644 index 0000000..352e34d --- /dev/null +++ b/Types/OpenPackage/get_PowerShellFunctionAst.ps1 @@ -0,0 +1,16 @@ +<# +.SYNOPSIS + Gets PowerShell Function Defintions +.DESCRIPTION + Gets PowerShell Function Defintion Ast references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.FunctionDefinitionAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} \ No newline at end of file From 1e35d8fbe53949928f662b2b82f7ded25bcdec23 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 7 Apr 2026 20:09:39 +0000 Subject: [PATCH 648/724] feat: `OpenPackage.Part.get_PowerShellFunctionAst` ( Fixes #192 ) --- OP.types.ps1xml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 28da76c..5305246 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1690,6 +1690,27 @@ foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { }, $true) | Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | Add-Member NoteProperty Package $content.Package -Force -PassThru +} + + + + PowerShellFunctionAst + + <# +.SYNOPSIS + Gets PowerShell Function Defintions +.DESCRIPTION + Gets PowerShell Function Defintion Ast references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.FunctionDefinitionAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru } From 1b5dd52b3949a12f76d42f6545a1cdbd78c221d6 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 7 Apr 2026 13:15:23 -0700 Subject: [PATCH 649/724] feat: `OpenPackage.Part.get_PowerShellTypeDefinitionAst` ( Fixes #193 ) --- .../get_PowerShellTypeDefinitionAst.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Types/OpenPackage/get_PowerShellTypeDefinitionAst.ps1 diff --git a/Types/OpenPackage/get_PowerShellTypeDefinitionAst.ps1 b/Types/OpenPackage/get_PowerShellTypeDefinitionAst.ps1 new file mode 100644 index 0000000..fc358ef --- /dev/null +++ b/Types/OpenPackage/get_PowerShellTypeDefinitionAst.ps1 @@ -0,0 +1,16 @@ +<# +.SYNOPSIS + Gets PowerShell Type Defintions +.DESCRIPTION + Gets PowerShell Type Defintion Ast references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.TypeDefinitionAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru +} \ No newline at end of file From 14e584cc0f2d0263da15d3284cf4a496b53cdb54 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 7 Apr 2026 20:15:40 +0000 Subject: [PATCH 650/724] feat: `OpenPackage.Part.get_PowerShellTypeDefinitionAst` ( Fixes #193 ) --- OP.types.ps1xml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 5305246..35419af 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -1782,6 +1782,27 @@ foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { }, $true) | Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | Add-Member NoteProperty Package $content.Package -Force -PassThru +} + + + + PowerShellTypeDefinitionAst + + <# +.SYNOPSIS + Gets PowerShell Type Defintions +.DESCRIPTION + Gets PowerShell Type Defintion Ast references within an Open Package. +#> +foreach ($content in $this.GetContent(@($this.FileList -match '.psm?1$'))) { + if (-not $content.Ast) { continue } + $content.Ast.FindAll({ + param($ast) + + $ast -is [Management.Automation.Language.TypeDefinitionAst] + }, $true) | + Add-Member NoteProperty PartUri $content.PartUri -Force -PassThru | + Add-Member NoteProperty Package $content.Package -Force -PassThru } From 3ff761b3bf9184af684378e2a4b1e2f2e4cb4d38 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 11:32:03 -0700 Subject: [PATCH 651/724] docs: `OpenPackage.GetContent` ( Fixes #57 ) Updating description --- Types/OpenPackage/GetContent.ps1 | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Types/OpenPackage/GetContent.ps1 b/Types/OpenPackage/GetContent.ps1 index 1c7529b..9061652 100644 --- a/Types/OpenPackage/GetContent.ps1 +++ b/Types/OpenPackage/GetContent.ps1 @@ -4,19 +4,9 @@ .DESCRIPTION Gets the content of one or more parts. - Will attempt to get the content in the best type available. - - * If the content is url-encoded, will get the content as an `[Ordered]` dictionary. - * If the content is XML, it will be returned as XML. - * If the content is CliXml, it will be deserialized into PSObjects - * If the content type or part ends with `.json`, it will converted from json. - * If the content type or part ends with `.yaml`, it will converted from yaml. - * If the content type or part ends with `.toml`, it will converted from toml. - * If the content type is PowerShell data, will run the block in a data guard. - * If the content type is PowerShell or ends with *.psm?1, it will be turned into a [ScriptBlock] - * If the - - Otherwise, if the content type is text, will get content as text + Will attempt to get the content in the best type available, using any available reader. + + Otherwise, will attempt to convert xml, and, if that fails, will return content bytes. .NOTES Yaml support requires the installation of an appropriate ConvertFrom-Yaml command @@ -25,8 +15,6 @@ Toml support requires the installation of an appropriate ConvertFrom-Toml command [PSToml](https://github.com/jborean93/PSToml) is recommended. - - #> param() @@ -81,7 +69,7 @@ $matchingParts = $partStreamReader.Close() $partStreamReader.Dispose() $byteStream.Close() - $byteStream.Dispose() + $byteStream.Dispose() $partAsXml = $partString -as [xml] if ($null -ne $partAsXml) { From 1622aa166137032c173330bae646a4a27e341740 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 11:34:30 -0700 Subject: [PATCH 652/724] feat: `OpenPackage.SetContent` ( Fixes #58 ) Using writer when available. Adding options. --- Types/OpenPackage/SetContent.ps1 | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Types/OpenPackage/SetContent.ps1 b/Types/OpenPackage/SetContent.ps1 index ad3ae5c..9e1c450 100644 --- a/Types/OpenPackage/SetContent.ps1 +++ b/Types/OpenPackage/SetContent.ps1 @@ -17,27 +17,34 @@ $Content, # The content type [string] -$ContentType +$ContentType, + +# Any options +[Alias('Options')] +[Collections.IDictionary] +$Option = [Ordered]@{} ) $part = if ($InputObject.PartExists($uri)) { - $currentPart = $InputObject.GetPart($uri) - - $currentPartUri, $currentContentType, $currentCompressionLevel = - $currentPart.Uri, $currentPart.ContentType, $currentPart.CompressionLevel - - $inputObject.DeletePart($currentPartUri) - - $InputObject.CreatePart($currentPartUri, $currentContentType, $currentCompressionLevel) + $InputObject.GetPart($uri) } else { $InputObject.CreatePart($uri, $ContentType) } if (-not $?) { return } +if ($part.Writer -and $part.Write) { + $part.Write($Content, $Option) + return +} + # Get the stream $partStream = $part.GetStream() + +if (-not $partStream) { return } +# zero out the stream, so we do not underwrite content +$partStream.SetLength(0) # First see if the content is a byte[] if ($content -is [byte[]]) { # if so, just write it From bb63a646cb85ca39487b60ddf57aeccfb0aa93bf Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 18:34:59 +0000 Subject: [PATCH 653/724] feat: `OpenPackage.SetContent` ( Fixes #58 ) Using writer when available. Adding options. --- OP.types.ps1xml | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 35419af..676318e 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -56,19 +56,9 @@ .DESCRIPTION Gets the content of one or more parts. - Will attempt to get the content in the best type available. - - * If the content is url-encoded, will get the content as an `[Ordered]` dictionary. - * If the content is XML, it will be returned as XML. - * If the content is CliXml, it will be deserialized into PSObjects - * If the content type or part ends with `.json`, it will converted from json. - * If the content type or part ends with `.yaml`, it will converted from yaml. - * If the content type or part ends with `.toml`, it will converted from toml. - * If the content type is PowerShell data, will run the block in a data guard. - * If the content type is PowerShell or ends with *.psm?1, it will be turned into a [ScriptBlock] - * If the - - Otherwise, if the content type is text, will get content as text + Will attempt to get the content in the best type available, using any available reader. + + Otherwise, will attempt to convert xml, and, if that fails, will return content bytes. .NOTES Yaml support requires the installation of an appropriate ConvertFrom-Yaml command @@ -77,8 +67,6 @@ Toml support requires the installation of an appropriate ConvertFrom-Toml command [PSToml](https://github.com/jborean93/PSToml) is recommended. - - #> param() @@ -133,7 +121,7 @@ $matchingParts = $partStreamReader.Close() $partStreamReader.Dispose() $byteStream.Close() - $byteStream.Dispose() + $byteStream.Dispose() $partAsXml = $partString -as [xml] if ($null -ne $partAsXml) { @@ -363,27 +351,34 @@ $Content, # The content type [string] -$ContentType +$ContentType, + +# Any options +[Alias('Options')] +[Collections.IDictionary] +$Option = [Ordered]@{} ) $part = if ($InputObject.PartExists($uri)) { - $currentPart = $InputObject.GetPart($uri) - - $currentPartUri, $currentContentType, $currentCompressionLevel = - $currentPart.Uri, $currentPart.ContentType, $currentPart.CompressionLevel - - $inputObject.DeletePart($currentPartUri) - - $InputObject.CreatePart($currentPartUri, $currentContentType, $currentCompressionLevel) + $InputObject.GetPart($uri) } else { $InputObject.CreatePart($uri, $ContentType) } if (-not $?) { return } +if ($part.Writer -and $part.Write) { + $part.Write($Content, $Option) + return +} + # Get the stream $partStream = $part.GetStream() + +if (-not $partStream) { return } +# zero out the stream, so we do not underwrite content +$partStream.SetLength(0) # First see if the content is a byte[] if ($content -is [byte[]]) { # if so, just write it From a6b2bc738a59ab330621e8d80d09799d23d79e49 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 11:56:39 -0700 Subject: [PATCH 654/724] docs: `README.md.ps1` ( Fixes #194 ) Adding README.md.ps1 and README.md --- README.md | 517 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md.ps1 | 229 ++++++++++++++++++++++ 2 files changed, 746 insertions(+) create mode 100644 README.md create mode 100644 README.md.ps1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b046a21 --- /dev/null +++ b/README.md @@ -0,0 +1,517 @@ +# OP +[![OP](https://img.shields.io/powershellgallery/dt/OP)](https://www.powershellgallery.com/packages/OP/) +## An Overpowered little module for Open Packages +Anything can be a package. + +This command helps you make anything into a package. + +The following types of packages are currently supported: + +* Any [Open Packaging Convention](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) files +* Any directory +* Any `*.zip` file +* Any `*.tar.gz` file +* Any nuget package +* Any git repository +* Any public at protocol URI +* Any dictionary (including nested dictionaries) +* Any url +* Any file + +_Anything can be a package_. + +Once we start to treat anything as a package, we can do amazing things with packages. + +Like: + +* Inspect any packages before we work with them. +* Modify the packages to customize their content. +* Split packages +* Filter our components. +* Join them back together. +* Search package content. +* Work with compressed trees of data. +* Have an in-memory containerized virtual filesystem. +* Serve a package from memory. +* Store data to N package layers. + +To put it simply, open packages are overpowered. + +## Installing and Importing + +You can install OP from the [PowerShell gallery](https://powershellgallery.com/) + +~~~PowerShell +Install-Module OP -Scope CurrentUser -Force +~~~ + +Once installed, you can import the module with: + +~~~PowerShell +Import-Module OP -PassThru +~~~ + + +You can also clone the repo and import the module locally: + +~~~PowerShell +git clone https://github.com/PoshWeb/OP +cd ./OP +Import-Module ./ -PassThru +~~~ + +## Functions +OP has 17 functions +### Copy-OpenPackage +#### Copies Open Packages +Copies Contents from one packages to another. +##### Examples +###### Example 1 +~~~PowerShell +Copy-OpenPackage -DestinationPath ./Examples/Copy.docx -InputObject ./Examples/Sample.docx -Force +~~~ +##### Parameters + +|Name|Type|Description| +|-|-|-| +|Destination|PSObject|The destination.
    If this is not a `[IO.Packaging.Package]`, it will be considered a file path.| +|Include|String[]|Includes the specified parts.

    Enter a wildcard pattern, such as `*.txt`

    Wildcards are permitted.| +|Exclude|String[]|Excludes the specified parts.

    Enter a wildcard pattern, such as `*.txt`

    Wildcards are permitted.| +|IncludeContentType|String[]|Includes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|ExcludeContentType|String[]|Excludes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|InputObject|PSObject|The input object| +|Force|SwitchParameter|If set, will update existing packages.| + +### Export-OpenPackage +#### Exports OpenPackage packages +Exports loaded packages to a file or directory. +##### Parameters + +|Name|Type|Description| +|-|-|-| +|DestinationPath|String|The package file path.
    If this has no extension, it will be considered a directory name.
    If the path already exists and is a directory, it will be considered a directory name.| +|Include|String[]|Includes the specified parts.

    Enter a wildcard pattern, such as `*.txt`| +|Exclude|String[]|Excludes the specified parts.

    Enter a wildcard pattern, such as `*.txt`| +|IncludeContentType|String[]|Includes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|InputObject|PSObject|The input object.
    This must be a package loaded with this module.| +|Force|SwitchParameter|If set, will force the export even if a file already exists.| + +### Format-OpenPackage +#### Formats Open Package +Formats Open Packages using any view. +##### Parameters + +|Name|Type|Description| +|-|-|-| +|View|PSObject|The name of the view, or a view command or scriptblock| +|ArgumentList|PSObject[]|The Any Positional arguments for the view| +|InputObject|PSObject[]|Any input objects.| +|Option|IDictionary|Any options or parameters to pass to the View| + +### Get-OpenPackage +#### Gets Open Packages +Gets Open Packages from almost anything. + +Anything can be a package. + +This command helps you make anything into a package. + +The following types of packages are currently supported: + +* Any [Open Packaging Convention](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) files +* Any directory +* Any `*.zip` file +* Any `*.tar.gz` file +* Any url +* Any public nuget package +* Any git repository +* Any public at protocol URI +* Any dictionary (including nested dictionaries) +* Any single file + +Anything can be a package. + +Once we start to treat anything as a package, we can do amazing things with packages. + +Like: + +* Inspect any packages before we work with them. +* Modify the packages to customize their content. +* Split packages +* Filter our components. +* Join them back together. +* Search package content. +* Work with compressed trees of data. +* Have an in-memory containerized virtual filesystem. +* Serve a package from memory. +* Store data to N package layers. +##### Examples +###### Example 1 +Make the current directory into a package +(do not try this at `$home`) +~~~PowerShell +Get-OpenPackage . +~~~ +###### Example 2 +Make the module into a package +~~~PowerShell +$opPackage = Get-Module OP | Get-OpenPackage +~~~ +###### Example 3 +~~~PowerShell +$opPackage = Get-Module OP | + Get-OpenPackage -Include *.ps1 +~~~ +###### Example 4 +Another way to make the current directory into a package +(do not try this at `$home`) +~~~PowerShell +Get-Item . | Get-OpenPackage +~~~ +###### Example 5 +Get a package from nuget +~~~PowerShell +$Avalonia = OP https://www.nuget.org/packages/Avalonia/ +~~~ +###### Example 6 +Get a package from Chocolatey +~~~PowerShell +$chocoPackage = op https://community.chocolatey.org/packages/chocolatey +~~~ +###### Example 7 +Get a package from the PowerShell gallery +~~~PowerShell +$turtlePackage = op https://powershellgallery.com/packages/Turtle +~~~ +###### Example 8 +Get a package from a single URL +~~~PowerShell +$imagePackage = op https://MrPowerShell.com/MrPowerShell.png +~~~ +###### Example 9 +Create a package from multiple URLs by piping back to ourself +~~~PowerShell +$svgAndPng = op https://MrPowerShell.com/MrPowerShell.png | + op https://MrPowerShell.com/MrPowerShell.svg +~~~ +###### Example 10 +Get a package from an at protocol URI +~~~PowerShell +$atPost = op at://mrpowershell.com/app.bsky.feed.post/3k4hf5dy6nf2g +~~~ +###### Example 11 +Get the most recent 50 posts +~~~PowerShell +$atLast50 = op at://mrpowershell.com/app.bsky.feed.post/ -First 50 +~~~ +###### Example 12 +Get all standard.site.documents for a user +~~~PowerShell +$standardSiteDocuments = op at://mrpowershell.com/site.standard.document/ +~~~ +#### Links +* [https://en.wikipedia.org/wiki/Open_Packaging_Conventions](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) +##### Parameters + +|Name|Type|Description| +|-|-|-| +|ArgumentList|PSObject[]|Any unnamed arguments to the command.
    Each argument will be treated as a potential -FilePath or -Uri.
    Once the first related verb is detected, these will become arguments to that verb
    (For example, `op . start` will get an open package and then start a server for that package)| +|FilePath|String|The path of a file to import| +|At|String[]|Gets Open Packages with `@` syntax.
    Without a domain, `@` will be presumed to be a github
    With a domain, `@` will look for an https url, and check at protocol| +|Uri|Uri[]|A URI to package.
    If this URI is a git repository, will make a package out of the repository
    If this URI is a nuget package url or powershell gallery url, will download the package.| +|Headers|IDictionary|Any additional headers to pass into a web request.| +|Repository|String|A Repository to package.
    This can be the root of a repo or a link to a portion of the tree.
    If a portion of the tree is provided, will perform a sparse clone of the repository| +|Branch|String|The github branch name.| +|SparseFilter|String[]|One or more optional sparse filters to a repository.
    If these are provided, only files matching these filters will be downloaded.| +|AtUri|String[]|An At Uri to package.
    This can be a single post or a collection of all posts of a type.| +|PDS|String|The personal data server. This is used in At Protocol requests.| +|Dictionary|IDictionary|Adds a dictionary of content to the package| +|BasePath|String|The base path within the package.
    Content should be added beneath this base path.| +|NuGet|Uri|A Nuget Uri to package.
    The package at this location will be downloaded and opened directly.| +|NodePackage|String[]|One or more Node Packages.| +|PythonPackage|String[]|One or more Python Packages.| +|Module|PSObject|A module to package
    A loaded module name or moduleinfo object to package.
    The loaded module must have a path property.
    The files in this path will be packaged.| +|Include|String[]|A list of file wildcards to include.| +|Exclude|String[]|A list of file wildcards to exclude.| +|TypeMap|IDictionary|A content type map.
    This maps extensions and URIs to a content type.| +|CompressionOption|CompressionOption|The compression option.| +|InputObject|PSObject|One or more input objects.| +|Installed|SwitchParameter|Gets the packages that are currently installed| +|Running|SwitchParameter|Gets packages that are currently running in a server| +|Force|SwitchParameter|If set, will force the redownload of various resources and remove existing files or directories| +|IncludeHidden|SwitchParameter|If set, will include hidden files and folders, except for files beneath `.git`| +|IncludeGit|SwitchParameter|If set, will include the `.git` directory contents if found.
    By default, this content will be excluded.| +|IncludeNodeModule|SwitchParameter|If set, will include any content found in `/node_modules`.
    By default, this content will be excluded.| +|IncludeSite|SwitchParameter|If set, will include any content found in `/_site`.
    By default, this content will be excluded.| +|IncludeTotalCount|SwitchParameter|| +|Skip|UInt64|| +|First|UInt64|| + +### Install-OpenPackage +#### Installs an OpenPackage +Installs an OpenPackage into a destination on disk. +##### Parameters + +|Name|Type|Description| +|-|-|-| +|ArgumentList|PSObject[]|The arguments to Get-OpenPackage.| +|DestinationPath|String|The destination path.

    If provided, this should be a directory, but can be a file.

    If multiple packages will be installed and a -DestinationPath was provided,
    all packages will be installed into that destination path.

    If no destination path is provided,
    only packages with an identifier will be installed.

    Packages will install beneath the first `$env:OpenPackagePath`.

    If the package has a version, it will install into a versioned subdirectory.| +|Include|String[]|Includes the specified parts.

    Enter a wildcard pattern, such as `*.txt`| +|Exclude|String[]|Excludes the specified parts.

    Enter a wildcard pattern, such as `*.txt`| +|IncludeContentType|String[]|Includes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|ExcludeContentType|String[]|Excludes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|InputObject|PSObject|The input object. If this is not a package, it will be passed thru.| +|Force|SwitchParameter|If set, will overwrite existing files.| +|Clear|SwitchParameter|If set, will clear the destination directory before installing.| +|PassThru|SwitchParameter|If set, will output the files that are expanded from the package.| +|WhatIf|SwitchParameter|| +|Confirm|SwitchParameter|| + +### Join-OpenPackage +#### Joins Open Packages +Joins multiple open packages into a single open package +##### Parameters + +|Name|Type|Description| +|-|-|-| +|InputObject|PSObject|| +|Include|String[]|Includes the specified parts.

    Enter a wildcard pattern, such as `*.txt`

    Wildcards are permitted.| +|Exclude|String[]|Excludes the specified parts.

    Enter a wildcard pattern, such as `*.txt`

    Wildcards are permitted.| +|IncludeContentType|String[]|Includes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|ExcludeContentType|String[]|Excludes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|Force|SwitchParameter|| + +### Lock-OpenPackage +#### Locks an Open Package +Locks an Open Package. + +Closes the package and copies it into a read-only package. +##### Examples +###### Example 1 +Import OP, make it a package, and lock it +~~~PowerShell +Import-Module OP -PassThru | + Get-OpenPackage | + Lock-OpenPackage +~~~ +###### Example 2 +Import OP, make it a package, and lock it +~~~PowerShell +impo OP -PassThru | op | lkop +~~~ +##### Parameters + +|Name|Type|Description| +|-|-|-| +|InputObject|PSObject|The input object. This should be a package.| + +### New-OpenPackage +#### Creates an empty open package +Creates an new empty open package +### Publish-OpenPackage +#### Publishes Open Package +Publishes Open Packages using any `OpenPackage.Publisher` or command. +##### Parameters + +|Name|Type|Description| +|-|-|-| +|Publisher|PSObject[]|The name of the Publisher, or a command or script block used to Publish.
    One or more publishers may be provided. They will be processed in the order provided.| +|ArgumentList|PSObject[]|The Any Positional arguments for the view| +|InputObject|PSObject[]|Any input objects.| +|Option|IDictionary|Any options to pass to the View| +|WhatIf|SwitchParameter|| +|Confirm|SwitchParameter|| + +### Read-OpenPackage +#### Reads Open Package Bytes +Reads Bytes within an Open Package +##### Examples +###### Example 1 +~~~PowerShell +$package = OP @{"hello.txt" = "Hello world"} +$package | + Read-OpenPackage -Uri /hello.txt +~~~ +###### Example 2 +~~~PowerShell +$package = OP @{"hello.txt" = "Hello world"} +($package | + Read-OpenPackage -Uri /hello.txt -RangeStart 0 -RangeEnd 5) -as 'char[]' +~~~ +###### Example 3 +~~~PowerShell +$package = OP @{"hello.txt" = "Hello world"} +($package | + Read-OpenPackage -Uri /hello.txt -RangeStart 0 -RangeEnd 5) -as 'char[]' -join '' +~~~ +##### Parameters + +|Name|Type|Description| +|-|-|-| +|Uri|Uri[]|One or more part uris| +|RangeStart|Int64|A start range| +|RangeEnd|Int64|An ending range| +|InputObject|PSObject|The input object.
    If this is not a package, it will be passed thru.| + +### Remove-OpenPackage +#### Removes parts from an open package +Removes content parts from an open package. +##### Examples +###### Example 1 +~~~PowerShell +Get-OpenPackage @{ + "a.html" = "

    a html file

    " +} | + Remove-OpenPackage -Uri '/a.html' +~~~ +###### Example 2 +~~~PowerShell +Get-OpenPackage @{ + "a.html" = "

    a html file

    " + "a.css" = "body { max-width: 100vw; height: 100vh}" +} | + Select-OpenPackage -Include *.html | + Remove-OpenPackage +~~~ +##### Parameters + +|Name|Type|Description| +|-|-|-| +|Uri|Uri[]|One or more URIs to remove| +|InputObject|PSObject|The input object.
    If this is not a package, the input will be passed thru and nothing will be removed.| +|WhatIf|SwitchParameter|| +|Confirm|SwitchParameter|| + +### Select-OpenPackage +#### Selects Open Package content +Selects content from an Open Package using Regular Expressions or XPath +##### Parameters + +|Name|Type|Description| +|-|-|-| +|Pattern|PSObject[]|A list of patterns to match.| +|SimpleMatch|SwitchParameter|Indicates that the cmdlet uses a simple match rather than a regular expression match.| +|CaseSensitive|SwitchParameter|Indicates that the cmdlet matches are case-sensitive. By default, pattern matches aren't case-sensitive.| +|Quiet|SwitchParameter|Indicates that the cmdlet returns a simple response instead of a `[MatchInfo]` object.
    The returned value is `$true` if the pattern is found or `$null` if the pattern is not found.| +|List|SwitchParameter|Only the first instance of matching text is returned from each input file.
    This is the most efficient way to retrieve a list of files that have contents matching the regular expression.| +|NoEmphasis|SwitchParameter|By default, `Select-String` highlights the string that matches the pattern you searched for with the
    `-Pattern` parameter. The `-NoEmphasis` parameter disables the highlighting.| +|Include|String[]|Includes the specified parts.

    Enter a wildcard pattern, such as `*.txt`

    Wildcards are permitted.| +|Exclude|String[]|Excludes the specified parts.

    Enter a wildcard pattern, such as `*.txt`

    Wildcards are permitted.| +|IncludeContentType|String[]|Includes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|ExcludeContentType|String[]|Excludes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|NotMatch|SwitchParameter|The `-NotMatch` parameter finds text that doesn't match the specified pattern.| +|AllMatches|SwitchParameter|Indicates that the cmdlet searches for more than one match in each line of text.
    Without this parameter, `Select-String` finds only the first match in each line of text.

    When `Select-String` finds more than one match in a line of text, it still emits only one
    `[MatchInfo]` object for the line, but the `.Matches` property of the object contains all the
    matches.| +|Context|Int32[]|Captures the specified number of lines before and after the line that matches the pattern.

    If you enter one number as the value of this parameter, that number determines the number of lines
    captured before and after the match. If you enter two numbers as the value, the first number
    determines the number of lines before the match and the second number determines the number of lines
    after the match. For example, `-Context 2,3`.| +|Raw|SwitchParameter|Causes the cmdlet to output only the matching strings, rather than **MatchInfo** objects. This is
    the results in behavior that's the most similar to the Unix **grep** or Windows **findstr.exe**
    commands.| +|XPath|PSObject|Specifies an XPath search query. The query language is case-sensitive.| +|Namespace|IDictionary|Specifies a hash table of the namespaces used in the XML.

    Use the format`@{ = }`.

    When the XML uses the default namespace, which begins with xmlns, use an arbitrary key for the
    namespace name. You cannot use xmlns. In the XPath statement, prefix each node name with the
    namespace name and a colon, such as `//namespaceName:Node`.| +|AstCondition|ScriptBlock[]|One or more Abstract Syntax Tree conditions.
    These will select elements in any PowerShell scripts that match the condition.| +|InputObject|PSObject|The input object. This should be a package.| + +### Set-OpenPackage +#### Sets Open Package content +Sets content in an Open Packaging Conventions archive. +##### Examples +###### Example 1 +~~~PowerShell +$miniServer = Get-OpenPackage | + Set-OpenPackage -Uri '/index.html' -Content ([xml]"

    Hello World

    ") -ContentType text/html | + Start-OpenPackage +Start-Process -FilePath $miniServer.Name +~~~ +##### Parameters + +|Name|Type|Description| +|-|-|-| +|Uri|Uri|The uri to set| +|Content|PSObject|The content to set.| +|ContentType|String|The content type. By default, `text/plain`| +|Depth|Int32|The serialization depth.| +|Option|IDictionary|The options used to write the content.| +|InputObject|PSObject|The input object.
    This must be a package, and it must be writeable.| +|Force|SwitchParameter|Sets a part, even if it already exists.| + +### Split-OpenPackage +#### Splits Open Packages +Splits Open Packages into multiple parts +##### Examples +###### Example 1 +~~~PowerShell +Get-Module OP | + OP | + Split-OpenPackage +~~~ +##### Parameters + +|Name|Type|Description| +|-|-|-| +|Property|PSObject[]|One or more properties to group.| +|Include|String[]|Includes the specified parts.

    Enter a wildcard pattern, such as `*.txt`

    Wildcards are permitted.| +|Exclude|String[]|Excludes the specified parts.

    Enter a wildcard pattern, such as `*.txt`

    Wildcards are permitted.| +|IncludeContentType|String[]|Includes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|ExcludeContentType|String[]|Excludes the specified content types.

    Enter a wildcard pattern, such as `text/*`| +|InputObject|PSObject|| + +### Start-OpenPackage +#### Starts a OpenPackage Server +Starts a server, using one or more archive packages as the storage. +##### Parameters + +|Name|Type|Description| +|-|-|-| +|ArgumentList|PSObject[]|The path to an Open Package file, or a glob that matches multiple Open Package files.| +|RootUrl|String|The root url.
    By default, this will be automatically to a random local port.
    If running elevated, can be any valid http listener prefix, including `http://*/`| +|InputObject|PSObject[]|The input object. This can be provided to avoid loading a file from disk.| +|Allow|String[]|The allowed http verbs.| +|TypeMap|IDictionary|The content type map| +|Invokable|SwitchParameter|If set, the scripts in the package will be invokable.
    This turns allows every PowerShell script in the package into server side code.
    This should be used cautiously, and only with known packages.| +|ThrottleLimit|UInt16|The throttle limit.
    This is the number of concurrent jobs that can be running at once.| +|BufferSize|UInt32|The buffer size.
    If parts are smaller than this size, they will be streamed.
    If parts are larger than this size, they will be handled in the background
    (and may use a buffer of this size when accepting range requests)| +|Lifespan|TimeSpan|The lifespan of the server.
    If provided, will automatically stop the server after it's life is over.| +|NodeCount|Byte|The number of nodes to run.
    Each node can handle incoming requests.| + +### Uninstall-OpenPackage +#### Uninstalls OpenPackages +Uninstalls one or more OpenPackages. +##### Parameters + +|Name|Type|Description| +|-|-|-| +|Identifier|String[]|The package identifier.
    If this is a fully qualified directory or file name, it will be removed.| +|Version|String[]|The package version| +|PackagePath|String[]|The direct path to any number of packages.
    This will Remove-Item these paths| +|PackageRoot|String[]|The root location where packages are stored.
    By default, this will be the locations specified in `$env:OpenPackagePath`| +|WhatIf|SwitchParameter|| +|Confirm|SwitchParameter|| + +### Write-OpenPackage +#### Writes bytes to an Open Package +Writes bytes directly to an Open Package part. The part must already exist. + +This can be used to rapidly update small segments of a package. + +It can also corrupt package contents, and should be used with care. + +To set package parts, use Set-OpenPackage +##### Examples +###### Example 1 +~~~PowerShell +$package = OP @{"hello.txt" = "Hello world"} +$package | + Write-OpenPackage -Uri /hello.txt -Buffer ($outputEncoding.GetBytes("y")) | + Get-OpenPackage ./hello.txt +~~~ +##### Parameters + +|Name|Type|Description| +|-|-|-| +|Uri|Uri|The Package Part Uri. This is the path to the content within a package.| +|Content|PSObject|The content to write.| +|Depth|Int32|The serialization depth.| +|Option|IDictionary|The options used to write the content.| +|RangeStart|Int64|The starting location for the write.| +|InputObject|PSObject|The package.| + +> 2025-2026 Start-Automating + +> [LICENSE](https://github.com/PoshWeb/OP/tree/main/LICENSE) diff --git a/README.md.ps1 b/README.md.ps1 new file mode 100644 index 0000000..2a709bd --- /dev/null +++ b/README.md.ps1 @@ -0,0 +1,229 @@ +<# +.SYNOPSIS + README.md.ps1 +.DESCRIPTION + README.md.ps1 makes README.md + + This is a simple and helpful scripting convention for writing READMEs. + + `./README.md.ps1 > ./README.md` + + Feel free to copy and paste this code. + + Please document your parameters, and add NOTES. +.NOTES + This README.md.ps1 is used to generate help for a module. + + It: + + * Outputs the name and description + * Provides installation instructions + * Lists commands + * Lists parameters + * Lists examples +.EXAMPLE + ./README.md.ps1 > ./README.md +.EXAMPLE + Get-Help ./README.md.ps1 +#> +param( +# The name of the module +[string]$ModuleName = $($PSScriptRoot | Split-Path -Leaf), + +# The domains that serve git repositories. +# If the project uri links to this domain, +# installation instructions will show how to import the module locally. +[string[]] +$GitDomains = @( + 'github.com', 'tangled.org', 'tangled.sh', 'codeberg.org' +), + +# If set, we don't need no badges. +[switch] +$NoBadge, + +# If set, will not display gallery instructions or badges +[switch] +$NotOnGallery +) + +Push-Location $PSScriptRoot + +# Import the module +$module = Import-Module "./$ModuleName.psd1" -PassThru + +# And output a header +"# $module" + +if (-not $NoBadge) { + # If it is on the gallery, show the downloads badge. + if (-not $NotOnGallery) { + @( + "[!" + "[$ModuleName](https://img.shields.io/powershellgallery/dt/$ModuleName)" + "](https://www.powershellgallery.com/packages/$ModuleName/)" + ) -join '' + } +} + +# Show the module description +"## $($module.Description)" + +# Show any intro section defined in the manifest +$module.PrivateData.PSData.PSIntro + +#region Boilerplate installation instructions +if (-not $NotOnGallery) { +@" + +## Installing and Importing + +You can install $ModuleName from the [PowerShell gallery](https://powershellgallery.com/) + +~~~PowerShell +Install-Module $($ModuleName) -Scope CurrentUser -Force +~~~ + +Once installed, you can import the module with: + +~~~PowerShell +Import-Module $ModuleName -PassThru +~~~ + +"@ +} +#endregion Gallery installation instructions + +#region Git installation instructions +$projectUri = $module.PrivateData.PSData.ProjectURI -as [uri] + +if ($projectUri.DnsSafeHost -in $GitDomains) { +@" + +You can also clone the repo and import the module locally: + +~~~PowerShell +git clone $projectUri +cd ./$ModuleName +Import-Module ./ -PassThru +~~~ + +"@ +} +#endregion Git installation instructions + +#region Exported Functions +$exportedFunctions = $module.ExportedFunctions +if ($exportedFunctions) { + + "## Functions" + + "$($ModuleName) has $($exportedFunctions.Count) function$( + if ($exportedFunctions.Count -gt 1) { "s"} + )" + + foreach ($export in $exportedFunctions.Keys | Sort-Object) { + # Get help if it there is help to get + $help = Get-Help $export + # If the help is a string, + if ($help -is [string]) { + # make it preformatted text + "~~~" + "$export" + "~~~" + } else { + # Otherwise, add list the export + "### $($export)" + + # And make it's synopsis a header + "#### $($help.SYNOPSIS)" + + # put the description below that + "$($help.Description.text -join [Environment]::NewLine)" + + if ($help.examples.example) { + # Show our examples + "##### Examples" + } + + + $exampleNumber = 0 + foreach ($example in $help.examples.example) { + $markdownLines = @() + $exampleNumber++ + $nonCommentLine = $false + "###### Example $exampleNumber" + + # Combine the code and remarks + $exampleLines = + @( + $example.Code + foreach ($remark in $example.Remarks.text) { + if (-not $remark) { continue } + $remark + } + ) -join ([Environment]::NewLine) -split '(?>\r\n|\n)' # and split into lines + + # Go thru each line in the example as part of a loop + $codeBlock = @(foreach ($exampleLine in $exampleLines) { + # Any comments until the first uncommentedLine are markdown + if ($exampleLine -match '^\#' -and -not $nonCommentLine) { + $markdownLines += $exampleLine -replace '^\#\s{0,1}' + } else { + $nonCommentLine = $true + $exampleLine + } + }) -join [Environment]::NewLine + + $markdownLines + "~~~PowerShell" + $CodeBlock + "~~~" + } + + $relatedUris = foreach ($link in $help.relatedLinks.navigationLink) { + if ($link.uri) { + $link.uri + } + } + if ($relatedUris) { + "#### Links" + foreach ($related in $relatedUris) { + "* [$related]($related)" + } + } + + # Make a table of parameters + if ($help.parameters.parameter) { + "##### Parameters" + + "" + + "|Name|Type|Description|" + "|-|-|-|" + foreach ($parameter in $help.Parameters.Parameter) { + "|$($parameter.Name)|$($parameter.type.name)|$( + $parameter.description.text -replace '(?>\r\n|\n)', '
    ' + )|" + } + + "" + } + + } + } +} +#endregion Exported Functions + +#region Copyright Notice +if ($module.Copyright) { + "> $($module.Copyright)" +} + +if ($module.PrivateData.PSData.LicenseUri) { + "" + "> [LICENSE]($($module.PrivateData.PSData.LicenseUri))" +} +#endregion Copyright Notice + +Pop-Location \ No newline at end of file From 2aac39dd0dbe07c578a66c0e1ed246cde4fdd045 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 12:03:46 -0700 Subject: [PATCH 655/724] feat: `OpenPackage.View.Tree.html` ( Fixes #122 ) Putting nested branches in list item --- Types/OpenPackage.View/Tree.html.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.View/Tree.html.ps1 b/Types/OpenPackage.View/Tree.html.ps1 index ee6dcfb..1a119fd 100644 --- a/Types/OpenPackage.View/Tree.html.ps1 +++ b/Types/OpenPackage.View/Tree.html.ps1 @@ -41,10 +41,10 @@ filter toIndex { } elseif ($in.Key -is [string]) { if ($in.Value -is [Collections.IDictionary]) { - "
    " + "
  • " "$([Web.HttpUtility]::HtmlEncode($in.Key))" $in.Value | toIndex - "
    " + "
  • " } else { "
  • " " Date: Wed, 8 Apr 2026 19:38:11 +0000 Subject: [PATCH 658/724] feat: `OpenPackage.Source.Directory` ( Fixes #116 ) Adding inner docs --- OP.types.ps1xml | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 46a2375..da48d9d 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5771,22 +5771,28 @@ foreach ($resolvedItem in $resolvedItems) { # Try to get the git app $gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git','application') - - if ($gitApp -and # If git is loaded + + # Then see if get can git it. + $canGitIt = $gitApp -and # If git is loaded (Test-Path ( # and a .git directory exists Join-Path $resolvedItem.FullName '.git' )) - ) { + + # If we can git it. + if ($canGitIt) { # get it's first remote $gitRemote = @(& $gitApp '-C' $resolvedItem.FullName remote)[0] | ForEach-Object { & $gitApp '-C' $resolvedItem.FullName remote get-url $_ } + # and create a relationship to the repository $relation = $package.Relate($gitRemote,'git','repository') - Write-Verbose "Related $( - $relation.TargetUri - ) as [$($relation.RelationshipType)]$($relation.id)" + if ($VerbosePreference -notin 'silentlyContinue', 'ignore') { + Write-Verbose "Related $( + $relation.TargetUri + ) as [$($relation.RelationshipType)]$($relation.id)" + } } $sourceDirectoryUri = @@ -5797,10 +5803,9 @@ foreach ($resolvedItem in $resolvedItems) { $relation.TargetUri ) as [$($relation.RelationshipType)]$($relation.id)" - # So declare an oldest created file and newest write time. $oldestCreationTime = [DateTime]::Now - $lastWriteTime = [DateTime]::MinValue + $lastWriteTime = [DateTime]::MinValue #region Filter Filters $filteredFiles = @( @@ -5809,32 +5814,38 @@ foreach ($resolvedItem in $resolvedItems) { # (deny before approve) :filterFiles foreach ($file in $filesToArchive) { $relativePath = $file.FullName.Substring($resolvedItem.FullName.Length) + # If we have not excplitly included `.git`, if (-not $includeGit -and $relativePath -match '[\\/].git[\\/]') { - continue + continue # exclude `.git`. } - if (-not $IncludeNodeModule -and $relativePath -match '[\\/]node_modules[\\/]') { - continue + # If we have not explicitly included `node_modules` + if (-not $IncludeNodeModule -and $relativePath -match '[\\/]node_modules[\\/]') { + continue # exclude `node_modules`. } + # If we have not explicitly included `_site` if (-not $IncludeSite -and $relativePath -match '[\\/]_site[\\/]') { - continue + continue # exclude `_site`. } - if ($exclude) { + + # If we have any exclusion wildcards + if ($exclude) { foreach ($exclusion in $Exclude) { if ($file.FullName -like $exclusion) { - continue filterFiles + continue filterFiles # exclude any files that match } } } + # If we have any include wildcards if ($include) { $included = $false foreach ($inclusion in $include) { if ($file.FullName -like $inclusion) { - $included = $true + $included = $true # only include things that match. break } } - + if (-not $included) { continue filterFiles } } @@ -5878,7 +5889,7 @@ foreach ($resolvedItem in $resolvedItems) { } if ($file.LastWriteTime -gt $lastWriteTime) { $lastWriteTime = $file.LastWriteTime - } + } # Write our progress message $progress.PercentComplete = (++$counter * 100 / $total) From 92528f698e1857da47509ee6c3b22ba877885f9c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 12:45:41 -0700 Subject: [PATCH 659/724] feat: `OpenPackage.Part.get_Extension` ( Fixes #195 ) --- Types/OpenPackage.Part/get_Extension.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Types/OpenPackage.Part/get_Extension.ps1 diff --git a/Types/OpenPackage.Part/get_Extension.ps1 b/Types/OpenPackage.Part/get_Extension.ps1 new file mode 100644 index 0000000..059997e --- /dev/null +++ b/Types/OpenPackage.Part/get_Extension.ps1 @@ -0,0 +1,10 @@ +<# +.SYNOPSIS + Gets a part extensions +.DESCRIPTION + Gets the file extension of a package part. + + This is anything after the last `.` in the part uri. +#> +if (-not $this.Uri) { return } +@($this.Uri -split '\.')[-1] \ No newline at end of file From 82fcfa7449bf8b07877d1df3aa9d123b4530fb04 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 12:49:42 -0700 Subject: [PATCH 660/724] feat: `OpenPackage.Part.get_Extension` ( Fixes #195 ) --- Types/OpenPackage.Part/get_Extension.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Types/OpenPackage.Part/get_Extension.ps1 b/Types/OpenPackage.Part/get_Extension.ps1 index 059997e..68fe98a 100644 --- a/Types/OpenPackage.Part/get_Extension.ps1 +++ b/Types/OpenPackage.Part/get_Extension.ps1 @@ -6,5 +6,10 @@ This is anything after the last `.` in the part uri. #> +param() + +# If there is no uri, there is no extension if (-not $this.Uri) { return } -@($this.Uri -split '\.')[-1] \ No newline at end of file + +# Split by periods, ignore any blanks, and return the last segment. +@($this.Uri -split '\.' -ne '')[-1] \ No newline at end of file From a6f074bc0ae9e5c2ad40e554cfc8680b644de783 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 19:50:01 +0000 Subject: [PATCH 661/724] feat: `OpenPackage.Part.get_Extension` ( Fixes #195 ) --- OP.types.ps1xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index da48d9d..f5dad50 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4081,6 +4081,26 @@ foreach ($segment in $this.Uri -split '/' -ne '') { + + Extension + + <# +.SYNOPSIS + Gets a part extensions +.DESCRIPTION + Gets the file extension of a package part. + + This is anything after the last `.` in the part uri. +#> +param() + +# If there is no uri, there is no extension +if (-not $this.Uri) { return } + +# Split by periods, ignore any blanks, and return the last segment. +@($this.Uri -split '\.' -ne '')[-1] + + Hash From 16a328971872fb6b3b02f4ec58ce1fd5885da1f5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 12:51:25 -0700 Subject: [PATCH 662/724] feat: `OpenPackage.Part.ReadMarkdown` ( Fixes #105 ) Reading front matter --- Types/OpenPackage.Part/ReadMarkdown.ps1 | 55 +++++++++++++++++++++---- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/Types/OpenPackage.Part/ReadMarkdown.ps1 b/Types/OpenPackage.Part/ReadMarkdown.ps1 index 83e3c15..91d4a27 100644 --- a/Types/OpenPackage.Part/ReadMarkdown.ps1 +++ b/Types/OpenPackage.Part/ReadMarkdown.ps1 @@ -25,9 +25,15 @@ param( [Alias('Options')] [Collections.IDictionary]$Option = [Ordered]@{} ) -if (-not $this.ReadText) { return } -$partString = $this.ReadText($InputObject, $Option) +if (-not $inputObject) { + if (-not $this.ReadText) { return } + + $partString = $this.ReadText($InputObject, $Option) +} else { + $partString = "$inputObject" +} + if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { Write-Warning "Markdig not loaded (ConvertFrom-Markdown is not installed)" @@ -38,15 +44,46 @@ if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { } $mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new() -$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build() +$mdPipeline = [Markdig.MarkdownExtensions]::UseYamlFrontMatter( + [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder) +).Build() try { - [PSCustomObject]@{ - PSTypeName = 'text/markdown' - Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) - Markdown = "$partString" - } | - Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru + if ($partString -match '^---') { + $null, $yamlheader, $md = $partString -split '---', 3 + + $convertFromYamlCommand = + $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') + if (-not $convertFromYamlCommand -or -not + $convertFromYamlCommand.Parameters.InputObject + ) { + Write-Warning "Convert-FromYaml not found, please install YaYaml to read Yaml Header" + [PSCustomObject]@{ + PSTypeName = 'text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$partString" + } | + Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru + } else { + $yamlObject = & $convertFromYamlCommand -InputObject $yamlheader + if ($yamlObject) { + [PSCustomObject]@{ + PSTypeName='text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$md" + FrontMatter = $yamlObject + } | + Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru + } + } + } else { + [PSCustomObject]@{ + PSTypeName = 'text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$partString" + } | + Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru + } } catch { Write-Warning "'$($this.Uri)' was not valid markdown: $_" } From a3e11471de4119ce4395db3694f243f32a72d7c2 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 19:51:43 +0000 Subject: [PATCH 663/724] feat: `OpenPackage.Part.ReadMarkdown` ( Fixes #105 ) Reading front matter --- OP.types.ps1xml | 55 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f5dad50..711bcf5 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2913,9 +2913,15 @@ param( [Alias('Options')] [Collections.IDictionary]$Option = [Ordered]@{} ) -if (-not $this.ReadText) { return } -$partString = $this.ReadText($InputObject, $Option) +if (-not $inputObject) { + if (-not $this.ReadText) { return } + + $partString = $this.ReadText($InputObject, $Option) +} else { + $partString = "$inputObject" +} + if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { Write-Warning "Markdig not loaded (ConvertFrom-Markdown is not installed)" @@ -2926,15 +2932,46 @@ if (-not ('Markdig.MarkdownPipelineBuilder' -as [type])) { } $mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new() -$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build() +$mdPipeline = [Markdig.MarkdownExtensions]::UseYamlFrontMatter( + [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder) +).Build() try { - [PSCustomObject]@{ - PSTypeName = 'text/markdown' - Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) - Markdown = "$partString" - } | - Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru + if ($partString -match '^---') { + $null, $yamlheader, $md = $partString -split '---', 3 + + $convertFromYamlCommand = + $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Cmdlet,Function') + if (-not $convertFromYamlCommand -or -not + $convertFromYamlCommand.Parameters.InputObject + ) { + Write-Warning "Convert-FromYaml not found, please install YaYaml to read Yaml Header" + [PSCustomObject]@{ + PSTypeName = 'text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$partString" + } | + Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru + } else { + $yamlObject = & $convertFromYamlCommand -InputObject $yamlheader + if ($yamlObject) { + [PSCustomObject]@{ + PSTypeName='text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$md" + FrontMatter = $yamlObject + } | + Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru + } + } + } else { + [PSCustomObject]@{ + PSTypeName = 'text/markdown' + Html = [Markdig.Markdown]::ToHtml($partString, $mdPipeline) + Markdown = "$partString" + } | + Add-Member ScriptMethod ToString { "$($this.Html)" } -Force -PassThru + } } catch { Write-Warning "'$($this.Uri)' was not valid markdown: $_" } From 82e8cdcf8fb7856395334e526a7eee58d935b563 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Wed, 8 Apr 2026 15:34:14 -0700 Subject: [PATCH 664/724] feat: OP Logos ( Fixes #196 ) --- Assets/OP-Animated-Blue-Text.svg | 14 ++++ Assets/OP-Animated-Blue.svg | 6 ++ Assets/OP-Animated-Gradient-Text.svg | 21 ++++++ Assets/OP-Animated-Gradient.svg | 13 ++++ Assets/OP-Animated-Text.svg | 14 ++++ Assets/OP-Animated.svg | 6 ++ Assets/OP-Blue-Text.png | Bin 0 -> 159690 bytes Assets/OP-Blue-Text.svg | 13 ++++ Assets/OP-Blue.svg | 5 ++ Assets/OP-Gradient-Text.png | Bin 0 -> 266769 bytes Assets/OP-Gradient-Text.svg | 20 ++++++ Assets/OP-Gradient.svg | 12 ++++ Assets/OP-Text.png | Bin 0 -> 146316 bytes Assets/OP-Text.svg | 13 ++++ Assets/OP.png | Bin 0 -> 134267 bytes Assets/OP.svg | 5 ++ OP.turtle.ps1 | 97 +++++++++++++++++++++++++++ 17 files changed, 239 insertions(+) create mode 100644 Assets/OP-Animated-Blue-Text.svg create mode 100644 Assets/OP-Animated-Blue.svg create mode 100644 Assets/OP-Animated-Gradient-Text.svg create mode 100644 Assets/OP-Animated-Gradient.svg create mode 100644 Assets/OP-Animated-Text.svg create mode 100644 Assets/OP-Animated.svg create mode 100644 Assets/OP-Blue-Text.png create mode 100644 Assets/OP-Blue-Text.svg create mode 100644 Assets/OP-Blue.svg create mode 100644 Assets/OP-Gradient-Text.png create mode 100644 Assets/OP-Gradient-Text.svg create mode 100644 Assets/OP-Gradient.svg create mode 100644 Assets/OP-Text.png create mode 100644 Assets/OP-Text.svg create mode 100644 Assets/OP.png create mode 100644 Assets/OP.svg create mode 100644 OP.turtle.ps1 diff --git a/Assets/OP-Animated-Blue-Text.svg b/Assets/OP-Animated-Blue-Text.svg new file mode 100644 index 0000000..689d0b1 --- /dev/null +++ b/Assets/OP-Animated-Blue-Text.svg @@ -0,0 +1,14 @@ + + + OP-Animated-Blue-Text + + + + + + + OP +OP + + + \ No newline at end of file diff --git a/Assets/OP-Animated-Blue.svg b/Assets/OP-Animated-Blue.svg new file mode 100644 index 0000000..005432d --- /dev/null +++ b/Assets/OP-Animated-Blue.svg @@ -0,0 +1,6 @@ + + + OP-Animated-Blue + + + \ No newline at end of file diff --git a/Assets/OP-Animated-Gradient-Text.svg b/Assets/OP-Animated-Gradient-Text.svg new file mode 100644 index 0000000..b5fce9c --- /dev/null +++ b/Assets/OP-Animated-Gradient-Text.svg @@ -0,0 +1,21 @@ + + + + + + + + + + OP-Animated-Gradient-Text + + + + + + + OP +OP + + + \ No newline at end of file diff --git a/Assets/OP-Animated-Gradient.svg b/Assets/OP-Animated-Gradient.svg new file mode 100644 index 0000000..8613730 --- /dev/null +++ b/Assets/OP-Animated-Gradient.svg @@ -0,0 +1,13 @@ + + + + + + + + + + OP-Animated-Gradient + + + \ No newline at end of file diff --git a/Assets/OP-Animated-Text.svg b/Assets/OP-Animated-Text.svg new file mode 100644 index 0000000..0962247 --- /dev/null +++ b/Assets/OP-Animated-Text.svg @@ -0,0 +1,14 @@ + + + OP-Animated-Text + + + + + + + OP +OP + + + \ No newline at end of file diff --git a/Assets/OP-Animated.svg b/Assets/OP-Animated.svg new file mode 100644 index 0000000..753377e --- /dev/null +++ b/Assets/OP-Animated.svg @@ -0,0 +1,6 @@ + + + OP-Animated + + + \ No newline at end of file diff --git a/Assets/OP-Blue-Text.png b/Assets/OP-Blue-Text.png new file mode 100644 index 0000000000000000000000000000000000000000..52f9db1747d11a52750a2222ef3f4081e4097deb GIT binary patch literal 159690 zcma%E30zEF8#iNy7E8MjBHC#}RH%6^l`N%{HZ}Aj+1j*Fnrlr`SxN{)me5LwQrB8Y zWy=yZWXakhWU0RA&Ryotn0mk8`}@A%nC?C2InVQd*7KY*TODWH_Es36(5+jy-qY=- zI(6$N7l8dwt_OVMQ0BR`TQ{X{)2IG3H=Ob5#f#Wu@7;R{v)AZuoy^tM9vt&!V2t*9 zVN?FQ_z9}y^;K!V#DE|MJN{NbzexJCdCCEbk}qo~nrIJYFekCq z%-4J<*iVgv+B46nP*dO^elk_Cuh6A1@`I2XJ7RwAN)I%OP7pl9E#hw!eZ_Nuhh4^- zj2BJo=FIh{(*ujp_Tf$@zCw316||hQ?p9)NRyv?}I~!Cq@iRsJfV}ZxIt=D%#Zm{7 z7J@0S740bU;45SUjNJ3e#MwL+V~m({63Y@V-2)z8_W5_PM{y?{pdN(}s`9DE0W)`3 zrb`ivPG_vrAw`5w#A;>_*P6yfe0R@E*+3s-D|56m@dznb5w9Jma*;H-v?=75Fh%+Z zp;spyd@_#R=Ma+NqY|)9*Jbg%VVoYTVRC`vK_nO4-1O;13P;Z$czGP{Q6aJ z0HfGdIdK=Aa-ayXmPmbtvO(O6;WhR!TQc*@u)i5FT>KSpK0LfS%;e`9ri!Yp^@|_n zqep?{U8Axpo~#@}`x=`VxJnyT1cWtmc_RZ_?CNKe+#o~$ zOJ3@Y>!e=Cv09|zC~SRpt${XBsZ!#4iU;@yf-Q4^>^UFRHj{r9r1E#sUX&DzIXqnV z>FRJCO5Aobu{%-MSX!EB@k$hbT{=W5&N|3g{!u#W2oc-hxWYc{IJ+$=3a5NqK`0UB6+(@25~ zoIU)dA{3J>C4hZhK|kPnmMe$0Hb7!e37IMp=8kn!%~7ZsY;v3eXo?H^Tw)rU4HK;A zGi}*(ZOH{@$%UK99u{Mv@FJgC60BT|bMC96ofHS<=(b5oSAZ&?cMOCV9c@s9&M3eU z88=*jn%*B*Hd%W*SA(`pY*v~X#rdKSBhQJu|IaaYsxAIHrum}3(Fq{Fid3;16%c7( zzeIXickVdpuY7o0H!5h&$Z)rl_)Gpsp}NEu1W|RMTye!ZswDS-TFxbYC$`Da9W37H zoI0_9KPfl%>Wta>dVmjyl4S(jkO6o^#=QK+3BFz zqj3)Mw_?DfzfSlUo-#Z@9csCe7$i- zpk4ms%ZJj}56hL|i<;S%BjyV0Sq&Z%((Wu2S^~zmJUyZ>9mT)o^Y_Xk31Ne4*6_Mi zN@1ppQ0T0@en8oI)32l++9+|l(#mJ{hYIayRzyr6R4592!@cApolN(=g%ZG$<2Yg@bCGF7+IJKBU_AbApF)Z zx%?!ORmAJGa4hF2JiK#RzkTqCkz(m=nyUWAKb{pZ`e*23n0k$3{-MqX6#;%bFk`@m zf_Sti!*rvYxO2xwn7xDlQ~w2~T?MoW_O z1Sbi(^Z*N`8TrpmYSNS`=x&K==n2nph_YtQUqXhxU57*g&cJHj?fr1UEN8y$%1qg* zI|}VyWO%{U<~qVjol$_xrI!IH&!rWBh z^p-6c55QjD&QNi@hwNb%$?$Y|IV%*N{!yecUB;V~zUj_NM@`?qikl_Dfhg8wX%J%51% z$5lO1TnfhxsaSwe;@Joz%a2`!Wq-&)<`>yHGhu*p4#LB0ChSR(a%})&S~*WdW6Dfe z-I-1O!+J5CDT73O5UVw*X30SD$aBTd-x&R;Qh}Zc+$OW*7w+yO<+M5}4+(bx(Rw|t z=|a(V<{HYJX&3-$Sd~7D!X~nYIlB{{AnibPTNsZF2Dz8`k)6m7jPx`xGu!wi=<<{T zWK}YVX?ZK);d4JsCc(d}loNMLv70VGK6)LfP&KKjXUXNM2MnYL$3M`5b&*|J14g%H zXIMilb2ObF*x&w^iZhIS;>nuJw=Q?OEQ&I25qbxb9g|s+QhPeGYeQGfVTJig3UtLr zJ~C%ZIRLx}!83-3bH6VbAQqVcGpG|(asPpb*Qdl&;LtY^8jD{Ptwe09g;;vKM4sFo ziD0U9;aDaEI3{Zt^uSpAP&vS`D_$eY@UYF3_5J0P>@*U)ssmxX-vh>Ol&U44yMpoV zzvO~`0I?WFGG$sIN&m?@uJOc0m|}VOrF054pE;Of5aoh~Ys~|tef}aPjuI<;I4hsB zhyHKipa_77JhL z4w!`&NH2ig{{hO<0Ml{jmA_(u;I8NQ>JqH65T~oX3DJa+?d8p-uu5}nI8%Ip=pwh) zSZ*?ZY?s4=g;#jGrU?uA^dLxtBWXj1h67ORKL7iDYgv~R;A>LT(9A*Sm>;gA7=nL5803Z&nI|_p(67Ubw$Va5^f7at>`e{C9l*Ev>23>ZXd5+- zsw7vauuaJilKy)epN=4ShIlfIx>#(uCs4gY#{p+%E_e%UdRAC8qf3f zD88&5`qG4~1rR)%g)*VPr>GHV3H$cbX$mbz%dp44vE*NIn8DHOkFD9#>bJd zL7*sS>?3sJl?h5{17L%*F81IL458c!jPYV}aM(#W4>vb3U@zZno6R$0M95wi zF$HWEodzY$*gqz$FXKNwQa5B2*o|3F=M#RET&7fMLL{-*in|@s(K$5_w~FD6Awjs0 zA4YTVgi@)fgoBxB>UDh;o!mriZQ1){?m&k7@7+{O2cK)ect=q~d>~kYa^d0bBd_Tb zbRfoz&UeQUCwTbS@B>8kXLrAVVG@v%L@H_v?mVH_IxHkb<}xl)NXI`&>dJ7iNW+)U zfsHMH1@o>_TMPv+AdtM2jdqg|s|-ntRB{>6{P@qa!s?Ndtd`|~Am2Fv`A%3sp_FPY zR^b@^z0?I+petWij-cC6bPl6JmISpq=>R3cHd7619K?c67#%TdNK|9B5C3#as%NI_ zkjMm{_D*aSw*gomeOUgJPmB+Ca& z7Hopx;Wx$yrV29n55!9n0gg@pE(bILp0=JmynR@w;SVlS?D52*0omw|9gYf8zM+$7 zSr6N?=S{bvhbQP0=5e~9Fg+9%#0BR;iYpe-Q#6T)ND{6>ka@}c!6uY5EE~p??&6{{ z(L^v}I|}C$)p10Jd62BYk?Ke*$@hZ7qUk69ULy1HRX#uf=*2C|o3)@go5iYx*ij+k(HU+M2IP(<2`D=b4b^Byz&w4@{>SkKpy z@wz8uYIzSs`%y}OeTY?wD=VH1swUd{RQq8z9@!wv8N647zj_V>K1`@ zM2wM3*+Hxbf0cN&P823yS&j60^|k6W0@#c)n&Osi~1&MnghAC?mr-L8Oex4p!OUS%TO?lr>SCY2mlv0AEt36t^oK zApAabc|Rp!SJlK$W8YM0NHmv!w}QmxVCEOPg~32Bwq;fDuv>706XpofOU#`z9U{Sv znE)lzm-pi!Ku3z0D`}LAcp)AXLN@8fe#(kH(?mB zkCb(yEHpucl6KBc^b|WjDb!fX&%3#BSN`>~Hbm66Wrl1CHA0%1vt_Y_vl+BXYbQXty6Cqj*?bw;++w6A=(^vYK#32aM5A41Vozd{ zprDy;s5@7f9`n&p6379aRE1VT?f3c4rYQQ;WqLel{gP05T!|-x04Mh|qJ`DL%$m*$ zD>@K<#O14xA(d~j21ncjhvP+@Y&(L|q?w=Wj@w7%G_$6Q;+u9On5L4)gYmjwf2I>D zW0GWvWF-c$5#wt z1SJv#;F9^tT&rS;Rf42Cm^%rF6d_47&iPlFau~3-03YOr1gowRl#lUQ0VZrh#k{!hoL6#DP)yf4PS41!7 z^TWE7izc*+_?BHxiS35Cv&+P4j@uhDIV0AKD1bFkcN{cb7|R!x9_TjYFK#36;OUBK zZ#yotWb#XbE^Z2H#E#F|_)6ki7(ZLCRg82BurFuA*ke}Q$)Jy{IUO~o{~JV(M`UB6 z(&T6Jc^Hkq2NhefZAlWV*as4YpWx{y?_L{_e??5$Gdoi+Rx%`BZVT^DCdAxzJf!IZ z%>FZIuPD5?>Bkw;2?Zh^Ah%(ONys2jL1Ys1poHgz_r#V|v<7GU26> zL8JakMYW+Iq!WIz3YN+w$gH=>Py8f_uu@2L8NX=pH)m z;aJfvnKYSWEuGKHCeXxCZzK#QIZv>Mh+T}K^FH_aZ8jBxKN;6ULV%>WfqlGDz>bUG z7ZDxaJT*qJG_YuR9RNmh?DbGQB?~tcE|X0FF~VA~zu)a6Fb7&EpAz#o3+UO~G>ED- z-1Aac-u)WqcR4I9CFLK$$L5jSC#K14FOysW@`3q6QR|WFB_cE+T#uYm{U{!BjEwpV zaPTT#7All1H9eEd-y~xJ=zYN6jBKB9F~vsmrL_GX(L1__XMuz-&Yenw@YQ1py9*%d6f1nZs)D(+OQ*_ zvuGFIPl60!mdnb_NCGt{gQ1Br)5JRZFEV#_MgT6?fccQ*Tso`@p|_DRq-z1n8Dj@e z{@C)^-TMEjZQkjW% zSDRv7?6km{gQBDf8O@yCwaXW7g_uyamye2e;&5%*XUN5@3IZerg&Is>%6Qkcu(5FA zA9&bs)YW_9*F|fk)dxw1=N56dfmlZFmTrk(sTeNPuj~hjuTqJi9252-O)*8LyMX+S z9pBRX4lSNe;4dNBP7p`NHatIg`0!(2a{^1~M5b=%CBqM3XC9tiJ(>Wx%%g{%rUi@0 zS^$1>9wT-<@Mb02Iu3iYOrO`4M`AGd!fN-fO@i3^EVD+u41D#{u{8rsLl3Xm9d9)_ zokgZ%3!@K5Y9_&72o?Llc{lUxZx%=@8Yh--A%mUsKzH1hR!dwqAUw&> zmZ{GvHURK_F8#&LebA?(D;+8lz5^Kt3|W!-f^v3^m-r20rNiwfY5f??3Bc=6?TC%I z!O(O^mv@dxzyUk{&!8Ei7KT;w;i9wgw8c@L4&xyz1UfdDl!k*0hw|(tx$;*B^h2oa zrJww`ocWtQv8z2<+_qlPmIB!dLe%w0o`azWIv^j2F)S@^c-Id~^7rhYO@fc%2EqxQ4=bAb3D#Y* z?bZ~QjXFnie?^?kQPX8WyL9f=<3}oI%JqjC&h3Fe*Nuzvk>5dpLjR4Ah z5g*|71z2UMs9mCzi*|MtF#v-6I_AL(G~8D@q4Q<0)eUQ9eDPuixrNo$QAEy3a0o}Y zjS<-<2wQE@Oxc5Cj7J883QT(VLImjlI*KUFfsq%CUIj05S22Ec0wRJRIs4#Y|ABk< z{69w#d4|BblT~KrIkI+Wq(hiz@-2n|c{A#Dr8KsaO&dk9@@nftBuhOHl>*39(4>{b z0WbuBYNg*wtAld0_?_&L$w6!_=XOMn;%`?+5!b+}pq3gwvA5V+(>-oI<;_ELYh4aEthTMpe6urh8)Gnb>CQ=MTMRciD80t29>ttys20ez>ekl995| z(p%Bv=0iE|NZr)QqUL*I01<0-RXH+plb69D@^{15NtPBnxs^d1`tNm?AEsA@I=_`c z6MBSvkFH3eB9E@lB09pzX`072kk+}AwtE62t#Hvp)2%BNB7t|>c8`yR$iV_*%oG4g zU9{bkoGu1VcMMA5ciDDNs8+xnFdf4L7cO+!c2AJ(=E>X6EO|B|S!&-eW2p8Bx|Bo~ zP$|_Bs|X#D2o6zGhJ!3!*Tgn59^Oq6XEEK7{_HZ@4QGfU7#bJ1LE_?pFoO@&ciQ$vQsjZ;{D4w zD~o^WP)wV1;0c>Pa`+7NBmLi)1;^V^^C3|StSzCHvfnCk{ssni6%|WvDwwdT+4HF2 zdL84h@&+kED2icQ4g~{uiD*QgvdUU{&AZBt1HgJH3;^4U4Dux+v1rrSm%(P%Y4w;7_;6opGegI~m4;)AHkg zB07pXX(XbEtN}+9KBH8Gp3d9u2`%0T_}FOTHW3aOr4RA2A1C~kW^Xj5C%QmB7rpu`e1WkKifpBH1QTOo>DBp z2f@`n1zHDuiw^7+Z^(8L32}V^OgDvD)b3}iwy^VaQUMaZ_edaXCY2u%?4=neiQfDF zI(|s&z1N(B&XjEkC`(NB-mjEz6_xgAy7+gDjQ8HB`Mk%4|fak>32U9 zp^a21NtKjyum8KU{|!)8984XKDeF6ME3CTZpRR!Q&U9WH&&|bSb@RZ z&)m|NDv@yu`O=V~+$1j)*!*<7AJO6<*u(yAx z>Hwq4J;U#!6ojp~45Ay;jy@x_9X-*Xoe@i_PbGH6*;i&&zPB{9X~Yuz;|gO@Eq1(- z{AKB)ZztC`j0fngydN$Znl4u5$hxl%n#S4j5AWWl?KXFQ77iubEy7Ln=28h-8NJgQrdGB7H*{Uu7kTV#kQ#1B0VJjngb8taIMqsdVXvra94o? z+*M!&CqKvlL*!vQIeQo|86p+dz6%6VA|41%@C#&yWl+&Xd@moEAsrFWUM7RjnQ$h^ z^y$?&GN06(g3`Cbu{S73#OKZcnIu2^LMWE(iaMpxybIu5RvPxjin6F5^KF+~Z#l8> zu;KpXUH{kFLuUwpX6EWni8tbn{!dmrLavxr)YLYxuuES^x<{{>knSi~R!ef0tH_Ym z^pypBM!DbHkI{?&#{lvD3|tFU?dKJR6>l# za2U)s_DniXr3S_3fyfSaqAtI%fonAm$>ab@4TdU|0SoNzrZdU^KQDR3ltmZ}%2J^s zePK?7%v%{Y13WewJN2SNqtja%bl~B*9QGKoa&&R}a1_`XFWnH*Wp;Th1M&#M6xR+G zacy^%QX+Ms+ux>!gEf9W{5WPJ#@Ia7fG&?SA?twCZJ%Du7pjzS*ddv&7?5%Zjq`}6ifLPt|r zanv6ZYt7bP;`jcIwExV1_it@nqU+Mmxp^43W%>N>w%~d+SLhIi;;h`n z<;ql?kZjt{ znZ1RkV2<{v1>rqNr$=(25j*_3ZBsVoWX`aTc51Lpf&HeZ)rd`0{Vqc(R1+Td{d7fS zRTB-hyWCVmnD8)LX%JTC-p4HOBAUd|%4cq=kl}>>{)&g+lK*6Jq=f&#P$&f!?5j!a zXK+ovZisY3UTK5MI{o+vim#{cS&EA|7?k1_!lg>&cwZFq7<9rrtcl*X_g0NlAN+9! zSqm^DMMFT6@KnR##bJ*P)yThg8B^6kE*5t|Rh5waS;dsj&j@Qff?ZDIq~o_baOD|G z#FhXzSQ0!6$NF04UV98UDpgJ-AV3*5$>0IOB6Go_*vbFTSw`rmWXI1Lx5IqPe-)Kr zHm)*qSW#=5ilsCGnjUC?42Z7+yH&X2wPakR>4}EOcor^hR5O1y&sdveH3(vrVz|^z zrDUI*FN_>_dB_71tC2wV;0q=9uT8RASsWexA^LZ&X?SWST4r^@syp`IBJ1we-wW<{ zWHr4mto`|;cZbi%F+%FU#+PFnn<|DK{4-;{iF~1f5TD->{8ieptTd5F;pU;&zB>-B z?fCP~+WOC$M?-?DFIVu1|Ij@Z=y>be_LEnOmql7N4{?vqU3}&05>)gb?@a~z+xkUo zZAHiW$`A9XqPWB{0nQ=*R?>5sGNW4`eE;gPpuKU*<+ID~e!?4{8mFRrD5f0-bv?XI z9ZLt^v1sS$LFsE!|1>6u@rN44u@bB+lDdk51R=VN z30oBKmu$s9j=9)`j~}#BTcK1R2DEiU|J&|~I zQ`n8@>*Xh_&Y%38xX!1N7cBbfB-^ojcwU|6&zubMvvnaJEXtKsstwd{%XZYDS+ z)YtR!&P^Q~Z#ysbQS~ztjw9z24W18yC-$QOe;r#`&l+6w$S7c};@vqyZ&5%wVh2v@0%1aYC?JJyjT3U37Z& zy(u@udP0T?e#Sye<{xSJjm%C{j840%z7vk6cbMm=& zi$|oreY)Z70HR0EZ5G!X=PpkdX4`vyk*nyRoSl4ka-Hg6**+I|lvq!!nov+xR*q+F zFIZkwl2wWAe)m7O4SNHA9-&s8#pjP`bo^=^-0aw3T@hrI&;Wmt+7x+$_2F0Ca%Ra% zofLIq8zvhMUY0z?>BrE1(nlLn6%Z!;L_uja(_11~0s8{fnw-AtlB?ykVsTF&wMGdm z1Cc#;Y5b!>5z_6qj&p^NJDOYXY+13kqd4=nbwl6wuhFePN_?~|pI?*040I~KgVMr* zenQbL&y>M&o5H=_#;k3B`u%j9!=bg!KRV`h#0T$+kPLl9mTw*aBdb0_qHy<2K7T7Z zn%TGE!ycoew&U%C?ikN>q=DaF!F*dc1Uqu4;ayZu(R5nA6l9H9u!=g0)?Iw{qVKnc zlquEZHXegXqzY&T@a*i$^5?{h(6sXf2TyNSO~-;8Vdt`cE~q|LJ7>a%ucQtm(|#XIv{A+(Re}*G@d>9L&s;$zP-PMk2%p(gd1)SU))<;0u;yZj}UEI zwWtSBN5;7MXx{_{t(Qg`7#!EKDBA@nu>d%}aUBKuSM^QI_>pt%V0-+*rm`QVGjRWQ zbO6(do&}I6HJ;63l|d*;y~bcZVhuk3xxjx*M|h*}h6(EmZ<~x!Oc5ufqDRL=_kWiIisI{NkROCR*%2HCXTM}tYF!O7)DAZ8BB9kUt_Il6yM z8zB+k^-$+%AlM}iL;C-rUzZ`e>tm)tvpE%LTs|ZOD(#on&P-Hort2Upz|N*H6ZJ zi`KMV#q@s!!<~c|Yq3)6-D0bDmt7V^&WA;JtoqjBJaT_DMx5l%L>bm^)bIr^$Kx(z z(EB|qCktjFbx}JTI`Yj5KDMoHhs}Jci?z6V0HxWDS@WqMrxJENEl@STm378ZSbOr| zzq$)dw;D6o8#iy1o2YH2l8lyxEKzc2Jylcg$9jFM&#e##uIKOvljMghMfQC<)pZD> zcU!~ml3sU)yINyz9rN}nZF$d~dvApt2=kXqT$Jqn;rS((Z`rk%-+x$J(U#otx#&jA zq`5>uzD1){wJCVZ;CscFFIIbHwY!YG&yh=)iaRoFP*(h-^xj`I_fnh^2@JJUJR0L^ z_Jp_VqU-(STtX*El%w^EUMHrgnrBT}C`N|pkD8&#;jH4^>FT{5H2Qzob%b^j2gdMA z)IT1pNX02}|8nZN~F(whry*9kD^lS6flRq>Tt@!tr#mc94 z!-z^R9Dl>GC$b1OQ(&q0%o8q&Vn7e(E6RY3)ouK2Qc`g|=FC)`gxw2Y7))rsJ8-(L zJVtz;CwDcm1o)?iHW05kd%X%==(d9Kf|wusd(KQGsww!6*3%0MUL1QH{5>-q0S+he z7vjsu`HK?WAxIa&rT{yW~4ykV&u&%q2}8@kHp&uxN$Ug;5DWVI0W6_gQu47 ztfF@1a8C6YuDxOzd#2Fu^T|143Bq)n9k@4GRr;KmTWT9hFWTsCA$f}}l9$7T9zsW3 z;Id`b5sJCM8cT+)vNorNdDr2%-Mz^2CaaQ5;v!t7Y7X`+%3#eDns*TZA~dHdy?M3Emgm#g2=4Y?+l zf(B}8(@Xf8Js4*2Gd@h_hW%4_EDX44E4;o`yvxZA7X5-AFAu<|`T6VV2_$2XHwv1f zBg2cQx%WKtTI+(A-9m9KdSAap+`{I$0l^xFw2qMmV}yV_)47_t=g*Ai zqo(a1(WBYUplXGMQSD;wTO)`qip{gRS4fz60HYu7RL6I0GrUk`@()dFDKT|DUxbG( zZ8Yokj4Ns;@S92Bv+k+JLF)L%;{fP9H}*qXpGhk#X#BdsN|$$w{3h;gBuPDu)sOopLN$!y@27W<^!xg-tXpx=&a}=j zc(;4lm&R+Wdi<+OJVBokl2|fhMNZ*GvMlQ!oSIE{g#jZOwE&j@B=yIi$%J zzpOceaT)%_vjc0Qxl{GnObRif3hAQL5LLzjfA;CL7;*KUInO9wqe68A2~3#lMFSza z@Uq`zck-MUdWHQ7caOd!9osog+g32_UJf#U7BuQ19&6e-a>-6islV}W3 zLX`KRfr))qke-_qiibg^`y*ETrOdoQr|B=^r-@C!h`1GMniczC;T2!ABu-4nW%J8D zbrRE}8ba?)4~$&KSEH>JcyhX5z;I+JD3-6B!@iv~kZ5LLm;bj+*S@go%+&JF`{&-* zTd{z)O1H!Om%wm853mWxZ@i(}>$H`wh!ztx+u&876&v)Yy*RnT>?8#bo*!EcccZSu zK_z4Jdyxyxo5mf4OE8~3?m+*Jkxp7qua2s6D1TFZK4;Vj!shrI2$m9pP6c2;%<#r( zhM%hnC%5S|1>6tk8|s|bmF@8Ks!rI}f)_iDouODks5Wv~8J6rfJ83e!UTMWp7snuO znOwaaCRyK~JS%yl@z}K>tO*_o&}LNA7Vn3Jn2S>v4dn_^=6RZv<{W2QJez=|{mDIg zZr6;PEhW>cubBwClhcZ%?Y5cwbN}_xHvK0H&M_6ljKlo!0yVpl2T@EdT2+rp?4^T` z`i1$Zc8e-e8!(9r5Wya9xk1pM$!XIvwcYpY?a2`Io^!|ql0xk^!kBeH+Hmbd-B8XoVfAb0Vt4b>?A14>!f=iqQm(!0j@a%3!KNw@ z!ZPQ{hXkjFKYCELZf}ZT^!Cgf&fADfds09u)Hb?i*TBdCbddD8T3QA_MZ=-JGSt4PFI|L z4B|;qG;>}do#=~mZH zCC=<>DCkr7DF`B9jD6$xU{0`ZlpvZECSt1LcEM_PtlH;}wNp=;4(hMBQ54%`xT1js z%rJRpI11XLIqnOlY*y9g^kJ@{Do$W}{Mb&ZV~x*+ZBDORVCtfWB*|+^aJNYX7G79> zVP*7iA>Exp#5z^mHq|`w`f0m;@UlzZ_`zH1rGwfX;5trEbnhbpF-#I80uH$hGK<;Z zlxiN67es@vNZ@xB@nmBxZ(KdgZ;!utQ=^suPud7!!@#uFR;#g&q$~2JGr>LR2DEINgEysd zHZu+GYldbn0zUQPa_yOEIyqUMpX;34i}a(8WJLK~nh{u0)Ki`44e(3gHvicEJ+)Vo zl4cmA4Z>#atE@iOv@Uy_`P^vCDsAP)TdJA|#U>m*Q{^}2ou%31bQiG`GwlZ|k4AYh@oc1kDC9!G4w_Zv zgGYv(QOsY2gm2E*ey3)z?qrW^MYALSU3la^D_-qKocCulg?@PNNZdg_5pM%ARge>v z3~1C68L?RkrBvS;r;K?bRlQ@6jXGlg8Dx0fTnpAV+$e#suB_OwB@>)AwC;t4` zxFYwsgdO6eDe^6sUo?ls7nP_{9V3MIblZjc4T@|S^s|S-ji#suL))<0gRPATuiAyN zmJR5Rpyv^F%WE3bCKgE|9%+}0nJnA~QuKOoO`=~(l*Rb%K<_F4NOzhYTk&b%tb%B@ z-#TaCuia7i)%sV_Dj(+%%Yla9lm^pytK_zU4+dD%0L+HXhY)|-Rk$6jsgSU9i&>YF ztgJ@7`&d+G6aR7LN82F>>?gIPjWZRyn2c$ts6bIU7)E&(JZdiKwl&QdZMc;iBA?ol zrq}jojo{V*gGYN&WrI^Kl@5J#$w9iSBof>4^}7J)QR~UySxz6SO&q)93}gPtxM%yW z+b*+92S8!YgbC9F6>i`Fo+sZ*PO4oA`C-}#9%ccMd+L?_G3~*qK)LL3ma z5Tg}%<7!oF%elHQ6V0sdRfm24ysXXYSpJP$)nB(i9#GPRy#i-U+=zFG z68PeKR2GUCo35t>KOBVeiaKi_BJL{pq8%ETbF?hzOXA zKA=i+T7^?p&HJqMmDsC-n!+d~*=52xo!Z>pA-m3ekD6{#6CEQMVs*X9rNLhLLXG*J z=yi`6G+!x#_^+YbH%l>1JafX8);b3lmtW-T$hA0QI^E#5qt0ks_5BNdT|Zt8`4#l^ z$6XkIuU*ob4{MJx={yk%ncpG!nT-J~!h0@daLD8e8KM~CG_$RB|2nJbflUjpy$P>f zP?E8dd*Gn!ylrXXmG-j7cR|vzst$YZP)cE|h6zQo)*Omh?i%D|tW~aG^0Gv0)J^33 ziI}@Pf`0}HE@KfmuD-@Ts2z?B;WJDZJ+J@3*@-qP))N}8nU6k z)Le`cY&q~cH%_zXfS~8cf9C&2LZ5&g%z9@UN7e!`sE#$(CQnB2u46e(uIedK4`h7; zU!M}%GPTk$j@|vFw z7)@fX0+`|F2rAy{1jqk=m~Lfjyq0Nut9WPbqSn!SwDhcn&kNV>P3x@#wLy9z08`1n zkifJazUJ3HZyvQY(FeGSXnd{Gey7;gZL<&PG>84TzIM;Ru&}J}eY6IC_tN;`y>if_ zlIf%`B;^vf5gn=*5I1$pKJpt9<9!Sq7rdLhDdlovbVu&fkcfs`K~;Urj?M^{A;(u&?DEP4&A?< zVBuAE`upSeO{auKP##bC#O+RFb^bp735-Jl`k)Jaj`>n|fuW6Z>7va0*|2~P`%RyI zCQNEVl5SePs(+Fc{K@lu;Fdv$j+|ZPvs}I}Y1EL#?0<3Bsbh>8w?kf;1jLP0Im8uT z_1m~f2^zPQ8#+EMe^zi`p(5Jj*{=Jy_0}2dImD*l{}>cBkf;Y|c%wTAAtL8N^Y2xS zNfX`MqiMr1qfj{Y;hLWDF41cC<4aB@Oe&K%L+UtO45YlN>^!kv3u1v=E^chF;;27c zi%h8Dv2eETyXF-$zFjZ`ZasHz8y@Gilxb_*P2F`?$G4W3Ke%5qZ{Y7Edt3d=n>IWe9GzYlq&GMqz3sblx~?|t zPSM_rO9kq5!t5W{B#O^+i~i0_V!Bv44S$m#ko4_OW6s=RM;{e$DCxUFtF_qRh&MFPO5mpmGbREhg~AXv51N zOINEt6)UjFtUuqp>j~@f*oV9q^iuex0L|vhPmV4CRuk2TJzo`GkDD+`Xh? z`9z)Cle4Som0K=Pjnx7?4+4(4W$mUX2YC+MX;2-XE~v8I8W|C&8x@$j-8;`LYN+j> z5|28q`qtGSBjau(UdzSNSmk&I)Wo$pxqEo6fa68gRk4J75 z^B}Jed3D%e%~#DMH`AgU(0iF)%3l zXh1e~)FJyV{H6%$YjDSsVZ(#aFfr(Pv{}ihx|9J!BAj#Yl6otPdX`i`sAjg;H=ePEra-shif|YJm>z>Oi~McXj;3qr)!X(v)ehk z6~ksvVp%4=${LnID~F37K^Ca$N4vSbwB2HyeGNjZVGN9#gXf+UghhRt{{Hs5 zk{Yx9p|-JKe-8?aF4XF~eW8j(K&zb-%21YzI}8fy8-D*B)wzHOOn+B zH?&VJSiD>1$BM_#?-w@&#aiY%KX2Tfw$Qo5P@PJrfp_QR%(2My8hE~voCBucsHi0G zA8v09l8sXk!y$*9CYyY&9<<8D=d^KwPWvU6H)fqy#(U^SWxH?*v_UysK@*4C0H_kA z%~^SiH0;(Yjd-fktC(xw6k|W%7~*&9bhQaq`DRhp@4|hyw;GgWKZ-FTgW(M32eKFi z0Aao5Gx)A|0!j=Z7BInx0m#)>RDSUP*dF}I}*@cTP`p&b=s`H)e zYQGrTdXv~2X~^T5W4apu^{72_g}NiXGs4#}(P`ot`;Qh@e~`T)OOu|=Q@r#suVu)a z>S%q;2R40->T9TtTv3qvsq@~jop7()8MQ4cIg(dDZJxzzj*Hym^fmMLvmYa-6=pX4 z(^rKv@3zaD{BID2m25aV=)v7*B*#dY_0i+}0Vn$SaGS`s5P=&Q9j*`i{w2#Hhwd8% zH8}k_RP*>_hWfapb61_Q`)+EeUY?RD@jR!Q>02WJytQ{1NF4Tv+s3#0t(REPw5)Ag z=s7zJtG4!c$Fsdw%y@J{(euH(pzHlk4ZoEXcS_AjpYC34Y`J=@&F&!81y*bQX$%!i z$9^0{pZO!|pQ1(f_kPPIHP4UU^V?V}Gzs{W0g@~C>&H!GV(3w|Du^5BX|M(S6Pp zzb(4|z-L};baPupd5KHxNc?Bd&5!cMjo;T`!?~pKz!-IGd6l%=i{ehAzhZ2$UEkUv zFJdlyn^Zr#d+3L#xZBt7CpDeEV|)!Oy5`N|PNKVb1!w|Z}XZDBj|?0`41(*+e$N61in&qyITNM&^*R|hRIb~C2owCy&8eQ-zFOGFH z6s}A^;G0RACDDk8nPNy)iH?2ZRxDpr*9$Pd@Wkt*?_4J>i(IS0Mb9nc#6B7~NYH;{ zXd7a4OgV}o2_H#;IoW#zXzQ0#C!*SR>SX1&JjoYi|1(c{`TN*#hwNk*tu;@kI6cgr zuzVpsOa`hGNIQEm5Z zjxg(K-h`mSC7ZU4Toyh%@HUo_?=gF#O&B$d`^1*0TLBEdY`hw6_i#Sr9z^f}MIFng zwFXCw@BOe{d8OpUpY)Tdbt|mb)Kp}%M-6(qwf6IpnN;n8MQ-MBzGGoZTLRTCsl{r+ zE5KF5DyKKKp3{o1^bP(r|Jk61iS_N9!#?eeDc%}$?9BJ*TTRt%eGSPwAfP@|MDTaB zCus3YtLRg>^30MiC4yx@B9{PTa#*e2HR@fA;Sl#T8t=TCbmr?ix!gII z_ZdrXTjp@}$p?Xtu126I{%;_<=6+?4ra`DO9liOqaMX#`Zv&$mk*jIf zqkcfPzj9OMGKGky^DUXBO*Lk0izinM;^$u&IygM%#rYo&+~M?bL8NgYn~YBx{pmGl zSi((P?UFiwv+BD|&pdJ;YmJ|F_t~kvSr=C^NY9FE3>q$BSzz1ro0`NgthFgCZ9@5M-S0P6*~EH8j}NZ{RGWDG%0G2}{3CsnX0>YXR5>SAZE({^;OZT3Hg6llQ~C@-mgurs`#u?Mgl0sbx>N{>Amv zhqtk2$JdvyS`a zneWq*7XBFdXOn99#FF=Vsy^N) z7k8YWg)<&@hf+$LexLhLZsF~26k1z%JbXp1ueW-@QlE)0Po2Q?V?^bl2oP(y;RBtbi8yfLCs_BFOvE!|o{<&;$XDCM@9s3O# za&a8w8g?0gj)~wO@;mrrvI)mx$d3fI@!svqMw)x;j%;&qsCB!d+5tJKewHP6;c-2N~?feFIRy#G|PN=8Y_-=MX`*lE=PE;L`%JRhtu-( zIW}9@*8FssvDW{3R{yW-ogXYRc(uYTNNrc|dESE)vXnikJqOWp{c`#D<+P@P`PzU! zp^poabsU43$%oWkcpUO)XrI{z-eEB>GHa63ygyq!9<$2q)GM0qx$WX_fco+*;GNCa z!Vlux3Gu8RO;JdVv?+484-7i?y?EWrsysJ?&~G{0*1lT5^Y#8kpF4IgeDJp4YtI(z zoEWh%`WW!^Fe39Dfjo_aYUGA1mEME3`1tsS>as@%f(y^PezJL0ethLE=Qo3n}pBU_GSzE?C`IKKbaBkx=Grq-#N1szxGSP^7vbZ)y8dt6Trz0rGB z(a?1T_RIgFI|Qtq07+Ja9Hm+HD2^o2BCZV;zzq?b=V(ux`@1xLcU|v-#Se-WgjmeI zd--5c?AJbKi%0$rieG|n$>L#Gm4d#gi`O4g_c_k8c{p;Opgy){f5_>C~~dKdzt1 z1{gcgaR6}v3Nl6EsNI(5*8BrxpY@%7r7yytHQQ!~zqah~0Kmd)A_apl%I2z3TK8dT8 zEd~!bH`t&&Q~lD&K&2I%*0b$BS1!4JOwDBb z{orFC;tN`~{jPZQ&$Vj8NKlbRTr!G4VCuT%lC(~&8>*7biWHC~mjuzo1ESsyby*@+I{%2A9*z4oJTe@13IeVu>D zW67XfN>+;N3mM1@0oOU^%FBW7LT-=JG+$|Zj^3KB1)Gg|Q zOy&nH+eF^TV}Z!^=xdW^)IX7pHE_&HW3CJnq>LIL`#Ey zD+qo1EOAf3XF?CNKH)L6@6en)wdfS5jMMj)$G5h>`B?YOY+>IiWq0D^isQ6uw<#pl zSa=R;+O2SS6JATM(-rs-HiY{TYrJYRaMBjJAs_B0Vja-)Jh8cOqO!%oxw*IJt*du` z9~;}`IcQCEgF*X|g*&42JG5M9YLCUj&*8-bNu z;I3PgBcCp@72Z=7T*Ra&MP{oinz9a5%<}OJP954e$9z|jn@6~V`)Ol?pPEUAIa4&} z-l7kv&A9~lj{9TPqbMTxKfh%)7O4`U}?4?KNY zXS#jU{>#CZ*T>n@3M{--R*O9!jm3)JrhDo6EZ#kA>>w2P29E8DZCUOoPH;7|3B zb&rfD`oG@NFuSSWoaKgQm)1qa>dhs~5}5Bos^s-CFxO!PnEV5QZIrEPVPybcG$RmI z?}G_;1_zbF9(uPOUUJOHf76hv{2|Zx6%8@)_B`FbyRQG`U{9kWQeDIjUlVJQ-~1uO zfY?6wOW@m!>@T-~z3#)#aHrV(Iol0`r+%5aF~Pw?Ze@AX)eWCKe!o>4KjH2D*30L5 zsu0<;+d=*uLP*G4h{XNMJ@B5cfLV-Ma^urb9z*w>_AAAiPOWyz{k#JFe>r8gUK<<+ z+X1;PId0&6=;MuYro3Y`zA6c|aOvuGxKGDkQ`6ur0 zUwNO8Ma7Oevmhwe`t&0&`=;PM>kJ0Z6D=R#gbV1GB1ZM z)*s*Mkp0LtzV*Y^kTvr%Thmm+n(|t!OPiAX+g_(^O?3t%p0JO<4)<7u((&Z5vgP_+ z55K6mi1(eaU}S|#{_7b{Pn;?;Z+k47Zc(1468>=g@rV1p=X{=TuJHKtiO}n-c4zk< zd7q+>_;8Nh!x~8o-SUb?A%=`VW$5x;Iq`x=fyJoW3wqucPqq4nR~{^^^>?^YwZ!PA z2C)f;!3d)c>p@h4k}+yXESzEkN@_QJ2LuYML-!sAxtZS{0|CzIGb# ztu@$4#m>Mx??`u7bMq6eyVM6Sgj1a)YUcEVxDVX3D+J|-C#_bX^vchhi|y9r&EtLb zTIXgT7@4UMeEsMDxVrK{D7Q9#i6SjTQIsOf$kr{(Ae2Jc3T0nPB1A@(>~kxjkiwL` zY>DjqI%O-eWtp-J4b#}i*!TI)J7c8pudnak?mO>$&U2pS_xzsUDKAQC#RMyTaONyZ zZN>Aj6s~BnRm+y1Q+j{H+nE-YhG2HhG>p zawr~;JGP*biBpOTdhhA@Zm2I4S0GE}Q+6I?rA>h4>;_M|f&9J6>QbjGrTrM@(2s(1 zhqfF3;+vfZCY#C3z z4)}7CJVEa7tzlyyNiHK!zu?<=Q2XVGlMtbi_%hr|8iL=qpDplzC2s@oy5}deo;cEt z>5KNn9vT+#e%oL=vWQ8u1)mN|_JiZg)1NhauAkm8e2fL2@(UYpc9%nN2?l2Kd$lWA zVya~K$lp55(SSUgbopMXFW=x6Erw!PbC(kPO8gux=#MG{y1U7iGzTvX2Y!g^PkFzd z&w9#N_U}|21nDAln>M&O{^`ywXGXO{wu4{|6S!a>D#tIhkb<6!&SB5bgy>Gib&MVb zO|L@FE3nkm--u>+tMuz-G>)A-K@RzkLGLh zNBM8@@WRHTQhAi=(jQ(#*oBUD}SV4Vc~Hl=)}s2W>Klc1nQLG1!XSy|vq$^u71T z+sraV4xdAfNxk6G(9yetadM0A?rpx^jb(nC2Si}2zG4DOm-zix`=^N~Excz?EDY{Lx!y zZ(qft)<}9E;<8N85P|L$xgl)1wE|!tA3=WW>dedmE;YJye|*ewP}XY8#8;D#9_r>9 zMbjaI32Gw6}Q5->#7;F3w5rrvbEtXyo!&JfOv!FJ@RRyK)|$}<=DkgbH}mVUhY9tp1tHLgqL z80TwXp_MONmcq2h&~o^@!D>?Lm&?;ueMk{yJR-JPd3e<;rU;Iy7vz9DE7?dyUxy%+L z-AdTi87#4NiqJg_JXGA)7b4P!o%E*d6OC31SO<4pZ20P)*%j$JPFIh7aR2Z98`X(N zOd!K36Gg=Oyy*7=nStBYrLK(aI-RXdTW zv9&JJ;K}U)`H4HhKY5<#kKNkW8)Elc^JlN&d8b+}T>Y(iUh7tPkmZ#{tujx{Gk6k? zU#dtp4+xB~THVpoJYEAKq*4sE+juFK&GdMpvVG_Y$jg1$%|x9wfF5sY${31eKNF6t zFI*WQhwg`i&^yVh?tCM(r`lVw%d_3ED+-+@!^e_M07{XZ`P-=($e=*big6CG@PhSu&6Myvnc;{HEgNdUxb zu#QuFwDsYXKPczZv#;K&x}CY;%(*%+dZOXkm3>WNj62CE-J2e^<`&-)z?dZ9mJbfG z{2yd*EZ&Bl0SP9t^xEG+XitoC!dP5SIMMLJy19|PI6>4=V<78|@ER8}bT1mcYvFH5 zLGd1zc+YOyS-5**sx~2t3l&k={cku5_oVQ@EiDBop8R5b4*d@jb{x^N`#D^s}7q;A9 z!42h`(BmZkgX*cl%U!gqO)c-yheGKC^8D^(9*K-MN`$4wx_(!e&P+ekTf}m{4NU$z(B=Ls)V)C#L4%D&XWkv7ERE!DdQb2Hka^t@$a(0s@i79obp2?> zw?~NDGgi{BHA!9c5ER9cTX-j2V547%Fq@6}c7k#d$nz5ag{=)=5djsx1)LG8oRPlF zfi!o+C4+>}wK(l>22-mw@;2XlMF^}_I`k|ZqZK?PlHwD2!`G!D`49U)x%B|+M)uHe z&w!`df%o#g^$S=5fJnLj?52vq^qKLYHz|WLsKTC?a-?CqPmEdoZ6Rr$%xg98VYqH~ zZroj^wH80-Jt_Y0{_=|dLhJc0y;GgccyZfN^H)AY&)xd9A;K@wDdUZpTE+b5j_*p{ z-%hxZ84new?2ml!XVPt_(MW1o#D+Uv6+1t^a|ee(SF!Dw3Zo01~X z(KoRg77NAm!W>JUzs)!TnxvX+t>#^y_Q_Q!L)1o*4V45G1P`MX=hvEnRSQ04G zOO$tbf#dLx(f_G4-Pc#*NG@>c{${z!Ho+WXW%S^A^YLl z5~bp&>>~v(@q{nbc}EWZw=KDAlb~}3iG?$=C2%qA+_pzX30Sg1^J=pAZ{C;)+%G8? zcm5)x=baVpW}aIZ&Ly;FS;6gexlh2=taO@InS8a{Lb14v=t3y9UKN_z4@+| zuNgWiO|Mgj00j9nc)#&aIty~xIp>zfeW%Ml(yQ?twTbMhG6GwsO|t3wk7ct#@jSp^ zeDfsO0YlJFI=C6y4b%NelvMx~K1e@_aWdK;e!3KMcO=AVdD;JFEr%m#6QUL_hQ5hD ztXEqJXfylM->6VgS6cW7=pGt8l;=ksh48?oO?RDko&zpWI9YlXvLYY>Y@O~hS!4QjXuK2v(4L|gmSm99Up-H!VQVRO=dB4Z08vT zyBi#Wmwvuq`T~nLJzdfE2ozw1Kt20Z9hjrn>|Rm@^gu}ux`UghNcl*>LIGVlV(fd{ zAB>&b_3x1?Oq!0ZK79hN|K5It!5gy!)Qi=g}(Q+r`Wmd4;z;$Np{!H>nde z&P%NHYT4R@Fg%X2|4+67W5hS|it}H7s^4aAlts_rd>!4YqYj%YU17q=&R7nt0n+|l z>7&U4pR)|&(b@^HxQZ;0%WybnH@hsme-QY;dH5)P%Wqu3Bc4_jGTC-v1ruvYsy{KB zqlEl(xrE304nEc^s=Mw1p67G%#p5Zjzrx)xqG`jj_I8S7ePF}emf$L-)e&$B1jL(g zy|F(IrVUVB*^cKpAbq@s1Yo$=V87rkeeEvK$YzQ#=g%1mgtlv!7Rh7v}d1mZJ1e zo^ndks?@7GgTKeEOoDu4^14kBYfUZZNWq@W7ajA_pQn~A;JATzIQ~m(pKAkTKi=B? z7Cg7j-+!ATYY&*dQ!8%`8w3945S{BEtC00a*Oz*atDpAqH}mN)h#2~LI;QN`>3I4| zmjqmAPjgzJQV-%rmi*y#H+CGmVTS)-bndIyz_taKTwpg0^3VO_5}BUIqst1&NqXG| zQw|u=`R+XhVi=zOnMsFsb1bbKJdJYk&b{Lf!`TRSul~%qE4q1M7?cy-U=sfCA7nk`#$!z{Y>N(H z3$2mqN0*bo53n~HyIzL*62!u;Xk35Ar1*dya=v;OiAnln-SN1uF~4V+U0(%meYNBS z{TPHHfk{KqNcvYIX)DC_oJfRXWr6NsH~}9NJ0y`blQg{a?8(Q`n#c|&rDS@RGj|P! zJ$#yH^e#rLlcHZYZxnHCvP6|H(Lojme6S(&lG=h{oMZE*fhn;~Q(A6Ja~6ZWcdk7ARDX2Y zhWiUYG+Vk;XqzN1G62G5nT#QRCyzS%PgUN|Hj??wAT2P-&Q0I(i=VBtz2`mPxWYow zEpO&N@Q~c4iI)qMtl?b$doG#`?N1ocCH| zi600S@P?`{B>7+GEd}WNm-=~}@mKa4js5_+Z~F3D#;J2HO9z>`w$lXY=&hiEDX!RI zm15h!w6B;lFIhGjEqjI!rPv4MW?JbvSoB40+%>Y=%XdE#y^-*SN8)*joytcEygL+C zub!&-8z)k>ZfFrEI!>h*l=Fu z)O5(X)7~7-P%k765XcDOAc7U=s2#$jldTGQh z^|!X9Ru;|%Dn-**CdAFStkm}Or|`7?@V-;!HL0*Y!EJM->jF{169OEa2;1U1L-fHH zA<7vH+OM0{bdo(Zf`v8oy@q+5Dy9qOs_d8@*5^t7@sK}NLRv}Jl7l&lCoF2mRU6p`jEWWG-TKcJf$=M4 z^We0|gmhx|+7~=;IG>~YM1Y4gjS<1HQ{*@tJQ9utM+F|rdI8jED>J5 zS4*$0%j56%3p8{uDxNaal?fUd;>+>yTxtD2u4!a1q4PrlRotn~M;+mQFLIm)1%bgH z;Cuct0}XNT8%}inn~Pl|My*im#_in0UA%3b1uNgV9V0bpSUStCXMZOtMHG(dZVb)^ z04|o*2SGOI#sV?Ut5h5ea=ysO-4T8|*>kKEr*Da-|Gu@2d#+mcN0RE*ogMH8qWIL- z-IticoH>3Jb5xtG0HSuKKwSuCGN^hFSYX;5*rq$Lhdlgg`CmN((<*di8fpO@8Y%i1 z!VK@l;4$94;Vu>TolREKx;jU==%2FxaP?ey0IJnr_47B$+1gWI!iXw7CvJ#=9>=z* z!^#DX?FEcF6sF(!i)U~7jiD$u(&-gnOM1UsY3@c>3NYMb3?jQlCqp~<8w~cv+zQ#D z8;a^%WL%E2-!pXoucsad-Sr(nZsJf`G%ZY1&N{Lk2ug^F)tTr8?=9qRgdeN6m!8au zOG{sxDbPz?JQKwq|3ldoBY(?mE#V7X>V?MN#R%66|AKWOLy)8t`fj`L@K-*Hym2wE zkFE8TdjiqC{53*l{UfLY8NvC`NfZJNMXX-c#yz79alAXh{MGDfGpV4#ZL6>cl&;Kx z(JXkMg8{+}X-AndqhZeu&8$4*X*Ds4sNs!sSvwwugFWTF%K&tDCNxM9JkFQMe8zcwgC6M8hd!mH{i`Dpu3c<$ zq~~=V9ey`j`RRZ-!vvUGj^>N^Db%_iPl5Cv3RNiZUK2njZoVJVmno3iRNo0TP5gEI z=cxY%?ZHHyY!A@0Bw9PWm6E)B$GQk5mcS#sLXj9`FrX7<^m7fH*KMZT_v^LBP-fZX zi`3)$=-`X3CdDCO9Nq5-R3M`*)_+qkSGk7%mHZ40R!7c^mu^T|4GkQ~j8~(lf6Dr! zSo|0_-iE}gvXXb|lEQk#nc4M~1Da5m;}0tjav6{?&S}MEV<9jR!7#T;4gr!)rby^y z>B?YB`bhSNQ7#IO7?kf^Faq zItP$6PG{O^_cQD~vF&zuQ(nsSP+K-Z5GUiQlvJo8>=&^z8)EZ2Aw5p*Zx)p8 zNB_@R0#`RadVdtF+U7YRyHt_LDccEXP8!K2zs5q%KV5^Hdw{hFKge@a7+KFCt{_$^ z0nkIw&okvkS=@ft&xJv1ptKb8d5(WK({qR>v8h%-`z&ad=-m@sPfwSXfz_W z6e5;d@0F$j-fb7?s60IYAb}!M;xd2}s9&_s$x0DzI>IXZ3%?u~6y#x_ESu+67K?jW zuC0@h1+DBlD5bR%8rkx%tzPq8lIIjZ&eVBaqsDLwOP!XK z10W5+N}r87AySXs?m(K+bKOLO!t*A+odFg$g`Hk)m z8o?d__we=TI{6>_@%#7byl9^dSzrm+#2reEyCK08xjzoOO%5#V%Wd)9AyYx_0Ig3# zN8|5ZQS)UQ3B|+OYrnH>Qi*f$%=-*G`EthLqt#J}HSM$0>TF>kcC2Xim=KG5^<@`09`?rm@fjmKxbXNCkkthkeZp|!n==r zgNm7#ty@L9a-u8!Zb&F$FYwrQ?}Pu0w^`29hn`eUe2Io87=+|V3+mU%1E1klGjaW- zf@ui)HAwcja(BE59MoivafHmq9gr3aUT0^fl|Wq2{UL$~Wku%6wytpz(pye|*3r)5 zmyD4g_j)D6_hEqPkGHDUJ}S-1MMpuRTl<&hv8|qyZo0uS;*-y+0q=p9?qLp0Z(heO z2?1u$dk%L|r~gNu`g2i!8qA;z%GCZ$Nx7pKe4^nE=RFNx*s>^t>J3Yf@?5<;6`Oa% zLO0k;dou7$hR{pszzhi|ZuQNOH=wirq2n#&W}b)oYtxMX9vesyd87E^u~*L}$W6I9 zd#aQspOCzYB=T@~0KtmA2T8GCCzsFf#lHlu=O7c;HV(Wp8F;9XgcNl!&|sjvO|Hxy zH1PS!7nYwAxx0C6;dF?J=6;V_lVg!lQD^$N=xfBMbA?h)@z*6GLVZH!UgEN)6W%ys z)Wt7HbG-PQhm>_d*sq~%?2QD2sx@Y zlw3e?U(p}-aAU_Ob6|)MhK^PaOxpjRrOoqix)UJP0gzeOW0QXy_uGRdSgeOiuBNU1$n8WZ%bvAxAR+h}y-a)&#?GdwJK@+k^lb7BD~>sU(%*_sjg%>|tOs6_8}G zppaOSsdo#}JcHpI&}LqHj;PI!EZlfGG>bZVIP7DB`c6>#m8^a7Fdbe(35sZf!xP); z_v51YUp)8wu5_);onS6CsVHY@+mf)#R`|2|a6o;3L&uZVIbkRE9}yueoe%XGd>S8_ zB|CClp6NMKndok55xNUCP}#(7w;0=O^SfLoLW4G{2+U^v04wCPpF+ZUFBIJVI29@e z)W$CR(5s4cQ3z`u51{Ln@6DtUP$Sii1)V%D$v_XWNl^dt8g!3o1k3V?)%6nn+!V7s z|7E5b+{Xh~DHnP_&r`OEi0Pmq&Hm`!40}M-L+7vz$99wN&(v#0+X@go+5r$$PC5TK z*}ym(xp>Vnw)bKstWGRyp`IKPtH-`POpRXr{E2MT7z!k^0PMQZNoDRb?Md(trXmf; z!N|Im+E+haqZmWQiV`7hd%kcUh5)U>lHR&}L}-|l{$l;tqon@=5SJ;PmF;RK+toGE zV+=gB5qZN%$Xyxe2J0EPPB-pf`5o`5aPp2GXppm(grXsvmy7dIZy`;NPh{qtxa1Ji z7p{}eY7C0dG3LN+h59xX&OLz4vW2nKYgr=12Z~xpM~0D;<*CRZJ@J`zRE)3+h~(21fzv9NUJOHi*s4b`0<%TNwlq`K3lm7c!Br# zFFgJeHlYx)LeFh8TecUDJq+`$@-13Fj4hOW)zU-$=EroA;c*R%sCL?9_|ShfVG~V( z^2fSD+-n^c7PG4E@BZV_p<{XuDQG}Wyr*kIQ83v0rjZzQ?Fu-fB~AnM$M~*>E@4_O zDy*=o+{kY_%}}p$9C9swPRTzx7Gru?v+YizPHsOa6pmdKq%D@0zGwSuEB7+g7sr1s zU8{&V5_2^-Ee40=jExrOws4MPH>`xbisKd1;DMVuz<_++?G$xn!qvA`JIzdAE`SMg zC`%T%F;4jNllx9uas$OY2*xpX+^JNZ{=FRTy9iAiXyIQyMGNm1)lAdDbNKzxcY@zq z1)jkk`sd*Dp+~fN(lJ7(5PcPTFh|J+)@x+9LG>*%9Dwcig7j^|I%Y{}HNA(@X%HiM2M;eTXB!gmwTab z=b>(Bz|3V8OHl?z`Iv=Nj{Plc13^2F!EmZ^KF+@?-G{l)zR(7>@dp-lnok8_z&5qg zs8g0aw8UB29w-j|b{$`s@FRpIIKBIVu!5twzwNJHnwtv|ocVelRA7fvJ5V4Z9hq2N zP;&(V!~XIkc#2(yf7y0xZ@UFP!PpxSA#F!f4rNUetU+M zOAT4~arASnRaP!mug|GJz~i3MOwj+)!0o`%yr$uXccxMvwu}aI;B{ygkOu9`r_nn;T6%$;$};;@Hp!Nyx&qzVmsbyfTcuel z7L7(wf4kij z)>DdyO|a~{JMs3lEu`m9-pmErJIw9{*LW~ZD2Vwe=qjXz-&WGhQ!FD*HA{R`TC8ze z-=Zng*r|Xt5qGEKt17LqP`0f0hpx;R^rr{Y8ainyhN3bqP9A8wgVMiwvdv;IlglsI zo?vD*Ohw}!cwRod5}hZVpDbIHFCpFsj5qbo<2;H-1&Bu5Cc>6O4{YUzzxC!wd7B^| z1ztkGXND9aE>(nVauVTr?c*w1Rs1~{?Ww<&iCZ zjf4pDt@mvSbCc!CzBW zs)g;(XoMmxWRF#i&&`eAIna0kj1>$WB)oewm&-x=!gDR^j;9XH90+bDbZSZ)ZFd8} z7rQdbIrrZy+A0I6yFW$`0&q(>u3w=2q7klilixQKcNs;TSctx@YvYF?JZ+q-^_Do+UqBm_X%LLB5*5sdwz}5 z)2qRC_eT3DyQplzaLDHSvnz?12(Z7Gc-Z59JcJlJ+vZNFKoQt(kdww%e*5#Y^;!V4 zSYUC8Z7McL;nwjnLybM(UJms?>mcoFKKbeiV4Lg~od_LhW#e2p^ZV_s6b_}h@?3jp z^6cpmn?Ldd=NTs`-sd_%pmMLs{<#p1$`%YTvmAP}Y}0+ILP@u;s5ZGvRwj{m4hfRZm9sZ>$k*f zC9CM=AP@qHWQNoV%CpM-X6nUaMED3x#x{JZZq2FvTKEWX9nc1gH?Uc3KW7?{NEq>( z+o7fjdib}6#LLJ(OH18Z{6n1E!Nor;TYipVVHvSzwX8Z}|#CUWIP?|^y6cebi1`7GGWnbq3?2l3)F;E@; z_$4?qB6M|sY-pP>t}+8n02cJe`erA*_1$5Ibc4&!{#erex|qQU28(PZ)ASjb^bb)0 z88E~JFKcfD?cBwny(H|e*cgmVgl=>@TiahFu3G}SZJ?q1=ZCrWQAQtRJ)J7j^u0h3 zGxa4xLG0^e1dv;Jj>c&kkEpKQJoD4{wZNsm@x~_HWSD}4&f0wqJ#r*1PobXT`pe+l zIm`7L5WD^Y2|EwHZjQ5(2&#t3C=$7w70pW|Vd!7P*AH#ZeP%K zC>^#KYQC0rFXVS5wGl{r(Rx+OE+A2%s{S`lm?MJxI!{VfC!NZW^C;Yf_W)z z<)_N%i&$+lKoaBu26b8l$2DNb6bluxZMU^cx1Z;>$NoH%U~DDnDB=9(RT|XXiwg!h zq+mqSY&%=KA?TP7tuEBoaBB#&0f%}}9VRgur1qO9!J*&WGYn<6t2I69SE- zN(Hu8khY)KfH4>VmOw%PySVF6#P@k%kq(z(cR-8$Ziqm{CO!xik<{b$1v*|c?C&?> z)WpMd6el=14LzUeu3IrU&A%$c2WC=T6l@81M=e%4uLHFJAOV9Znp;MQdDy0VrDukS z!EQASd;98IF$++p$HwTl8sR%(AeT5kkEf)H&Ij74Lj_!Sp+$cmfjg)rW8@gG=;mxv zRx(;K11wPywBrienn|lW-cMM+nq9{1dSL(>SUs>r5o+lNtwjbT+0^x5A4u$#JLtiG zMm5YH7}TmS)CbArwJ6((a2Exen{sg&!wX=!Z7ZP;EehW=t)_LzSll~3(@7Zfd6)DX zgREl5{{XO_R#T_44h z>jS!I?{gm?0CzE*r3b%~hCf1j8Nd=B$m4gelt1D7ee903{J#dn7y@*G*}PS__vn=I zoj>2kz_g}r*|Fe#tC*Xp3F}_BHBqs!D3y)Vb`nONWvJJ+{1GFL3ADKws^nAvM8{QS zu!^PU2J+i0UecC?f< z%srAj5+h-8N({3%hJWMEO*!AyGA~H68F2FtE!0}Mgp$1mD_x6H1Gz5>xj~F4I=^G*j2vu;EX+-*7^$#w zmOCe^E26`#aSEK?8ga>(G63PS;Xgb3zWSKVq0~QFzv_kgCOAvIzCD9=dNJgBBK$F^ zZzDh$K(ax+`N!I+Hz?l)BpqP7Ny{hsCI6v=F#{r#GSNy8AtNp>@sE;A^Eub*Q!?fQ zjfF{aMad3bAwMl8b z2le>GY%Uz79(Yh8jzD5q{QNn0y%bL)yb~A6B2Fdg`z?HLugx^XM`St3WUt3?y)ro zzK;uBQS%2f3Iq}mI?$Z^=8y&@H;{tnW-P>?N+bs+HDUx&epvQg5~PEmB^U`V+iHgb z+CYDAh!(s-N$Th5bxhlnjI4C6)F^w(R3KP}%|&U0rX%LkuI^wMWBEcokXYaukX3JqSpx{T*jwp3sc$3|5Z7ISlhChohRYDD9eP>4+rbb8ku zRR6p?cvrL~a9L2jq4!N^l| zTWtwcTeg&ySiqLXfu9{U(-YFIxJQ;M%FP6BVC!wckK{1z`3iV!zx22N219@an9|yK z1iEsNyP66gGFj!DDz~>^v2(d>ZHMx=V>&BzACh!VXXkbab&^39-oDa4YG&ny>6!lP z@rJu?S1dtB&!Y)6iZ=HiTEgdV(Zi;`=0bH%x@Q$0^5e=^5q<6$%@vekQw>DR0`0pe zX(@i8jt-#ow)k|@{#Z|W{X$kDaJ!@S`vkYP0Zh+Em|`buf1CAwCvdllodCQP35cg~`@zu0wX928R>^^!IxU1V(JiO8(n)F( zQ~>eCq~i|)V&CSnQ2??%Dd*S5c)w(27;B;=XZ+?dVmEwEBwMGI=0D4 z_;7-0w<;>u$|8+@zle;sgyY#|Sp&>xz7w?Ncw0hJK(k;lmVR=)o z@{}vCJv^zrhb!Y;mMOcRi=BAFBA9%ywX>291#0>n;94lk^T1cDLpYyqn%)`k{E-BI zfXqWSi?(IqQHlvOEk1$OD_iTzlP((;E>`6~rso*3ac^_I{$&7y0@W>XOO@7pCdn^2 z!YYQJkwURjW#mtnPTseqGuaAu;M>AL`8EmX6gdI96@TupvQ4L`r^I{=d!mcIZNzPX z2o)07Oq)HTBMP>gAqtjLB2_8vJ~Z0IgT>s0;pi&u=K!biZ8mb8k$szR4EVb%U_M1H z;rNsEP1)%J;Jgqbm^;2JcQ4DJV7^Rdd*F1|zgBp@NH3e-e;=ewvw zrU6FBFCFFg`!o21(@f_y%-t8=FN-G8L57gQ>9YaUony%9mag< zC@9wk?E}BP8l?AFsgGedQQTt-J+jt}p z=!TS|k7@npo}f-z6j%W=fOC0iC%va3K4+5wL@fz0hFBQ(cLzUNPnz{MqEu16Jg3?G zl%x`feYQ6?(`O`*JEI&&^6DIG4JPS#a^ftbBn#*@gVX^ksWLjuBZ; zer@n}7XYe&l6*6*Y=YbfMD){>$E_jO*c;^n@#7k7LSg`O-pf}S3wG32H_`p$xY_KL z7N;mz)+TmO%$iqxT-cF|Glhh0vTl&i&w^m{!b{#tYxHuP{t~PlxRfDn;){$$4=1nK z$ARk1M}}1u?iR5H_JZq8$6q#G_D6H=E=a^mOwZ7Dt+#qb;gT1XWPGfZms13CMCf+0fykTn zX3nY7-ZE&u)%u<{46{)#I{7S22dE!7D3m^?xUiQM0N#LPi>vVB_BSXgLibcj$c*_`uO&^bsXNd0@O_*b zK_tysmCHLPKy<#mGA2mi5$J}^Wj%q_ReECT&p)`1u&7>)Zu7dwb-+yL`q;U~~&yWmwuD5Ko<7c7tajc~IVY0<)Fz(wUZK$8qb zY0PTf3v@?)NHFe3io$VgP7^`tVa=&cyLA1NA#=-BCieU&SB*Nr*(CQuAjM;0*t=q# zj_`Ya2TWNPieI0Sw=tR)= zkY7|%3MKQ~(Yv58u)OY{UhkhcP0xvcW;FoK1^J6z9k!Si2XC<0le1(pZ?58*EwCf= ztzY`3zYYl#loNjzGO+)n91g7(HzTm=fiMmh(B|89-J+BvTPzv3K}eiwf?nQt_a44( z@LS$rS8;j!ZvP}7(KXkQc51DbqO8&uzySWLHL&Ac(Nycbe{^A>f4$EB1+LYC=XBGp z$YY2T25ZejdX5-!?p1!NaV0U1{sSKXX@GHkLk!L~%DuOK*Aax#Y9^GPKY08vPI7>{ zjgZuZ95Td2i($K3HY{ToV-+vB{Yjl)3RxBH5t}D|atBL^>`XP}V zfGM?=7e#HLpdaxBgChfwqz9Ki-3-l2L5Z@T&g=lV{Q%sNCgZZD(FZw&56oaHN2zi# zZ^`lsDe;kDdi78_e&la<$iJSO6qqrQ?tO0cB-wX{I%QPCp-4fgS*L$YJUvvTiW@PQza)J)sS7_0#uV zy9^(7g1-o0kSA{nGED7>fQEa|G9*>@bfbL>kQ6B2m$@^DYBR`9Fa>=d`UO*hM&af! z0C9r%kN!a_vVbF>7R7zH(j8lJDM3X--n2A)DYq}A+`_|ng<#El`#AV)rpzmrH|)%mA^26sKk`F;!V2=HqpoQ5}RUkg=8wcFr|p3ya1|49*;_J2*1L8uR0#ko&+OoO4L&bHu4x0YlUzPihm_VY8L@1@7!DDTtQ z0k6pqQRn}jc#cK)9QhY%b!#KTQkCH(6lQ?JIxxTE9(QS>fTFiD#GgZY?w!W%5QtSvopY?%n?>U3 zcMKH*<%Z40D-Z8K`9k53WE>F2Oab3o+ZU3iR+^eEqMESp3fq3e;uDHLy+X$I)New2 zZh4#W>}I<&lX}@PeDjtAu2_Gl69n=hpr}FuQkSc`V;>vkN(374{fPW~K>wm^FqkX* zo6xu4NyUH>|1TWnBoAz-2@)v7_LTpGOC>lJl48BTc?69_L37D~T1-$4etNOAXz|o< zk)ggj{TJ72AntF50u~y#-mYFZD7sG@6lv@`Yd-06z#Cqlg6 zUGdaF3@RqoChA_R@OV;VS*}C5*Rtno-ufN<^4`YTNKs7rSc!!?Y(vc`t?^U!4cwac z!;OMGVweUN^8n#}yFXybH+?e0dQxjf)&`-dba&RfiUV?Sf<5K42l5fIltGhL4)3s00jS6Dj`80o-knE((jk4K}tce&yXYLJ9S;0Zq-i$ zhAjS}eUxsmSYuDjFzGo5iGb&wC7Seb55z||4T$17tAPM?-Y zj*YR2bU9<+eCl6eM?d}U4o{^Hy!$+JOfg|Vq`EjvctFW^))ZTDSCPCJBJoY;NGNZ% z+;t$a=;H%C6u+9OzEI6fQj~p1f2b*Ef}FKlyy?cZ|APpTfZyxAui$44~qr+P7Em(0#BGR%)K5gPjk4;-*HlCOoZKj;yCUhO#ud5|*!- zbDLZZ+ni;k`gciFdV}k|rDGWqTVVzILk>L>kw1L{Oy%JaUmldU8%E7I+;57H9l8w8 zO;C8_y_Qd1?ZFg((iGD7G}2|RC(pP1hiJs-_-I`oi>lCum$;~u0X@Lg{e|5-l{W=O zx~6=Fn>^;Sn{wHNIR{MCg1t}yyj_<`-{xw9Gzsn~+%x>561euWH6npr96A4dW(@_X z6ML;EMnv<|d&?KxT@>JmiKDlz@TSQTxQCUP`dfY(=ezkRTqH_Fgiy>`TPO=SLb%4c z?R(A&1M5^$BrIu`8o6fg>^LUcg=*T{^a}LaPwkVo4C#G}n_m5fw`zL0Jc)R2Jt+1d+KKV~0}(VYL!m{frj@TI#D$%bK@Y?uDn9fvRcs{6pOP6n zR8=iM<^AjKDDN(_ro@>9NCBtx@>2agY$HDwRjtWZ$z7=Sq=AkxxOK9zbxM=SFAVBI zC5QN?>Re~(Z23V;Zj9*FigNpXeBiEz##VBGdTvUlO`7%`Hg4z?{s_XcG^#H;)9P-1pdZlk8ho(8~3N` zg|4hLM9d;CF54L)l}p2YE(q1dbbkz!&~CxoB*?~i$(S0jerl^>9%k}$hx2^*UrY;3 z5ED?4aHVOzbiEO==GEx=DsP|X zvg257)RQ-7xnXYYs0On)88jI)>h}Gc;_+)ax~q8!1-Z z4QY+dlLJmFtMCS)g{f!q79~<*MQ`I%?Sy64r%bysS-qSVT5Ns?lq$aV=&$5{)pKyV z+HIrtU`PY>3$j5H-Cv*jFyuf-RAQcI6GJUT7^~AWcUUye z8%=FY!FsN7ZIKJpDtac30}pG&$^uRH)tW-0I6(+}bV28h-C zI&F&_AC@5C0}(?jb0F)|aRRj2ik5%4nVzH2%}vA_lP3EH`6kbdn|IeCX_p0a6VL3^ zAK%04+EYTwW_vh2v+MAsf9~S>2fDY-{tYPU#Nrs9Y;X61=eA3u? zLH9{QLdML`&fisCD{(LVe$h+bMHNvFFpHm%Jj69=sn>WAR-p_pYgNjW?dI#0<~$td(KyA8_00ZVEBCz3@K^8Q!!N2vokB zzqa^EfEfw!+fNL@c+Ig*b(Qy&HWa_${WS+g z1XQNWk*Xqahd$nwXh`t0(uvh~O!ldilIjw{U<4fz9o3z#)pEcx zN3F#)_N;~TXN~;!`1V=TbH>J|1B>L;FZIXDpVhn<2wIm);^~5oc8A4~Lyf$L6#==x z_ls>|;fiA;(c^^CIw_RL7%8tNv@sQNboqD9_wz(4TM$rk7o+v5bAo?44cL$ybcUQDZ7i>n?y;7 zxeiT%d1Z^jl*DLPNURvF8rcBRmzEjD28dSDn#mr$V0!O&V9|Rq`lbWI)hxx6ZbL+9 zLOVZmhw;Df7o!WDz{SZCv-rFsa2pm;s>0O3eA?e-cBP2pddj}@w8|R13zrv!AZ)1p z14OAuXc!PsG6l4ORM&CfZbJ$_!f=5_vcrM1i<6<{6FpIi15wd--MM5dDZsrLF1c)^ zX1$DnhjHS0vV%GD@yp$y_C^sC^c{N?vtov>^#?v3<}hh&9ndE}bTYZSQS4>qY1Om{ z2C8%#7yYh<`uQ!d6z2IYp*Z4hAprQjkw1N0vYsCAP9{Vx)I9|!fZWZYqBJuhcg|;J zwS1@rA6?KE=CK}_OO{fo8wDVCYZPWdZ-q^7&!$084t&A2#E$xRot<)YjDNr-E)nE9 zZ`Wy5s|MUHiu-G^!!?oyKHx`D-RRsYNSM}G-=I*pF{J605Fgg!0FOcUO;g24esgTh z{cFK~ZMwnoU^YR&JK+?;c@{v#pMmg?5Pta$V3d{OR}CfrJXP%?SZPgo;Msude(>vw zNyjbdo8eQgm8UCn@vBDxpj6~dUP7#`H)0aRRy%ZS*GLqi zW_~|_RC;xQ5W{{m(vV3`%c|%L-M)g%a?flHpM7RheUQ5ZZc{_1!KJD2y@}M%-T!em z^@3$!0Vgg7GK9SURQCSW$+^mO_AJs$Sd`<>Fd%insB!O*l4&uAMIc9*gPuIRfzw4s zTU=(8(4%kAYz&ZcaCyjLl#BC&W(A*TaYH6xF4Ov4WnV#TU$ML+z?i4EK+6A|6LVI!L9qCZ4OGHRaOQD%P}({BF934wbQ0>)Ltw8cB%C=vTYHsM%Gq z96hR6`zWlRmA7x{vv-pH)ZsAV6*9iyI&oePPx$3iQx#=DT!*(=B}Zmkh*r+|a%R65 z$LRmZA!C9Gai3HCo{=8m^YxW303BO|3B!uk>x)&j69#~Sn5%C-E<~7N=O@f$bZ4V+ zTaf^qMFqMf>ZHL9(`_bmupD$xAqD@XCE_ z3;$UO9E185QanO2)cBfS^ZpIp(!1Vw;A)FtBumC9R(n5ed;!scu!+G6P|{?eGP|bR z%ZqYt`W!D?w4@!`V`1j#X3?Y28~iWU71y}*^y`ULuAqaivgl8Eo5cbEbUXjR)V+By zFq7|tnVEM>L1Brwc9ssX@?)rh)ij%H2C8+l!bxnxssB*Q}vIUe#k^( zjhEXM>Q=xdK#FA#6$gqt;nEt*@L6}1RQ#k_tLDJZLMb0t2plCYJ|JIAeRI~p*PYkA zs@j#az$1T=9{T)vvC78FLQ2rBG0$*$;Wm5<+mk+KpDZz#4zS% zfPUiQ)L9_&^aM}vt6DQx%JZ(g*Wz5xLt2|DbS1Wh>#mLdKDvBNr4dZ!G-G+(MXQLm zdi(2K&5VkVffh1I+&3bnyHXE9cD;4I1kg2a9MCp4c=aC?8KHzoMLZEuS?yS2^Y{n6 zLF>g;=#mqnw%MLwa&3)g;2B4?f#XTV;mX!4sLYWcCwNUAqvKwefeGqM$9qV^GKaID z^JIew-a3SK(F8ys3zG8Wu1}EtzUrjqqul;S)m29|{eFKzP(lPjL?wo#AgweERFr0P zNJ@xGDP0>3P)fQRq;oI{X;6>`l~QUn0#c&}W9;{y@$vin!yV6d4(EycyzcA1?!C{` z<)HE!CD82G;Oj7q1PgXHif5+y6{tTbwy7o*wObp9(UJF zLDG^lFFy3S+(P9%m6RiAa8gA*A{?fIC_$j8KyW3w;1f`oDE=kXLw#_i?$-l7I$l6o zIpI3}zcnBK|JkcOMfuBwWss;OqMA@NYn6#k1I^voM zePE9aAl!<^%%6P4#{?G7N*T4Lxun;Ogc<|SM0D{xs%AA0hFqGd1TH8sPS!x&YDRe# zGg9-3KSGVCy6a5YTk_06PwXB0hE(jHUjAwek4FQqO1wK^k#R(2zJGQk>*?FG4F}@e zdR$e~r`_|W9glVy##@yxdGI2jesyDpIB?6ze#m|X<^*tnAG_;)C$}!lU;2^juL(@x z4~wWE+|@rSM0oM4?I!t;-`x96Dp2{=vb%VUQCO7V&VLW6>OOZy_mB9^W!C#L7p zgLt%H!CUTtz}awjvRe*JxRR5)cdOkz-fpjFI>aZ*Zp`4^6s|aT`{ljP-M;X~g+=F$ z=|^gZ7r*TRkUk~!;N<&AB<4rK(FqDs?^4||3^XPEH8cqtKkEEF|9lPH z*JYnRFrJkrRvRQ7O>Z?B7lspt~L`-2tZWp&Iit#V)3C3{7`nP#7l{#S+?CkuE zYWG~X_O3tEN&wkX94sltv3iSS{nO3rQKl+7e{WJ)d?=$kzOau5Z@NH zv+Hqfm%`zWo_92Rm$mX4A1T-?dNe*fEvCI>d_lrj=*uJfgdr?NVKPRy6czdw6ZCw$ zN;D~w|6AeP5QrMd6SWb!=5l0=)39KO!hKHeC#Qup%6nnwp_Qg9$1N}4rr2-or-Yrq z>KMptcPDT0y45_2HpV--Vs*c^%xiD)CzI(6A?RKBwFq- z1_A5cVd{VKd!$9NXSi?b1>_t39l6C#;<7+_SxRek?NTYE8~Rq{J98rXdL25JyV`}P zG}xnVJq_0CsJeTtHa$zt>@vMS&}rDyVX4t7Q)-F|3%t1ZSbt8yL zmrU;{c}sP$`%SEo)vnkR&0Ft;0Q=D-B1-+e%D_@z;h42@r1VZ*`MdBr#CZ|G3^_C` zx^(O#T-#xGyzhKPY~m!`+qm~mKMQG#9fTI8%_p0GlN3GZMJU*ihR$~_+GM?$tV$l{ z(F#J0Cg-PvzoXcVeee|r*Y&=)yd)>b(b2qJ<9oTlquINr)e=!J*8kHNRd=^w_<-#8 zljJHD2k&WWa%S#YXR!@qoHmS3GVT~&3zH8PTMCX&OrM5_w)h*r6z31M#Y6x}9 zULQp}^LtIAu!lB?Ifl3utO9sF4(^8A7aa}QavaaJ)IZ!i@S77&wT!g`&QdP9hAzp> zrDWMh2WX!Yl-m3uk3KJt9NCfg_r;fTVyoXOh7%JT zFgbDm1`QlraUy})kV)x#V6u^bGthrKa6@R>gfXUPQdl)Awoun5U|cKQPzIT#w&$@(zMgUX+Zc#b#i;~4=$%w zlJzqry9Eudug934Y|X-tAVJ%5sFmtBS(dk6)07}@ocsQ$SkG@2i-UOiIe`{Sa`-CP zR6Sw!T3nDP6bH?-$(-?RBzJ!u!@nOh^h(V>eN@C%w5-?u8u}SHP_HrU+J0aRtCeBn z)oBjSZT?i%w5bd&9O0eBDjd9nWuYtNt+GrOx28q5z!!FxN5N@+JD(OFpDSN6>?~uN zrz>lX>YnZ6_p%GUDfamOl@Po4o9@tIwS&uX&Q~Q%LJo%FaCsMDVMb7~Z(YY?T$qMb zd`DH^Da`l7u9QptceG~1Z~(-5C;0MzgCc-O_AzmdH<9B0Qphj7{QGhC%2S?iE4MS8 z>mVT}Yq!c5L=!u?;{?|E`?#UbtM*#^!BuLvWAjA3q({zfEsBoJ(-pNVZisu-t$xfo zS8-@@ik0&={ocooKtu(6sH1Ov>R9)QsN=~ck`gY>=fjFn#qH7(QjF5a)Y#cuC1A9D z1bELuji=Q12<*X*8dM9n6NDO9XM(8I`Y!(zLk1qohl&jXD@&;WrnKoBxnlJhoecbo_WT#jyO~83P|3j%)d zwo#IC+;8jBii^2v#P)Zs3Fpz7% zWY#V6wYZG5$nm8*x(6xW3p_@NUfMTG050lKKknxyUFkbEB{ar6tv|9YIXe8(4=FSJ z61EC&+;2(mQ{TSWM{mb>m+A=Y`P#`&>>!@9rFnIC((nrXQP>tWezQwL4wds!p9c14 zu-PpnKllmmGvExviy(hBvj4G}E1a6Dh=?pp)qG0#Sg~%p8HAI!1F+7PY<`JI`$xTN zd5n=`GlpE(0`rz3nkh_--n?Ag^zV+lJ}=O2`;lx)=%X@?6Q@sJ88pAv_kHV1p4ER5 zA7_4V{RTXiZTn%=$u}41wB3Gq&&5${+99&`+D~=Z`ctI+>b03w(ZmOO0SFH%=e*vy z)uxQf4P9fl#L(Ewzc7uDREgr3Q-pVtzaZ+G5AtOG>mx&2&s<}psc)MxFuboi(=v_X zhG6p6qhxaOn?BVvJrX;5Qmh9y01R^>)!GF8TwC%xcj?1dDN&(Qn*Gmwl7&NSp2BDC zE|&3`Uy4HpVRG4gj%xEcJEDW`%>pxeK8{aTUconMzT)=vF|K2H(bV-_RvC1&ws!@$ zQ|Fs@d(z3u=%o2Lc|0y@ErHut+RgI?-{n39Wnj3^)%{O@Qx$*t2+9{kiV>~tuGGJ$ z0Vl5{%uL7?uGZcngC+N3&3CSaSeM<|OT1?|yjTY&Z@z1wF~p!pdBgJU{k+}UiXva> zMGcv;T%=RCFuC{{xOUpie7H^X>qowg=v~dTv=`u{WF)P@EyLSLe}-GwkrMtmhvr_f zl(sQnkZJ##pmqc@CdGtlou#O3zDvHPIca0ep_)Tj7JXk;Y06bAKQEfJRahRW3SaJ?dcS)j-+UR zyl?F0fNtPVSL4!_OHWKJsZ84NuM03=b6_z4g*R`eA6VLv#Qk4p7`F#`{vTu z>@JG+S!y&Y3UH<+_e4Qk!5)?C!3|9y#(ag%p2dw=W=NIjy$E%o|2bJSN=^P*5RMjV z*E8@#lJ^h(dK8+Xw`~Vr#$eORtUF;c^N$a9uCLG2^^0Q_hPc41!>1OFpL~$quVbm$ z3WHM*&`==}V9s?|W)HqiLVyKe`-}2RZfe)42*)4}v%u zp4p>Pv~%Jqlv}%fRNczZZXm^PQS2#`&$fo&kVD>OIkYMq4z#*CcmD5T`sIzU`UA+| zOBQ$UO*8iii$IciUQ(vLcOo%>Iu>5o7WtU{jfFYA39En`dw zJ^trN4ZX7*O*@-n&~JxZ(uwLVkGb%XZXPGVB48<+$GAlCNuYG^(m^{M0&p*;WEtw2*g&OIt%< zmQ+CoP+MECQu2z^Co+b+KE9FsX?TZq2`t%UqENGfq58$*A_$ z(q84J)n3i}6~;w|@df}LKe%91$18iSa>o;NK2khP?1wBf-sOX+K)U8S6C zMLj0gUP6-p5PNwG3~@EVon3h7PbqOOw-folLsrb5Pr0+=0L@avB&5j9#Q|>)pUJwl zkMDZ#Ss`l^w}ADr9tMEw(t<8s+=RBv5~~*Z#@h^J@7?y+=a36Dp)jR3{N~_Z>qjGy zH?o1crSXt{Ommv1IR&HWfgVGi*tpl%g5pN!_Z#f{w4H@*V zE$vHFaY2r&#s#||Rc@9ULOnMKV^!dn_a&GDT#9y~v$3!eQwj<>m zK8o%11omBjz_Qa5bie6%+QTe-7Y~gs=zG}x&{R^krqL8TpKcfH>@q$(pHZpZ(om`g zylN>ludCd~QH_||*%($qfO&ijin;HEw^@?*atZRg=TPuj*gU@H7ad7myFR6@;g|kA zIzrugnbjc5`X&9fxyhRQeK@pXfdxasqWO+;s_Xd6<2R^6T_x7{$%=ZP zbc^ac#dDoc<{2zQ-&4j<>|Mk+UT{K=EdF?(7Hz&kai^q-No7k{{{p85)wvY#-7H?4 znoix8=Uh=-dExEcl+4j0Ufodx73c<|duXl!Z`y3cza7sZ1Kiln{@=9NF!{_7J7G}) zX0}>Abkxxn@z%Lc5swT*#VMSS;Mb0%0o#NCA*ec=h{mH>UKBOIP275T`LX8H9Wq@# zB2mfu5+d;D_a*kyj!1_cc;WkS`KQD!crR16<<~SXifJrITtl==Pg`0Iz&31(UQkZ* z4CrC9J@wFji`vYoM8$CJ0ajG!2if1vB@Bko46xHHP93xW#c?7+{=o@25k;P~LptP~ zE4E{pxusXbj84n<>ypFJ(THwf**cxAEo4#=OBdt$A$H2Wgny1s@%*cVTCwiDO3O=y z$HblKKq$GkF&ed}L9(7?g{tquUm`Pa(+!}cC}mEjlsGl;{5(WrUZvl=W+cnIo~k@! zHxEUWO5po{O9>3Gg22Gm)vFCDqJPmG!nSKp67!Qz^2gOl zk6Jer`Q8w27UjVPZ6maK*RhM+JHy;R(mNJ2+Z2*gZ<*64aNbvGzGL-vhEbdELl9^7 zSAoa4qMz@S8zU~2B7nUC6pb z)so8dW-3kJh%JEf>xUoLgy#hx;nRxg=PN0I6?Pn9{Q1NQUXVWtg34gVK|6OoRBe=V2jqA7e z6pbSvl_d($PrR$mS>5Iutn7!u&UCS%{xuX1%qatZAuucC^$msE+-{&pD7i%s#D6}1 zigbBeMHbX{o+KWjdF204Wb0hl)fU0$VsWM&i*Szp4^bPnXsf1DKs@m)us9zA!gBKX zIsfPA*B+qrMzN z8V-B75O8n2?zfDRF3tHz zM}e)bVV8#hQKJUwd;eSwlxM?lJ>i#8G6K)KPF?{&^?PiNI8oJ;YBI3%HdP26SKZRs z%M`NBD>Lt3CH-2V=B`2_$D8AO{5sSSMOH5qz(Om+0S96RGitZc@e&5qvIBB~r}p9K zS%9$N2XAkRTxfk?v4*XP866w|GV}S*gL825*`|H@`naWZE~6p*6UbiSvZuP5R!mK^ zyVzc@%W4x&M5l5f2=v?4FH>&I{X%*U3Zdxe4v&cROMesv_)XFf=K8i|7l$C~&!8Xs z?zj46!B6pSS>0+Qtt0zA2q1T3ThY_dS(fSCRdwvXSh=6>-<$N|M@wq|53q(gIr|mi zw{$`Y=E3w(YpWBvSL}S0^FiFR*eOU54Zjw{rJz}@eVXCQjYrz9niGN@-HAAf%WO>RV~#+!oHM zgeB-q45h2hzI)b*uSlMP`$e9Mk7+#3+kz-1X2y%^ecG#(c?uN`jl^~aabN7vVZH9K zbWbCzIGOdXNV)5Bo%(8K+}-c>Nq(IFGd@6-pNeQc8n$OfNcb&&c~&~t%}7%HTqQby`+XP$bIJj0;SNAgQa zH}Wfo31OO266?`tW+#mI4Ylw6&$0I=Hfht6HGcH)4BU`1n(6o8cdh`Ugx0EtbpQFu ztsONdtpuo^br1?-A{=KcvwAkX9I3Z0xCpX z^rrA4QEKeuFlpLoY#n0Dxj>Y&f}P{dpVI{#F?nXPytV8o*Gup~&9Ek}aA6F)GOA8=U(Hp@#+UkEl2r`Qa@yVmcT`hVF3NnpFrE#=jJ} zq-cXah)z{wNSJQsI=b3C&V3s;lN7rjW+b~jmOjWrOj#OqbCyoi_|>IB?)NNShjiCI zkQsyv$A0Hd&=i=}Oc0iK{YKHFz_y-8&WA;oU41y%GVLq!Mxx_ts(i6J-n4i1P|q4F zEZJqOj5Y`o#klX4@+4?WIr$A;{@Fcybrh@cPeCLz_YWaciaj6fh5Zw`5bRhfukC%$ zL66!DYz!qhgta&tQn1NYa9?cBQ=j2YOMi?*CPJS0)-~$kk7iV-K0UCw;Z+pmOcV@p zoUV?(;Mwq+^dW*>JWUyL)s!hFcIefm;HzXm$i8!2Ak~>8jugrczLuLjY8x&oi>xyr z+bMuqvN>TLEW_7MP+M6^*d_DT031g~og3ZcHCn}A=PKKg^oInXe47G1D)#hZtlWQq zATL7F?`j{su^Biw+-KvZpc|v}A^aNuRLvY6zHd@^5>e6@>3M_KI> zk={s%W>d@d124`^;&zfZkbb^P_cH7HpO4qw3%MV1mBLWvUcLXv#+E7r3SvF_Z%b-+ zX!0W=0bd_v33JX)38gD27eVhbaq6xT09ot~?dj8_euO(?cl`3c@eBL3p2>`q0OnP< z?`y%F_cACxJN!LTfK33Tz!Bvw6$!xENbVUZfqIp2f1YxT0Eggg5C##nUU&5d(RVk6 zxV%cni#?3R;`~y&sgJ#_%eD2nAwbq|1{#`1LU|`72o03KH@f%)q$oj?iB0JiS%k?K z;z-08pOlnE0+G~m3%~U7Y4gh}%8M>Uaxl!KP@2VlGTGEviPPnGkCKNu>_U5mGsj%a z;>>kxcQj5+)XY>?HhL2SHm)#HVqJuJ^ZpVV0_2Jij3Q-=(TLq&UYEGzHy@j2Cti?p zSXXNww7<$ZgjH=lYzQuTvp3nnkmcz&v0i7+$iE(sJRaV3-%$1~{LzLb<~44{XL}#^ zFT&SqH2SI-H22Mk>RTtgj`NPY2=eY|1WF>*G>9e!Y`IAqFzl;bn(n8*Nn{|xwOXxaq~P22J28Lx3jhs3;0`i3 z5I<86s9V9Lo`j4W!IXL!o@UzL!k*bzKu}}OoR~K$qI6;)7zg#sTf?~p=>0G2y~p`w znS>Ol0UdKL9q39WKxl6!mCAYH=OTA*Iue8HJ zFoxZIM~O#1jn3X0euy$b-eB*BD6jH}x5xkxwV#|`qESlI^p@KP3{S14gKMPO|9~+~ zCLy4)H7!=E4*bvS(ZJQcO0JR!%TSuM?X#KuoFz z@K~<@U1Cx=^*#`DfIHgH{xx>Mvcaqt1u0FUwiNDnKi>3G))PRt|JX6J+mJX$6re{f z^~D8k_C=nJBe!*^iA!I7?m7QpbS3{@h(ZV{S;qp8dJpum_59>hFr59u52*-Yk4UEZ zR;!57&6Y4%J?V2BT}o|H^$pJ6&x-ZLZl(R4lkRV{iZ|udezx73mZ8I&bVsZwd^I2= zYqOG>_luCjzZLe?^vBk4Xxg0}s@a?Q%<$iU;UPdbZk5|tFKNkX9!OZP8f1YHoP(@J zjp+;552(5GJlVeuP->`yTjUI%O7_xaWA#qIMjK(dkA6p|bK-X?l^Hx6`Et~hN_NPZ zl<}sacBfW@HJ4gCWq974s4dkB&C{?yAubt97)B)^r3(1a59Gj&>`$&;lw(e?Ir+9A zq~0@-5+Iw>W9-xStafahv4Hf?*os2VL;$DI9$=E}Vos!?|K_5=Q)ybr4`AdaGpAdJ zeQ_nu0n{Q0W*ZH?H>R|4Rh{XM=Dn*CdT-dPnIS}~J!_AxpU@5(*am%Ju)JV83pj7F zIELVv4`;?NEZD)%B~yftCbl6Fy_aBWmCje2pFfSZ`xtWn4Tb{X#OLI|*rwuY4D8PP zXo;4di?Z+B6qY)4vt%-OzVY*WNy|uF0eQQW{gPd1&J#KRiXecypBY~0eu~lm=c`ND zHAnRUp^1Lj3#;E?F)pZ;XyEG%++dr9c>ejv^{svK5py5RK)BJsWtWMKLmoHY&)KxV zl_w==g$&x^jVQ9B?gfxn$#v@Ssl4fj`niuM%FN-WgVkK9M0JY$ZQSd48E62Q{vn@b zhk5o$$!%35hcEH=JY22&QsSpZ)I1+Iba-10KP)b(wO3FIEI$PgfQ9oC4atc}Vg=++ ze_;a|>nyjj$Z@dk-A%3OMMZP)H7MW1ooQYwi(IGazd0B zzchawlV5n2K3Q06i+_2~4$5vVN++qMmYFnh@(k^`aowcZDnKGwLOw5avJAF zL45YIh45d~Wj!J;Ui5&o{I&~C8=_RjGw=>V)j&QqaaKCdH)sTJd#im08}Fo7`S*p# zpxM^x^;4)UQeU{V&=pjO;5d!QJ4;Wu-P1jKYwR1_brCv&S;HWRY_qNy`4aKPtZTEF zqQ6?e8T0Z?lT|x6(f|l*8~W^KR?D31YSu01Vw5(GVYd7WK7-!1o_C6Uk6P4a+ZY;# z?HsNo#FzY>?YdQXzH4(eU^;EJny%~0&tdHi(8O=D^oudETEbJAD3*l9#v@0!R1oQEZ%#dw>WJ1b%xd1slWnqyn|a^y*PRI zBw4PvDdb;#?+?5$?rWcWec&q)o$4C;FaNQ53t{5+)>sFgZ;phpTUN!a8tGcveb`*V zUxVLGPdT}XNxcl}m+QD?9;c1nI;!;k;l}`k9v`g_s-e|>hM`F30kZh}$0QfCx)+Hf zQ;*6+YuWj|D8XLrnD{_V+Eu~6Arw^y74g|CWG!a-@uPt{5y;m?)mX9ZS44>PDbn$s zg=8yHPI23ZJ-e`<_;9s$_vyUp=lkn4aafHQ$^VFHTQnh3RkBw$3H=xUPtbiUm`>ce zJ9!p}$izG%o955gjhFQ1fQzu96YCrJgKo9WZRIa#0}#qQOM6}Iigx%dZJEF}JOzdX zGAcM*84Z3p=jYRPyXx}i7cKW5s3(Tn;P0!4S$w4LcAa2>$?Po5)!q)-2c>;47{6%g zb>+eAePTeLQ|;83U9DNNQTLmZ7q0JfTF)u_;a3{$qlhrJiEoPK`L+Zzt2BQRIXk5F z4_bgQz(49GwUE=5|4D4Ho+BQJbFcxW%ZkL+rGb~v)_|Y-#Dx7^ssn}J^&W-Xz=YVx zp`NSM@-6M{wktmM*cIg&^*}U}J@(~nbE)al=awzGC*H@Ea{^8#%WBhlb%@AJWlF09 z-o=83f{Z=M3s;tqL0(l#5*chN9K`M8rI+7%D+?E9QAW}aEL!5B6{`pXFX{=rMc@V= z4D(t2s*ZM+auWB%jRD?&-ly+H_|MRP+iKRi$RQV577R>bgLDmE~qIdc*vD44hF?$Kb8v8utykZ#=cjt2kdD8p6A zKA!{Z-;7o7i0j#3O&-`I_+zqaH6s!#W^^fXDbDFA#~e1ZyiC~U-XM#F zPj#6dfF5pWQJxtal~Iy@UA^n2W2cQ&K5xEn%`BZGA_wzyt9h5G&(X!Vx3*QdU!6eQ zg$r-g=0RMCwgca2w!SXs`zq`J+CbdZZn#VbKQc$e4j;^}>-kFiMmurinc_z7DSyNl z=;_t&O@=-fY0go8VoRZK-($Qko%P=8RQqNz4GBR@vS=rk`L!>=mI%yZ$jGSw@tqG0 zPgCsIf$VbASwoLBb*OUb5xUkx%4y|Tel@Nb zDYHrEv*J13cQ)$aQBvpmFXl7}2eWqCzAtdIv$=?M+5im$y=*^gGV?6jJ^B2#tXi6& z94;l`e?VC09uJvS`ixK*DR%to$c~fX*y_3g6kkPCeH>KOK~sUD?DlT+q{O!;_bM#) zsqLRek3t*0kSR@9Wz3`W^uU{4C?6lx(C)EXgGH1r+P!vMHqhmYU!*oQ@!o%i6cQuP z`>;Nm;x}xGa%%2o2=7$2q;7FCmu1UKIbB0Ao8hOv-Fm0PYk=`zt&{1HGFxxJWFJMC z9p18WORy}>>=@4**+<+Tl}5$Ka+a_xW}Go;+lf4j_cLgtW-5UJs=x<9oTG#zjbWbr zb}?h=n^>%GPTBZ5M9vkw&-Iq--?uz8 zYCk{VvE%J!;>;d3aitRZh9WnZcNAa!O6d6Rn_luFJ-o*UZq|DLTxTaE_1Zf?Y1mvB zXW{eKsin!{&vI_;^82Xa@#~7chk;n2E{#6qq#%B@@5IwJ2}tK$X3F)Z5_l0NgD9fg z-srGxuFcm~fjQph)~RF!sM{P+O7N)vB){l32kZe!{7yue#Mi$mBwYmn!gxO@v6FS^ z`8WN>XEI23{jfg7W8yJ5+smkcRq&YR+q4;XX}1L8F)kdxEhQ7A^aO4Beer^NPa;_U z^ckXX_Zj1Tf9r(DVv6-@M%3_tE`q5#n9B1opy0?8~*nK&!FYZ==Sro?8!Wloh}A)PV{` zL#EPJ;~7M}8aalap{|ZUEB$qD{;1zXB(M1aefc9-+0MBiEPowkCD3xbQM(A}O{Ad8 z3*g6AsVMN|Q#7@xBwubbn`^CkKkRO6F2hqz z@}+pxdgLaxB;yp<8_Z)gpH=W@rCw+C|5Ukf1 zdKmVjYw!k81w^@&yq=+p`5zUkoJDI zFDe#~VRfvdHawSAS(SvU(5eGduD*wup*Hs?ol46z`&YsFV$DED*+gq}r`L7(XOfdc zk@Oytgf+-maKXcDI2oVgHL5u~HxtJM%Vf4e=f2k*x9w4#A?{2d6&5W2H$U zSV#1WlDh{H5|kg~2^OH}UgHeUP@@;kEc1K=cW?(+41&ho1 z%e{;Mp&A&<8no+~oP#x{VCqND>(2@N-KS_JqSy*Tc2IXbY3wh0!#l`HK*F1W;aH5t z!ZS0c!8<*S?TREmii5i;Q6Ep-ZzE3Q@mc5_M4*1yM3zJ+x9u3ejhi%LWd=}AFfNx# z=Giq9HBVtsOWR8PJ#p|uYi(wt1fiCDuUCP`MoW0 zsCWGMHeRAT#-};=* zn}!`msTH4maN6p76N}HIJm5yTt7~{nn89Xh3gjTaQ9OHwwsW@a#NcamCfP4@lHc&x z^frKtpaGCUnVQ*rui6IUYA_9vj+Y&4AC<>76zmRF9n}~@m?-X&-d2oq{hIK(OY#GU z7XuA5A3Pv<5qFu+F*5K2EBj!Fg)XjZa{5Olpizchydn6<^$Fg;&x%D7e9@Ad!<@x7}vWxTSel;^Roa?(m6XSmne5cA%g!iogtylpuxQgM^R>%6w zwU*+iHM={|LqJ|NDj)e|mLiMP%A^86$i2bo;f;TSsp=iw#MpJ6$h8h&7pzWBBP$x; z$VEbV(tK}Yjzhn3s|1i;&})l=2U^W(>$mRc$Cdq<#0KId>VI_gRW%ej7EPBPc(=$J zf3LG`#MW-aTcnBiF1pE?`+TcgZ#|gYid?TQ4|J)2zH%2!Hj;6M`FCoUwSbU`7#4qB z{f8<8tNgOtJIS>UsVF?w`JIhM@1g+c>PZSUEz4qHAS0$cNy0kg z4U;eH@!hPTo&(YD=U}B~9iBo4k2*Vu2$EYBa2!gnT!@14%|XIOc*nx`%0d1r`+nTI zGfF|E%3~4!PLxpq1Rl8!KbCrQNK6p(l91jpAnqv2$P{LdmHiutcp?f6d1_Wk&+8V< z_{k~$$bC_;y*@o_TsFFAlHP0*=j)VJMT))5;Qn4a=+)={5K9z@SaPqIRR6B^X!}fR zNCzjP{0StDBGT-QF|*S|;1CZLDJKJL_RPo9$W;73+2K(U$_6}p$d2$i!l|FQcla~f zwo_9~*|BjS?DstZ)8Kfrf>wWK776;kAlhV)=77Kc1PFgG;s8YRp;38DLLh9Db^^0X zBM1ct?)A6|xdW@=9OqRX1b8=|IA@^>o7jbB@#j~Z&E%F| z`}oLyF=%SKy9i%0OQ<_B*}fsi-PF4tWsI)%JJy|)kucb^x%qc`XNbu!14c~J@+^;# z|N3p&*<&QN8P8SU3``~`UX=cJehCeh_lOwpS{{h0E8{!#^b(^E$8|XDV!e$^ zqaMyuWhn5U-Yj&1kl;VZQp%?Jj@b2A9|tQfd`HKg2V{s<1Ikl3>*Ao58&U{}fTY3l zouU)bgjkfPr4cB5a(eyPOfu0WTIDiGG;?KRYOxNbA==CPO9xvU*-Qs-4C&Am@xYBX zj`fw8uhVNYec(`N<4#E|axZCNjvD2DQFL?%RwJ#Bt)jb`mOj@-iwZZjq*Mu6p$U;^ zmG~sEwC5@}GgBeQS$d4>k7>bW&WTEJMX~0lUbtp5f>+-9o&?3G?Rv*#$<0iE<8V1K z*(FE#DKV4{4~e&m0i`x9!;Y%Q$iKo8fKyk=gnobm#dEIcWJCUJJP>l+b^yfbI@bx? z;f&fO>O|)CeQxjz_nPt6C+Gw1dh?k*c#T}4uE5?T`OJq3vvIAR9I?5=d15hq>63~B z_PG9#gdcd#;Z2AlN zITS^CCnheY2y^GHP&TyP;b|W_ciGRfD?B9t*J-!y#%GI>ToZ1uw|jAsF}*MB$ZWtM z`=59TSW^Yyo$JP{3Hgv;-|HH(Vp8MO3LSn1no4`&3dTV`xSH{l)d1N{9wmsCQ6?m1 zDxYiExALbe0(A&~hwx@z7?!@JE5Vs+9PQs@FML!^Eeoqb7=X?C*D}F|zA>nlmh4Wi zi||ME+Z&Kk?t19_D@uvy`wUNi;2wS6>iBGI+?Lt`DL>viApU^9Og!JkAeh8xJvsZH z_#z1nke_^^#&SZH;M~w&3K~84;?96CUj!0!KM}QW1n4%7aC0Ij6Th_K=G#ZNT4YFn$2~VFDz6jo-vFk+zXKjt{jFrKnXzKV<)zb~mtxw!aJ9d>Y9;+M z931Ux(O`kvCn{LLoj!x%eOvt!P=ZG|BN$#8KWVWb4z`+klGl(xA9cIm&QZ?kRCjDOfz1O_!3W%k+yht>g}L z*_@r49BM?Mu`5{I@FTQX&z9f#7U*jFyYnlX?S(!hE9iDfXnt%SGmyQqB5EL<4i0(N z1b-&wzhUtFO$)Gz=}}W6b&GMGq?4Vv$G3-8ln2j1)!#pzcW~c>r|-c0JU~<53-do- zJeYJkc2PXqcnO#cQRYPunbf3{;BJ@rF-{tH$_|smu$WVxifZZv%B-AK^gBG8J$YRl ztq4+T=(deL9ctXMYQ^7g^-D&fK5o@vaOEebQsC|CCIjzKe71vnF7P!oyvk-DArJ3+nSLCoufO zR=;AD+@up^zpKkfc5}Ka!0DXTd6?gp1z)>U*Lf>Sm|vPj*^R3%$#Mrm-}Mi!_Ia^f zCOOB^cJB{183W9$ggM~gzgPlJj$s{WX!lqbE0GyZjt#!9u(^LXO&aIF1OZ38#GzV0 z88^rPz6rD8bviY4;vP0WOZ+Ci$aiw`+c;=z@vv?O%ZS0v`uO6w#$GMHW}Q7-gGlaK zIyD2WZMkjpFd74?YIJ|usRP+S6$P;yXZ8)>S7CUlGm)YxPM^QwT?@qA>x-)HvL1dK zs7>tE{Mvhu#CLLRp^LS=ljYj*bOE)XwQwd-)DunUvGl<)&2|7;x5H?=Bh-&e;A?UN zTdive+G)hMO1fXeSl&l?IB~bOW})!4sM@0ikWU7B)3DvE{Tm$B%63(CdonGcinP_Q z5R^2<_(rlHMreFzDa7_?Kf>DiO&&F>;o+_l2mO5 zP4u0yndNY^(7sHwcNGRy;=g!U3O48xab*UsBMjQ6=LOT8?kv08G5I=|X@wIPAf2z` z(5OZsuqoG@3PJ)6cJjrRt+g6vgyIh8R&7jt1r)J`B*+Vw4jmu0dv&I7>ewehNLDUn z&XOR+k5F5aFk|*tj{m&Q2oeYhN6?L`D1!ddPFBeWJd;&4Xi#)Pj4~17MikfR`+>uW zmmf^i7*E~3Ilu1;w#gksc^(@$5aAmsY+`*DwUG~Nu=lV%G?mT(ZNj{f%4&;wV;;I_ z{Y?8xoGRmWn?P-S>MfwbC?S2Z=Q8{i*~rJ?k}K)A0bw|GJOj7o(KDO=q0wx+b+z#% z@jr(EWcc_9dGgau5Tc`m(7tF7+7g*G?xMCB%5f0_0&09IJs>yV%TmYE}cQ)Dt@Z(Y2d+y6cBsXS&(aez&&% z_~Gj@ib3U*0}48?PA8s!w3tkRP1B0ug@UD$=5NPA*hwISu=p`)DKfpTfH0L9rVVoYaK*50jYu7n7gWMNu5m{0jMoIW z&Cu;k8|1(jFjQzvh@-x!GPH=ET6|!z|~_V>DPtckL1n#@#Bh z$t7}u`tL$UlM%%#us$X(4q8%e__w&iSGIei8GPk;AGKy&zTkYY-;3-lgMG5AxK(H> z&Q#tktdTWBb2VN*mH|fS&nD@(J-Qj_bRG!ACc4R=`b`$s1{IZQ?dyuBLlUp4$?4|F zBrh4&dK1I!(E;NqAK4NFA0syJrxXD1TF*ys;Ur!m97In9+oEh8prHz zd>U()j07!M`}&AHJFSoE2@l=kVE(1c8Rnf4B=iP8rU1(5lP5a(SV%m9?#gPMc0%lS zt&lK0OYR*9Ze%O99~oy?#pJC?!Ay{apsaMMcj|~N@LhAX7$NkOA0=ss9Jcjsbb00E z2Q%znJplR-@}Z112~N3@dk$j=neF?q_0BCVnyayAZvCmjmTxBXG&DC2@6r9{cckRo zOvIFzwqrUOPV()1o%7W7MB2MZlAMouK=Cpo=Q)QmfNXaLp%+MmkfE(@Y>?nxyYfUsRW#t?^JOl*8HRCq4>cw zfLv{p=LAE)ZfL3Z;K0{D803s>`)G`>>9pa;O7Y}OMCIfRFGw!#n|=h6sv6H?TMSYC zdxX}yqKY%+)DTvQ!nF+UsIXq3)~K?CWW{9Y0zz->w_Za?hZp_o$*b{*KV^-UArb*x zK^{dBwKS)w2ih#pKdfvcF!UOt{8Yf)Xfugnm=qffq@n_tSKP46?u?u#z%ITez0Sx1 z|5j5lr=8@umE1DZlb?Wu~76zX95HI{O)R3;+a*CW8asi1T*wap5|qqodZx{4rIi z{EeP4$~yp+eT%g2WIR<0%z?h`SDZ%+*%QqZ3{99G$cO;3;XWf!*t4*re8?wiFtgo$ zD0if+U1-}J45BMvuKO2T0}aJIvt7~QzRh5>yrh2!7f8F^-t`dC-dY6KR)>vRpkFlT zm<=p$aDggip4kUOUt*j1<%%8}T_qy>=Kz-sAg#WCY$b3Xjg!eWFRy2MSehe66}p_?vC>q_ykS`CabcHi4Gul)*_< zpbNA4x)h=ddo(1`YC3l7cNhk^G9~{CR+> z$@9`qhu&x)sdx$BD(>a_Yg!gBiJJ&P&IRSRuCKw>EPqjj zpb7}xBm(`o(Bf`m+$!7N`?dLbT>nA)XGW-|Yrr^vXa9=l`BCc(0kyhIMd@u*;QA`( za{eaH|KsYqB(rZZn{P2zAl8t-ir|nVFTiHeG-A|^LEt% zg(L=gD8Crl-UoQ=(jPx8?p?f6&nPZeJFk*}{^&8L)--{4H@tBWS| z?f?V?N`V1)qIvpJQk85TuXh8}W;yc7%q044wGKU7o$i0z=r?r!ejHXLxXlMjLAy|= z`6!K17nv^ADyi^Y)fP2avrjEm*$(Hg+ZK*@(UBX=(2@iNJ%ymxyR!2_vtOj|kPg2m z5s#v{(A%pfy{$%mk35+0+Ajw;{~Q3)7c7IfE3=l@bmbx+qz?%T=Qbm%1I+Y(h6#ue zA3~JL*^0=Cj&;xne^vvCA$uY>g(JaWGOS;_@RERXdKzTQ^*`sGRChY1REHO?^$m)W z;uj%f_OSxIJUx?2m{vgeO zq(e^JV(Z%_87DWN2Ou&zfQG)I|6*-9OzULTnRrEl6C`}FT>(YRu=cd(o0QaqoxtWv z45JXsOVsyAwa-OmZYJAo4433jgx<}IW3ruv#Q?-1_fKKuoMivNJi-a^!JSKDdXK7O zFV6Q-m^b0Zz zcz`3MEY7{1b zO;MjHf2nL_T9Dznc$MTF^^i}>`z?S~+qW5NN~o^;Ww0)H??1Xr zgEqC5gqI9qm z=KX>uw04HJ19mNTh6V$Ww1z}+T>3V`tg)4MCb_}ZbwvTfLCR=W-?MufNmbNgF+sbt zL#K6M7I$Hv<0*ORZ?n@9z~G(u2L+BIG`{Z*ceH!h<|W!e^Bx?zek38*esaTdpCWOH z6EC)Pc{emb=fFN&?F)~Z=tg$XJQ{PVyyDJkYB!x@+(?L4^M~P(oZ@sjS zYvEi+!I{)bvhf9T!{hCfq3-w*W(+IqA91O1xkjXn(~c25ubuqMu@I3E%Y3%8VheaG zQfu;hBl%13sGke2`=H3IPY&$=82{E(_oNd{F>c- zM>TL&2JjxIkII^58dU!v3vOQ?KOP|Soh;}SZcx@QD7zmyI5dx%>kv$Gd>QHeCdJek zw8-AQ^{aDu*4wsSF5`b>$~&+*G$@$XQGzZ~LOqJZHenJUTWEY-^OjQfk&cd)F5;kvzV#d{SO(;}TTunW64eBSTlD znohNsZtt+{+;W|h{f?w;Z|tCK{}$@LGIHw|(pN&t3%9zT0CB1W-J&^L26yZe>?9lq z(1$h;gg#?tRjjD(e%HGJ?=pssT>d^CurpK!J5oC3)TX8@%{5K7Yt>9fOSO6-KgO?f z$`BURGlte$A1%Kxe+GFsm!zq35PxKTGJ;QQBWS)lNfWGSpd?)YwQ?-{e$47@-!{vI zR8*2ENZ6Sx&bQpP+yMpew~=g$@k>gcul>uYhdK_mC=@-s=G|79QX~lyx#XA?)HImJ0rIS zdfyV>`U&NrM}U|(Oe)n^fWsWwjNE_bGjs%jpX6*Ul3lCpzFY=+_kp=k1Iz+Z{s&B< z#n;x0`o`bLD#})aQQLk;@_P`0a}-R zs@21U?RKK&OxqsSTqrtAgyA-;O}x&xT+%85_STg+hvx==xI6ul{Xe-6^gFGp8Q;wh zpk^oBrip-LiY(-1X&m7FJ*PCbe{If4n4R z_)A#I>;7&}h>HX#lsXTs79<@loAL+BH-7cgG@^6{`3nMRc&b?+J8W&&Ef~!K<~sj-3x&GnO5kgEeHQ`_e+0}1scrcfoUOd+2aqF#QBJv%PqJ` z(A@)ccM^G&Hfadct%2dTHYuG@RoEY|63 z<$YZ=d}do)OJ_tjV$e*fPAq3qeUFW&g9UKaoEp!n^xmGU9M4T4gSrm+>*e_qnBQ zKAs=@MiS~GiQhKdf@KmD!<4H^dO50cm=chJ!uTUgjgy*#PFp}U82Oe>n}HY+5$og! z-!0I-;^bzuY-1`NYeYh!kzW4m#zxsn;w9qbmPQa30x%%(jbq@HaR!|;9l7J(5@^ps*^B<^bO-4BzL0z@dRu|W&L!DR_Cv?EKJu?#vU+FPyOF+JQ(r5J zx@%*i#Kuk(^Irb>mLTjAhvF>XlL7p3rv(?=!q(e=CFf)G#r$@0-CJZ^kPZ$cAfO~f z%`Aq-y6k$?7tSO3g>zaq0{phQwDMc{ZK;WC=2HS+KTg^jnrAI7q7;87J-g<#@FQC9yCAJR=Hvyn_Skfj4^Bm>VyWdeIw+O!WxM6FfU|jOLfnB+W z)ZAZZ4ab(}QxL%0mefNfPeP)23DoW`kS96-S|Xs&7u}Aniq$D^po;fejAyLUImUg7 zN6JtvhBu(5%+U1|ZkWe0)@{?QHjo}H^oi)q!-eKNo)Mks@yVSV#A$>w5u8ml7}Wl< zsMgHJjt0{CkIYjxnj2SG*if=EU;Q*q%Dv>p$|;}}Z>d9^C-9cnl#Ehk5_L^-j>7q@9b?C?=}%$$!DlZ+5o>ou z(0qqv^JxJj;K=dDML+ZVHT~mL#~V`h)*1vJzvV6x!x7^C-YQanE;WyI+!+)(&!m19 zte`PJWVcg?8KuAZ4O$~_QZGL~IFUHj0h3$o{iYyGK$Fzt^H7AS$~w_y&fiq3Q~ zPTOmZ4_EnSJbG8^CnYpmIAC|S8BiD}z-plJqy^ko9b!?v+0 zRb|^w;Y)2+hIwXdGWcHKYIw)38}nK|9@?q10K-s~q=~;)VfjA9`u6<2pSWqKbwS$; z#N10EwH^_yPNk&dw{mV!2>tBy*eN&CgwEH7j_-21m%kp=0uG4f)Y)Q072$yS1&RhW zq_~5ZMMQE75LGcVtquuP?0>_hw@WaEGg_#zqtEf{L8E$rZG*t9jbGg5FDjM0Z}<7C zH%zx^x@zuyThbRVKbhlP?7cs4#dHkDqL zog1;b+A1vn^g1G5Os>-RnzP~Q63j`YK&P8x_w%St7< zy(m>RHp@)xL9I(0z@)~LVb{jjp{xZmsmH=_N)RE4fv34ChTuAXSQp9V83}#fv3X?l zGOO#>ZIP_ONOQyZwfX_e^feh6+lEu^ZV{EXPmcL16~}&_3?^BOS+zL~@uCWQbZ8uX z+F40tdC?_M;@Ts!qoeN&MDBUjU%w+JcwDH56 zrRfp&&y50&ZG<>Fj()+q56ZAl&xAa_xfG4S39nYPRW?<{y@+pK+fr-}zlz<(>-S@o zqCn8HAiNST zUEM@>p<|7x1%=;^U{Y%3QMW~~ez@8@O*iiu#uR7k7lgkgx3HsI!KP)q#+AkqAy3`hdlCYOhCoizjJBOJe2bE&n2R7pR3HL{2#P9 z{bJBJ26?}D?$!AAL8L!?%U4FD^!t6Pqwrg#?We>Jwcuj z)^gjQ&*`-LrV2{w*f<;j4j zVG%*QzlO0b4>xXNG){7ld#kGwlb9^6>1(<3pkANWl@^PIlR&mSrB{cZ_RU)MRIo-? z<+IBzTK9-VHy~U8UTsRjzeQFuJ8dsb>S!9SjlMm)z4bQ)@~*Wva?TKak#rn;m9Y;b z?S2TANg4)~dB9mWK>Pv3nUvJp-$}2w>5>cRS=t%w0(}=f)`cN}(ukiGZd>Dady_Qu z2fa*RVdY;{l(&1`EJ>(uIjO2t6394Aza3l=og>~s13@%q|E7}g-Sf;ecIs9q%`Kmt zGOfqtb;4dZ22vp>TBlf#A$gskRSRXv&Jqu?*2K)G#kLPp^2YJ&*^c{6?JD7?@G0Kr zh&3N8*TRymiV|m$Jd35}1TiSfj1=k^Wu(=XW6FCy*Lmi^_RGMtaN@T)@L2Mfy!)@m z4-T9c-mYxY1@J>}wNAb`d9uXM7#j{Vy3#X^sq)#VJAU8I@Vdd;>(6vdtw4d|oyCAo zQAe*z@1&hN>83AMKuhqe*6#(^rT6hIW%{m97`1|geX91Fs@B%;i(dzGrml-e;=QNhmxwSd z8cVo4Gj;KW%{sk+wr3Ujg6uh5lDI&OKnGkNSBe5|~~f}0zS zF=@YrtLB(e-A})-u;tBaA{}RcKTtYX`}e!qKPb4@6arG(j`dp00tXueeD8M(pvr`S zz4R&XMQo-|RpcBq?tQ-z-^;I#JyTnd^5^LEo;pMPmY8nQ)<;d#(z{o*l&mj|&@l!s z)K;Fm%{>~Unzz~!m-p$pv2Jm0chZP%upjkrET4F7x}7gxPrf?n&NUU##Q?=>n|*N! z{;#h%c4^}*HesG^oH+Z=@{}rP?t}!5n#t+Hv2lj!jg3Zhl{rnSJA@S8KkeAciEYed zh+%6wG23tcDW8dRUIIMDvC#b5g$hKDzJWlv^Z3!?EMJD{zg^OLSh`_MJ`UdxNQ$l4 zf_|`#v!B%|F_p$LJFqXqHvQsQCGih+6B#J=%4J&a4x?q-a$9;}8w`J?KJor4*zB-r zi;@L*P8|U!#8voTpscT|Z&Dp_HCg>ryGcz z6|s!xOnwEjezf=PxDhyf-4+ehWY{W~I^&ij5?uIn^I@X;$x2gLzlZ$0t{NK8TYm(# zN2~(n3`|D&^&k8|Pit*T?8Ne2)_??W;okuByiaG%H~M)hfp5`L_EMnIHBsk?^_|0q(1wA2?FGU zn|`ZfAlVb^JmU1XYNy9(mLX0n7p0 z4cNI-t%-XS>lQ>v@~URmnJMuPe+J7w%TwN9gD(7=xaO64FD}krK-_C5U~@dTMCDWE zhrSh!hN_)%=V-w!n{6vi$>n7*qSbB{5pI@53jWBBIOBlq%?e>YsT^JR2qK^`tQ3^i zomuz%>86oL9w`brscZbXb;+N#uezV-Ra@zaKXk9!yk#ZCwbEAV@gPd#1hNi-xa68g zg04W3r)<|i)kZGIN>F+F4@m?hf<(srdX{MF6_Yw7aDJ2;Q1#3B9He~)#kE3{&9qgMoF6XT3L9}w4^q}Vxwdks$B zPAA-2o*$yqshAkpIVjsedd*PS`i?Dfvuu#8uMSQ zZoWTPuz8EiSKahKgCK9XUKhDM8hzXwIzGgC;7e+8{68H3-kiL zsV7AWz1CGDe@@QhqRbV5d>bnn!^k7XSg5-Q8oIf|pZYWhFZD?X%c`a=S1+7q*@X18!e|&QfrHlV*RY zgE}2Ln!-YM_d6`%ePoYu3q*gyueMK{I~8WPHtAt24dRsmsdHGT2+%>LsItqwu2h}+ z2GqN*4tSd@2EkG*Zd4xVKUU1|uFDzgGcSNZ(`c2dUxH`h9u3Dq6tlnz5@5F^|r zIs36-6`j1Z-%AEzkbqtt*+XAH%XKdp+;72Mw&ac0iFpV1zy+0tnn#7PBMhJ|f;@67 zmcb(0J|O> zTCXF&(r;?e{~6sTUcP;@F@}v0)_yx+Vm}FN=TjIXLK}K~;Ewa;k{!`Gr?% z=7UN5=Tq^KAunr(|Jt=t9s6X6jBWC#-7>AO{fKJR{E;<90cte$N-6VGH0C%)mBp6) zTkqIo$|qTnX!jyDnt91g}5@+K_kFwbCa&Up?_(=men>t)nY$gq|+9cwulZxJy0w8|l8Z=fYMFAkC`#%cCn zId9-VO=5FiYavG*MkXewy}qlvVO6fRGXT8lnSh0Xr(lh%Tesbw;O^+7ZpO;z=%b#q zUlIYXBv^`^q{$NO;rc2xXCFFkOuAsQ^xd!}vA zm$=;01x^+;qyoySq)nc5IrvNt}0>I(%y+@t)jef z?}0YCFUZ0og`%*EYqg8pN;y|&0&!0i<4TjRCOf{Aefx~i?`7iy;>qzEEHGY%f5xOw;7`D3$PR(E@NFjihZ)oGIboO_pOCUJbNWP3Hfs zx}juC>(bfVyti*EL~e1sLVr4SzAR)R6nH^`cK0Ap<)#|SC!*qVrkfu^Uw!ASJxBg-50d!4Ibn zC?3J?_`mJl_R%SMlf?Xc^@ZZv7>7z;Yg1L3M9$S&x;lH%9v{gwfR&u4nl>BxJG#nh z5RVE|uI-%&Z{5s^-TK?ZGd92D>l^gJ)GTfH7*a91W$-o7eyptJwa- zH;oE9{IeZ`&uf00b1Vt+qey*Z7HsYJDV~+67MHXQ^ro*(qm?EVNMyMet#ALY zX!2u0?S!L7pAFuEF&;wjfc@=2om1yhuz7nW9$*5;4)Xz;f?zm&7hmRDnqJmdAl{FLpce$H$e^#J z3v2cV5x0}Eu_s;3BrJF`Q*>6TTQ#Ohwxk}bf=rbP1l9E>5bP%kkHjp?ty(OoEVDLD z2iB9GNXU{I-yKfthP)_hg)in?C2N<(9cHfG({U}Zdml|~O;`f>?cDC=D4jYNp!Pvq zg=j$jNN_7asVgRhn1M&hX{H%=bNz~yY!RrDlcacIwtcp&kI#wWy^lN)t@=9rQvA^W z0Cgnl?4P8}EfccFhZr@GOvDG}#sN=YrRk-b$+@l=gxpA9XS)3eb%Pn1c=q>d5%3@& zZA3JAnslNZAHMnakRrBZ;5RSppBR{JZG`?Jm>X8TbFoKqmp}Nf1K?~9-Z#No@QX5Y zA}`(gzI!aP89VzfigzO+_k)jbo6pI&CoVol%wGYuD&+xj&sw@qRNxLTsRMTCMctzC zP-}L?6jKfI+;4P?AouPfHjtkA{jsLPdXfC~;{1O@jBE=cAC_ovv^^xzID2?J5RnYi z0mT(ozzLXme9X9!M?7*#W$p>aSMW9ZB<$urZ3SbN6$&i>kMm_J^7v~5XHG9*7BOM1 zbHg^M-s`c-76Y)%rSO}!-n^it(d9j8JRnFj-csMRjK>U$z-~WKE7wZH)z-TY`Bq`R zIR5BF-}~!*-lWx$SuT$2hXo7>tihK+Q-lQ*s80DJxJr`jfy+WNn72^$lsV2ZR^MMP zz{B-x@=*KB1BH$Tt)S-s$i_ze^lT!dvXap;!p0|j1ZO}|gS(&3az4l{Bx3HtEO*F6 zROV_m4OKQ2XNPxo$^5J~2$JfY&snV+4C;6ScP45(1$QdXPiv*zjJwHKg2W8JbcpVU zos*P(g^mHxAqKBx$G2~f4Z{#^ptVpn9=q(5IgZnN+ak9$?~*%SR*iYLCGjE?eJdVy z+I01k6t|Kl6wiVJ7?8<9L<&>B@b=p+Hhhl$96oR5I^*D0kn^1Cg-WP(*T~uXi~J=d zd-g^{PE8B>jibRIPc3y5$(sEKq6iCzGcR2n`!I3zZ)Ji~b46N*gO%tYg3j2FUBRE- zdN(R!DN*ZgD!n?4r-Xykj3&4+f4Go+5D2oJaVQ#y$QBsgc%d6d2O+8e8D(C*o@Es? zme)l(y)uZ1nEv1a$m0f?0Fo0T_wi1{s6UDep}W0yB@(xCG0sqzPtsfSyg6=V>t~_I z%ri8uy=mSa6n#q1AW{JH^$1Xl3b9VM#1CCu3PVKVR%eS7!lKUw^cjA$X&nkMu1xF_ zPB6LuWg#U4QgLGYud6ZyVZ7(&sW%Rwle8qwG=NUxaXzc#G$2r*3=ITR{JO|~&gU78 z{+8^<#xTh-b1gm8lHYX&l+AMi#}%K42JLEMFg#1f9b+OW6#?W5W(>6{@Jq2)ws%)3 zJO}1|MfZ{#!_4j7Z6P4ts1)cIhpBBw%3xS`zp`pAHu2zHhNsW%4E65}JlYwu-C4(( zBUbQ|_~njGf2T{{QeZWL+tqWJN70o&RnIIg{+Tr>{n!!iZ`vJ2|pQzR)7=`};-&<@H&>s`F zgHff0eN`z5AqI16DsE@^wX4rXF>Wqe{5`pitGupPGx2NS^E~TB$N9gJkxNZY8>4hv z64R?>J45L0QWV}Dzd0)G`>SPsexw8S8|U<9y=;nRIzi-D%t~t1hcHn8(5iiqvA0bTJO~BDTDFrX)>?^~H9ycpWC`F3NvA3Oyu8ONsss)eqT> ziERi9B7npQok(PZIL%BOK@sU5C()W`e>mpDw0!bk>6CMJ@p_?8&n&ZgCq22(<_dz_ zDW~d^YOSS?{Ls0d)(pFwc9DK%K%&%8)-w$TyoL+#>3sDf;kmc4;^xT5mtCh4{J!gM zGTB6cAuALwc(+CV=Qy$w0QV#k;I4c!xQI0DU_iwped71rj*SugBEXtYjg|O)Q{SPT zf6kEni#~we%@pWzy_0bceI(RKDb9Nd%#vcPE_}X>!AQ_duBxikz-ym+Tr)@bJt`TA_gTD91WX-L7dm?dou?59xbgOFe1q$X>y1{Tk`Fu2oP5n*>7dG@ z&M=b-<#W#@G_8y22{Q%e5j7n)x>_qJb6$!w3lyJ}T6s5|dGlgITy*6tuMLDUSe_(t z)MWF|EdW(v;jP4}fcc?9^B4mkSsZ4wxF^0wWBAMTb?1bbHTq~ks^5>rj}WtqR0f5j z392xAi3JhKF^?$oW(+eaZ zyn9fr4|B5Js6k(D;^&ek-mdx9B8TBf@zUt7BtuFNYSxR?XXP5>RT*YDU0(9&jk$K@ z);n1}Lxk^+wWM|SUd&@~-z+^xw`vX|oP(PHCdN&iAz}o9s`Aga?e8eU@+m;z+Ds7W z4)t<4#ewo<=!VvpOWz*7!_slH(#5Q0`iwmVVQ4w-Oni1r%p=d|4D`b3j+qj&T?|7F zfe6InVDOpvXX+!h$cm^giJkjiHi)CaG;qsA&pDSAXPecpnXB= z-nJ|Z-?V<^Ax-mZ#if~JEY`2AA?q&ZpKW#((3vj+>`@wKQ)p)0&Hg*}YK?3C_-{6~ zRw%z*VA}k%CiHvrgg()-?K7xeL+f;>o8*ww0OPB(CXuIJsNdEx5aus!wS(O(&Z64F z(8+hVKGN=Q*D{$?8J~RvX&>|TUJ`6H9*LLmFHh_dUQHNl+mVssZ!iDpa~E`Ods#O< zZQd3>_33+~qi!IpHD~|4~5d-NNYW+30RH%rT6aME_@8cuInsPx*TBs|8S$ zq&UrHd$40IEr|n$MTN%ac)at-lyy9NgFK-+hu;4 z{#YG2rNOWU-7*KX?L%Y*bs`Y@+MJ*HX%Ih>r(cK)*KEp&v0KW{Vcr>7DzW3u)3uJK zCx{b=()R6z9*%FDIRXTV5w7YY*~i|Pt!q;4a$$}|P6A>ofT;|VPW&TlVg~UL%&NY5 zKrw~U$0I1hHtt3Oonh2+OOQ(|w~Y5ZVl_GT2lgoo8YsB)#h|Uf01n8ONaiV$Ew8nB z5Z#uR__ZnuiN@4inegN^^B6RKSK^gpC?QljB@FQt*$E>jg6b}jKw>Qc`Ila+jDe{( zB5y?9&VGATkvl(rcWriI-O2#HYVsXLM_I~2@*lMToGuqAG9G`RCxD#iDP8ww84;1t z@^!{_O~OQqLKB~_stgvA+_(Oq)u3(`P`34p$3B@ZRx;<5E(EFoq(Ij|RY-;SZ*3)F zLl6S$+?Encyd*H@F|Ctl&bx0{fZoda=VrxcPb1q{FjFl?-$q)cIg&N}j4YTnB8MZz zoa#_k9>_SDlR-EE3XdfSWt2TXlPwl(`J;}f(ThH&z)$oy6~)v}FC!isr9OO0L%Ti0gech5{YqtQ z51o<#Z=R34g=oz3_8J!|_7UX@UhS88q-IY3T!6)^@411iO)VE~pf&N+t5XL+Xe%34 zmeqt)3ykp}o%s7~m60kgZi0TJ!hLZN;-xU_5 zdfzXL!6Eg+rE|xGcP*wL7BE4S(#=HW{^1_E~X^LvpS4}ZJgpS+d@0eB&$@Ou@yb9zzk zRYDB-7fG@HzE{8P*5)9d)c#rB=^IxMIXKPW@}$a4fJy>3i4I13>7OccdMj~#Mu=9} zyuLwy9}knzs!5?+^KD5KpA2wcK!abiAIKmxdWP-}9_eT>17(GsN?#}5Jh>GNX7%c? zZyH!dvGS(*Ufrb8?V1v2H@F4LuPU+242aUUrM%Fux%)0HfO3MLSey^29l!nI&2MBp zryJG5ou0B?!&>oC%SHjiGOz2{MMn2hjCU{R)Sf3^ftny;keU?xZJvvVXEJ>II45dc z%yg9o=`01;Dap}--D1G8tSh#Y-%Psf_2MchG2Xk^k+7A^uMI}m?X_tQSR4fDHi8qk zR1`x{w2LW>eBAG=C2JQq<7!wo9^!oG3scPpeiZ_$G}O9F=O*#Pz{-kMSoGrzP!YxR zu`Q(r@fv<|9@<`-tdagTd&7vduMs8h`D)7rP(2cPAog>=`bb3b<_y~vVj2pN8(l#9 z^%V$|yvlNg2JRrcjSV&40e!O;Nc}fIhO1ng1WY9?i|s)I=;c-W41r&WlF~3{EA|+! z^=C9O_FFX!ffB=wx(P~Ov3xfja1~ivr|v=nDITy=E&6DHz?od`J7ugb)$T7w({EFB z{edrP-r5AgS4oq323u>iYCqv-&1!y`zTKpGa{f1}UPMdz(Kc9xUAIx)>d7MUXJa3z!0~M$B#t;JyL412(JdoUpFpQ3ZFjCZT>R0tJt;YO~u)GKVSd7vaWv3NPoH>{Dn!YE0f z^{y%w9nF?R8nsrPwB;LV^X8D2$aK##57}VP_;G(;RcDed|MJg)=!!h!4U^Qqs$ZNs zf`p+dHzR6DZ3^7X%hA`Y(RYdpqg3&>E;^GGxS4t zb&rOP3Ode$eHgUGk?|lyksRkfQ(X)dlZ97o5 z6b3c?PDJE#MYvWCYO;2kqrk#oj=$}TZPG69WedK9F(=qf+|gYosg@Lc5=ge@E#`6a zEBMIu?SyX`9bii9E^B}<60_4{fv*;Je~r;x9i-b@yI3n*R4Y(vVhm$#Ah(k3<FO2Jlwaq1hLH;EbB^?y_#S2&=Tk3l8Pu-vRGwmWUX}cppCes>Y zZb37Ci!6yL{GT{52p&e9Lv2k1o!09?N4)kOW;#8I9XQ7P-0;sOeQeGnKPLY1e}->Z z8fVmT(*l}~P`~!71(2AfB@gITi&<#|v&5v<8G2ySxTZphZI{VcC>sY{lM;SAbbJ1` zVQp;z2Gcx`0)mm`C^h`H`1V>?z~BtZzb8Su8xc5x&RU-}4)AUHDp3tvZ+OghgL=B& z^Oa0G)`%S1-KBLSF-(vuO3kK+#WKs8{#Jyw>DGEylasv`IOS$Hv_iM9JI8Px)@dP& z46BAZ6+43L^^cA(Kn=kC!nreH*x)*LrJ>w^NrkSwIWM6v1NT}{FY`N3D7R2QKUKtR zk#NAo^*yazsH8HbnhwVPCa`ivZq!^^e~5bA=Q*4)Q@+g|7rnZ5_M$JCzV{o{*ALSH z@UOZelBrEF(5IzX^zhQ)#Mp`?kbQtyMxXs<3CbG2KY{dN-|2Eo3=~v%z`VW>P+`<6 zmN5rZ_#ctpIR&bm!j}rW3|9S@=5;{1p)AiQ0wIz#Y#6Tvs&dU>_>WM{)96F}D)5M_ zT-ub?fKY7mfMN3JKbIDvOHAsiC9^p5+hKNQH2_sC$X8;htIBe>W$>oeUn=VqzNgoD z;5G!icccI+Zy@Mtwe@cB`ecz~kofF_(-PbmOBwL|y)KT^Kb_s>P{|M6X(~xGDPYrp zEwqFVItT=zcaUusY-NDpTWhAbjR7seTB||bI9nY)xi*hhB&6>f*AYX%Z*e1UiIb{a zlI}+PD%$4e`pvvWHT^Aaky9U=Zp?Qcj_XOWj$t@ZwT7P{RR&_r)Mp_b^b;MCrT-lB z~?Oq7k*Nc#Qb)aQRnA@c& z*!inaHZ%3_Hyg8#F?-li(GJ9@5u@JN!^hkk?of-xI)pID87!6#X)-v|$L!k#I^?EH z>z%fqwcL`^+;E6&xnd?gd*a3IKM9-!Ffb*<$RWrG(x~IaRdcxSt?nm!(8&Xx2O2P) za;Gw}=dBUGduy$u#Oc=L36@1A{E33rO$L@u;M zRpT|v=wm8}1G!*0J=@rK*sMv-NfscF_=QJIbT0#ClP0c*UG7WR3|PI~QPBcDs!P3y zibx*bb9`lToh$Lf^XC7uJzy}8*+C&f;imO@uzfXyn)i2^>ggFZvN$_s%9LhGa~#e(YBtn5`sF9ue~O{t z?Hi?oB^7x2h{wq|sOG=6a)T~31ZH?9?qma&8L7NTTX{7>KQ3I9w6b-&q`2+HLqaeK zx`jzkQywqI;_T1UvVj`r#!DAnHcJK$f0NkpMM z_Wur0q0Wvbbm6TuBlTiLU^z>cai310`(@R^5b%0W#=8pn0326L&n6A5{SCY*at0~0 zt1IcrWJRZM_AOFmZuy8QAWZ)n>H9nn4L%~6HT01lKx)*v zVMdx#cuSWLV_v8-$J*;)YFPN4>O;eRpPZH*3?Ft0^?#Gg%+eIAfoyoUHJG&D(o(bfpI#3b+<4db4meaz6w#gDtoSY;Ab)j&VfORJmVard!(p6SaYL^_0h zlzkG^QnDieNaOIm_RyxAkyvp+shNLuNpO-!a&uK5=b9$_$v2`78~vhr<)0;d#(8)4 z*fek#N)mjPC1$+(>t?_ugVE>p#UZR>*ih>yI(h9Y%zLLuIQ@wJ-cF|u3x?-w=39+i zJASYm?h!FUx5CG>=X2~b1yVK}z9RX1-5N~>@~@4hsSm%P5i2mtyS&dmP9^bUP2laj z7r|R@s3@__nD?iN98K1AJm705J+n*$pLF!K^mn8a%!jsG=L&qe^~Vs%_JEi1C)#7) zuq{CT24>)a`+$=7bz*Ql!tQDe}8mQl@V$+h#6{a^yr=ZeVhz3gfa>;Bz! zL4W`b-oFPl=Y3Ho^!nP%V zF1$>%bUnR{B*fpah>CK}ws;DRfB+{|(Y!E-I}dMc-vOkbBFXK#4$Slgf5AU%90Vr; zl)i!S7^#hbxRYNq_(M+9pK-emCZvPTI^r(+My=L0%Y9Aj1~^-v;RbA)C#^NYJk&oC z)9`MnJ0bdHrQ5){8N7AOnS-MkGLTDWiQ=zY5`i~ZeB^Z6VO3m4B7#{1Gq}JoGYo2NqmUPxJbbHIy zT?&6^Y0N4xLyJ*rBxUdO!tc0E7pAhvfa>`(HFGQ>#8^l>f<7>R6(TvFJRA29hv$f% za*!RguE#0A-s!2W`N^NMq1fqt=B|&_H}Py`dr*^?L=QGS?xLRFd-0d_f-PB3&xHGJ zR{?SAd%X5!_&HD`9{$Lw$O3jYDbap(0396bD6lWK5<+AMS1;v;2ktHBdNx4!d#Opw zenMjUr`61@Z|d13e7Wgnv7A%pAVAAb0WEuQ1r7O{Y(4B^A46kyS3S&U+{&PqZU)R- zVaRFF3p$V$g{}jFDPWs%UeRFlfq4jb4oWwUdUZSx?>e64WlCvgwd!uX&&Y&D33?C8 z-H?tV5QO0;f$L1Q#EhEVYH|`#Ekc}H`(9sjrQsp@;rbA^bAvpn23Rdhyi}Y$-YNE< zQG7g3!1uRK%~`o6CIOv`t0_8s2|%TOj(b)@lv_HAz(WI*k)liMgzQaX=A8m)X56nX zbEBdd=)B)LFGW~lm}lN{XoVeqC&A?JyrK~xej)05lI-RB#Y2n9=Y@daVqD(h>Y!}K=44b=NH9l zG|GO*FgMkK-TcjL!n5pNR@>wZCm_Tj48%+beux|#=QJ}4inh^U5XcFuF+PDQ83X>5 z)Wuto&fmc_`Rv1J7&I9{X%Sk~bQ9?05i}n7k7ISV>WC8?H61kQ2&DCRyXeKI?-;D2 z;#(P#73Ww1x-RTGjFYfeseOYzhGBFwG7Y#3_mqF)@^+(3{9XfTOjuu``{+wLX~K;u za>9-F?cy-%+|O9pa)`0eco03EI-bM|JsS?hvLmoIK&Rat6GFbkYhqgf)&wj$JfGBL z$ul;4h<--KDtdwovDV5B4h}BvjhDb`Id2869OOX3CJ;K<#LsY&4*J77R}!PW^Dmf+ z0*Fah^vZ6x9IdXI6CZdZ`h#b*U6sMv1PqRPCUNc!p)Sy%!PX~9f)9&r?a;GP?A}Of zcCw9;^|SC9fzYDa%NJn8tkffi653$pi|Ldbg*a#-$4@H^C)@KHa1e?4{h6fI?{DCF zbN7Y9uAk0b6!02eP&T9h!|nxg)$k%M^0kxoF!oL7zxLYE0sYzhS+yy<*?)JG_7JxF z-v)3hP({*K*5PDuD|siuAM7oR0IA)FDfdhR2l^Q6>lwXH-L|7Q;(Seymzj1y-IGr@;1W$r+vqnYHi%p6|u>xT;UX33N92$>D_P5ZU>BB>y-Fyx4(^ALQ_Hn7ii&I??wM(-uc-qEv-p%-U~Nl$ zX8z9QDaEk8=#OyA;Db}6u49%yQB>>S^BMfVfP8wT25Uf}Qkg`QJ43P21L2%hP!`C9 zrbPWF<IgCxBprFOr0~_!oV{_4z)C z_>kBK77@j*;ClFl!F&{*0OR-&;)UJ5NJpT=680AOpcm3UiULjhA2k8} z1C>j$y?4S}B~HqULL6b90mU##(}hi2F!)}~Y)pNbxnGJ!!P(F|xSX95XZ^1Z3o2}( zMiB=b)bu%$34)vnaM2(>EmjoRdt=a7PskWoybJ@5J4E3=gXjwI%n=Cy-(;aS|EqMKI|{b9?i^ z^BtwJJO>^+ogGp}0BtA>ffSqS`T*4TGjU_Oyh(t6Q1hMZ+Fl7o6 z8ui3Jh=yR~J#@bD; z&tz<4#BQMYWsc#?F8-lAh#2;KLpB){PdxGR9`UyUE!F>$;A{e7B(7xhs7QZXO4_J+ z#hVfhg~+d93h&RSLKN&^&z>MrzCZ-cT;>ty@(M>(a%g&K8r$HYm<*yyNilr-w^j;( z|AN}T0i1Qb0QD72EO_42wJ2hZAz_u#SR{%cjH1o6#GEmvCHb$3qYT)BAOyGu zg65|5k4A*QJlLy$*k5*Eg_KqrQv>>Wz64lu``=tuxPBwM)fSxVr}aWE$01Eg2Ef;=D+f!P{;NX-E0dB#`y0#(%1yhXX7SH+5=zZsAmIBF zFh$`v%+qSVFxdP|i*^UV_VS=4on0*iN}%AcC`krKEKT$I$}jSM#adXEn2F@cg^N;I z?Y$T6+eFEGjZ-!eaE?bMqMscQ+1LVzZJ-4Bgcd+O*RDvm+Ht$#7fcw}27JLNkf9%b zj|C-X;_nb1-clRTO7RbhPOinREijsiZ0Mqz?dofID>w<_2>*>J0ZZo!9S_X*Z}wu2 zB1@t)LgbL`8$36<5(j2NzOegoj`#7Nmm9Ld+hllCGlLhD%@J&wlE+(a>c_;akJf1L zV~rJFQ*DEaZ~80^*VF%!POw3?5AAC3aTJ;dPsCOXCDeO*MT9K`5bNqd$5BoC+a}3? z5G-o#hMufW-2DQ9z;lDQ2d~E+y$4nqq|oGLrS%fM_{w*^*{#!7OV=@Er79rcLjE_q zdGAtyJqo%xJHF~vG)AdMQ2$wb1JFG0ysMsR^E_)gAACtk+UKtx!)p=1KA|(KRHzdq z;bem*^W!DvpZQpyS#xW>W3*|r#4NvN*qh6C@Fd{=lfXq3Z&;`WPg4H#h~#>zmR}V8 zzqXdd0V+?91liz=XV10c+J1n!+(pwRpcDy&SpD-Jf}{x;djOrY6aV+&mjes_>bn=u zi2-Juc*d$RR0sZV%fr+8h;wp3T1#ioY#J4XDAzSlWv;-8;P>9XCcUVB=m0wJ;#|6Y%)dNz}AH<}*| zKNrbm4ukMyocfjk2DmX5vqaXh+h)$eTMuhcU*jKJQHtV(AL^m9`4O>05AYO<7Zqz< zAd>gK{ZYR<5=BF9kq)7&gxZWEw<>y-pXK_M-j2P+s;+n=`p7lN%18)wdHv*!Z1zgU z_?ng(OIDrM$+4(0`2LE~^1^@Yed9x6aKimgp*s+#o-zyCfox3y!g`<`RAHleuxat= zKXflg4bF2(0xpZ%_2?sWMy)*uwSc`VQapD+b1^BksO$G%$o=pQ+oJdOImOzAn`3ZD zm@d7LbmFjpeMuR5Nt|c*yIudyo74&h8L$X{n*r>CB)@#{-yK*|IbcYfzkTW<O<2N_{zET8uE^yI2CADvO7_!6wg`8HPq)_!xm^;K@kQ@Q84skAd3P-~Ab=19SRUqkrULoO(QQ=uCVBz^Ed z#q!5o+N}EyNvs>m6lWKd)Fv}xHRtDe!0)Kr zq$SN5ex0ItO~&)DTLb0*zfMByPo#96CS;1LN zAuB~ReIw`vJGEEv->L$+l6yOiOinjqr>AQKSpfQPjHZ$2U*0c>uAGS&secon3ZVRV z5sw5vqy)_40^W`1UZcjS*C5T}U*~+`@x+DNXCau>h8Hp+M^H>0{CMrJMHNj;7EMx1 zOg+rfE@jxDRP)EalpevNTd3L8K(+%G>}eq7Y3ITFa8N{G24D?qh3T6#tDP$oZz{Flj+hG zpym1iUOi2L-1mbYfLH(&F4?I=puy;}ZoK{N3TCt7D{t75cP^EC-^y}1Pk1Xwh%Zn+ zpDl7}z#3rI1R2~WaG%U%eLu`}`aiE(8>SoD_gzp3M6`Fo+W;8VbZNl|$QSP|8@XvB^QeQf1T=SN+?JJ1cDG#8|iR3oN%l!Gc#2V4wG?^Z%MFq(F z58J~p!l3pMM7@G{5!MatA4nhcX6?MJETM&#El1=-5FpZ4BF;=Li)&jFpLeV6)eg_~ z``YTJdj*lCeUuKu|B_#;L%YcMCcb$l@EiS_o-WCr8-E5sMzez{07XrOj?H5Ku7PKI z2<3|M7^xr7hJHiCcA)#fsRFq)e(w6~r%_5Ty<)O&A?H4z6sc2QLJO@=0#b#!;RV(z z3Gb7YRpvr%eSjQEC39(U>Shpt)&BRE;isW@^5}{PO}0JJCLxC*{FEB1`22cPQjoMI z(_P1+4q~U4xXG&7X@a6M5L^`{l}W!g<^^VuT~jhYUN#2i+?A1AQ3-N7{ck);e!_%J zd_vraC>TsS{-4w_l^E8P0!i3Iuz`U#go{WyTVK3>Mt!~bk`bcTul@hn`tEqDzyI;vt}-g4 zYg9-!Sq&lNRwQI*Ww({R_qg>|qOR=Ct%$PA&bXCPb_gMK?Sw9}$M3xE;QD-j|MZaS zp7T7t{>Bpl}O zE1`N1eVUav&aHLs>AE0^pZK?V(lbG_MCz4kd^cd38Q8}&$J3nL5C3+^IMgRWFeU(89$N-VKM`xIM>O!;3%H6kmI2ly)OzPwzNa6sMZ)%1p47 z8YGLLTZ>hDphsefh2Zq<5eJuI0>%jhF5#;S&rg%*v!UNXiA~gf==rV{(5wAQi?Lt# z*}~q|(H|3$!V zKeFuQuzB?vtsxH)0=)j)N*44CD#(hVq{GCCOrmW@WlWS#ebnAwS38T)KY!Lg)LNG) z^WVM=uAxwKXMx&MQL!}8!1$?BUA<37Po0W8<7M_ktosbTB7^`w7y+XMB7|BuT3=R^yvPB18W0U#(O zUx7aLcj?@GMGky?hs_ba1PaG9L0{j5T+5o3$P~SHCNAvU_cOUZLj~Zdt^$(d(|72K zuBoL7Rf-*+um5cNQAsnoYKrrYT-LV!Ad24+pj=K2E{0KWYkUaVJ!~Mx^hnWOJKwFK zA2!0rZwAG!paTiCGI=P_LYE@<57_B1#|}I)eqd`WoGJK-I6uA(J@8QIs^=cj%ixwP zpR?=`dd9c_EuoD98>dt)aW7hl{VB-xlBhIqC{0tIp&z6Ku37*WG6VDbGsEe;UoPjA8=2ByUGXT0e=}w3Obq(&NdtiIH)FQZ2p*?caY&JfBpJ+h#Sa9N=Q;$yD&pcn<<&2vDIxbui^~=AB(V1QQXIfb3GL_)sO< zyK-PK@VT>fvb0Lh0a<}xV4p|u(dMv$kS}B9j|?8z-j?O>1(x35>incMBf)EUB#vFG zZJjbACxS$vp&Ay!w2wW^7-_;m=~=YD1yy|wR#s~T>xKi-DwmES&0Ec#~!oUGKjrfp2> z(MJ8Ek^C#gQGv>8IJu2h2$|2ID`(CNtA{)(XS&nn19NM+5@Ct}mk}M4w4`Evni3HA zI$@YOC3^M77ciiYkN@DjQUV@DhZNNIKj7_%dS7r=x5)1Ht!Rd*8A3qr%qu1Gw%`fR z;~g7s{-k{E{qdr9pD_D%fV6k8xCd*-`U}}MnIZR@il2-`7W^#~4spb7_2z?Jej5ek z@a72tem06ckh?8X5SqR+(I~X*CiK79KdXEq?#O+IDEss|0~#rTkHBNq0qt4qSM&}X zkh(X(8vnJ~?BKs??IC8u(`y9iNe@kSE9n6RURvi=%N}bP zFYNc5dXf7=BpZy^E6%e*l)SIc9Z~EyAkN8hXY+xr<+YLHP=TYdq-8i5W~(WrRWzfqZ`nH{(|c$P@g8=QdRD z$W3HHaJB3e>*EcJ7Y__PKe>F-`LQj+$@CLg{3-BBdmi0kHdXq@c*mG)&Ll0%oWM^3 zSbypgN{6Ef(j83|X)Zt)6R)8oYzqFl2!*0?0w?cfOfuY4h`N1P_arZfLyxwtWPt?P z6IP%*46a0_4JR4ZgCkr)LoYGDpytzNfm=Z%65GrPLQl|AIl0cAV9bQsdK4DOd`CV~ zIFs{S{*OZfsNk#|;nI9?!)M^37!W-$W@BUBOy2cLDC=Jn>$q}7Qw+S1M=K=+cxorp zn?Xg}l>-s1JIvDaTU06Cjlo}K^?oThlXC&b7Ozh}5{P4Uy7K9mq$rgE*Q0qFQxpe%Y#rNS&a2ti+-{bEH72R#6gsh<3S1|AfCH}=}hung{*mnslC=+@*%tAFt z@$GCHXo+ArajWA+P$+owHkIB9z1ej40=49Fwta6U+x;sKc1jKpd&d)q?Y|z;kltyu zsp<6=f9Pg%+!rv^m#%+oVi6X$Uh9sN=ZWKw|2q@` zMO zYRq*Jg!DSA5a@0&>$vqBW|7qyAr~7tx6@;+h#$sr?ARe?r>x0H~TH7lPN_HTTQOUD4 zxdbxPMzAchi4I|ANOG7de@!$AP3lg3DYf1$bfQhoEsGdAZXW(htZavTOH90cO5`^JEu?ypRx zITP@?o6VA38^|ak^r62!2?7p-DWjRfnS&L_)zw~yg4EK0H`)Z$*OHNcrEP8tFQ4Hn zpnummT&ef))&pC^+3GJKUk(O1k5iDS+7Rr6TkU@insE6v0(`bf#0pIoOg`gHs}a0( z)a=hE7GtcSkFgmG2kWCwr z78%fNx*l!x9VYhRU6b1k0{IuxoSC`JV(RbX)$eceYKhHI2UkzF;1`vfl{RY z$tY{bd@+UZT|#pG@$ElC%$$Pg!P9yahkzEOhsNEg-|#KUo+POylI$}db7LN9(_hbu>;@b06iuow;nK?G*Jnb z6rXSt+I7kWTS13%gTUje#gd-lb4SYm)9Tc;m#s0#bQA3X4$wzneTrVcxx%nvu(xWV zwNo=s?j^U{ zuE5j}?dhF=4qjdfYai=)(_`J$Z@oELocZDHLeIcZ)mxLcvG?_|FgOiktT}d9-MoPT zGdD6|7Zv!;ygCSDY4rFUTO>d`s8GFt64sX2dB*N z#qq&uY=rX475v^TpIQ#u&sOL?n|)MkyG~mQfP>jS4)=O2gQF z8B4$y^a-jRR%g(Lmw%Gm#5AM0ejl%*)#oXxnC-zzOT#FWi_N*Xd`P&aILZBdgP>Ej zMfSRxbGzEvA5K=kXc7&vv?Ra@*tQX{47hpf*ck1+1;{9DZTyy5=ADGEhbyz8659Vy zUOt$27RfkB)f4%CL8mgR)gAXvLS zg$|q%u6$eS?ljD?H@@nsUtz?p@NMYWO9K{Y8Dlj7``g&%P+-UM1neDJo5Y%3U2dIT z!PahE^@c9dT0AgbM2U_6#tOpG)i7Jy|8e=sm;Qp>OrP1UbsNRFA;Mk~26T<=&ACVB z(pc(w%>XPTwSiXk&G<6FlOJTA%ALACu6h3CiwMM)z}>jMSd>KKy5#<>5c%=?xi>_9 zK$+7BJs@z)J}m(4--RnRL?UBfQV$D4{Mby5&Q2ZjWVT4)?H^E{ML4Q9c4by-4 zBDfcoPa{7ZxQrWZ>6+9H_ICN^Jm+2unDVD1#FWbCWT9O!C#j~$XE@6Wxc)?m3thx_kzQQk^m7l=drrmd}G2SiOpvbvj!Z~dXy@Py!$0#2OA5oF}|8t z@uP+b-gsVRpRtj>vk@VJ;Q?40xEp7qmTk{kaZ|-o7_Xhl>IYnR|NJ!WD<8ME0n%lA zb@(MdKmnzMY<<{WdXGz6Tmxp+i5%&2MlVcvO>geuST6hP=(n`<7cIL`Yq?Ibz-xz<3nQ{CTHc?9953=&KZn@i3OIzZL*Jbi!GOcTz_{^(fML|Ralavn4{AJ5~ zVTE5bwqPdwaEC>vT+rAf-R6ta)jab(VtI;I|-ZC(YigYi>$DZa|!e zIdadvlb$-Be0iswmV^Ybjj~B%PgK32*{LTC2Ft%0LaZ5KZ#z8_iZGm(kM+;<;WZw#;f@%H(p1%ynE%-MrH$=d0nIq zfRVij$o=)1l;2y5rHuLc<6WqJI#YlBmDV;VANAqbNb!@(sbBAxmo0yE5wzILWC!0P z@;AtOZrurOn7bY~feHNnUDc;OVIi$*hC2pQ=a3Sdf~^o~OOF7Yi1c|w+ydoDJ01<4 zub#Hg!PV=cW`42hqxP_?!CEO-aPIC|!?x)b|AKa*wLFA+bm+>PS(ZgZ8zNgWZrH>IbB z$Jq7`2^!N2F`|Ou=T={OTx)MOJiRE6?&9oEFz-<2frPd7MS$KG^>|qTIoqCfouPu= zWNDQqtPjN54mC2ewD1K#tyy*jUm?dZ?BAs-u1d2{$#U$)TI%bL8LR1Gla0l zE)-`=gGI8_+AYDf(URNYxFek#jqc(0PgUnB)ixC@iJpCMJkj(5g}Bl%3K0bVZgc*E z@Gk!LJ|^$gB1CTK?DF!od-?XqFuUchS(TImx`|84|->S}RjB2%ee~Da0=B)$y zTkJukZ5Qk!$KZI+<#Ri>V2{OyRZPV2*0s3kx&=!Zn?19b;_E62A9q8-7Q4A}?dsRd zNBe8=2Z1aP{N@Sc*giQ}R@)T$;1qnHLn7)I=pUl^sDszxx01ZVn1=})XIhYonIHGe zG*1i%d_^E{($Y^;Ek84)%&EGU;b`dH|Bg_8a~knC0I2|2IRwF&aadi4+*r;OOq6Opyu$2yTx3Zxc7RL6fppQ;xBqwO))JoT?#Z#<}IzwFz zOd3)P9`A4QTa~|lo3z?50-|Xcjk2)&KeP{si`5kmTlrO+mPI1en?j>3mR%*9dp|z| zik7P&e5NQ7ryurmtx94fc34$@MWZW~VQUl2W0)GzYTD97ZZiBi^A_=tq!lF1sgfXO z!Y0C`O_TZ=W&Nz0C9j$#^Gy5qis4_rVKq^*9E8Q#gdPn=EKa6ALv(Tci0SWu7!*PR z_6GbGKS}Aj{M|f86$h>R=}Tt^ITrxn&kL9*QUIc;3QyfXPF4ATv%8n;(N3xtEsw$E z7)Flk1`E1JuDL#5OnpewS-V-a&yop%ZKd{s)wQ~7uY-lgFCY()NQ&GK6Xr^AE-wx- zh~MnqNaK{Kk8uf8KL9hM&#EYH=kqIlRX$?^blRj)%xMZP0H9mGfHhSKi=D0-hSNM% zLMCi~$|~mIp|xV9hR=A7KG;<8Ck%J4SF^g|^D|^1tU(aR@~ym1&T6Xq&G)58leuas z<3LM^(b8PpVFKO*sfqpB9QBj#%eFlO*1wecz>KQ@*{^tC=e_ab*s4+=|Bal9Xk%qd zcG!ml!~Q9_h4UG@)B}f#m*1~c-|R|N{4ht&KtT1D8b^-6cn=R|_y`tKI$n^Y&+#+C z@0`k7n_kG6X+b28UoNL1Md~hljJ+xrrwfxiFw*i2AtBDP*;pc5*K)?+!2e%km%IR_ z&7^{*Q~&n474WLr_d5$2`-Gisxc{Pt@TsGFt2dp0^4nI%;>>#mem!orjSnuAMA^!k zf9I?6c9!rO4l2$~4oYXy;E~z!ea7(@z&dc*1&33d23U;Ws}c^txrx)2f4h7huOgS> zR@eM+`5NbN*aPGVR0G5CPmRC1_ovfBLz7IO*~filVa$A)G#uzjbYK@mh<*F*3F?E+7v=JR|uG8}@VJ(HDIAz0T%B zfto<|2#=AQ=5aA&!9zD-ob>YJievQAF+wG{!+lFI92=R&n+x`IR*F)If5M5~dd2a)rj90lfFy1YQ8ZYNrIC$>@K`cv*PBO@ynfW97-H z`Mi}MW2N`l#@h3js&K*9aO~~Xxrffx3nNr zwqQsI#E7@yN@PUbygTC`Ou6fnN~>(eBFfosr1|?}?!~@gH1@ z=4YQQ!AF^_`d=xs2v19EJ-wIH#FQ)hPHr7{+FIItNOX%Sh#0K|2x(eq^`u;uk^LF= zjV(SYY=}4Qh^BC|&oavA_Jlmn{yer#j@oUyWuUuUCBQw&Y%y9CyyE&bf-h$|FIO{Cz%cUCi?0%kW<{JA@V@_v91ov(>hl zQI0C@@uljE<3mvU*1&F(#U1Ud-!mtwMK|hah`nJ2BuY!iUlBjupHR6X7wRb zHUI+o1#LzQkSh2&FP`N7YA*|C3E#B@v%Rxb5=lxvuy&e1^06t*!omK9TFrPiwmyYq z;K-3V>l9tdQ;0|I1Aj+nviUbrgn+1o1Xx3VGw6mY&Q3L%<>QLsjUQTwsb3Gr)r6Lu z8OSw-h0)rGJB-ZDv84tb{pmB3c--kqy!^n5_dtqfyv3zckn;@pNpMWAnYsd0||pJ6EG& zOf&B})IYemBKvCm;^6Ksxvo)bS=`iOrT>~k!igFlq-85Cm?-ds6AW?ypKUC_LoEPU zFfR&vps|@;2?m1d`0g)`?~;PXLfh7Mt(RI`+;rSvX=b#tumloX)8uWRw*KX=N~0`V zLZYrXXFcxh>eIVG0vyCBIsG>?K!2Ct@7H2fj%3n6yZ;qN9dS>0mtt5vz1UW_@cp~T zy$L?>_Ov>8O~ch-Tb-q7-kh?HxAQT9?KTSK$0@%G+>}rt@ra&%*U2i9a2^a7P_ykJ z@pfpb#g{6vHxMMZmU+|AW20%S{ENq9d1StUKagi)4 zbWZBX7WxT-bDloYIM_avW3$`op!@z9Tpb80Q{1`^njdRT#)3X!<#J3#(Abm6A>Q=p zA;&S<$=j4N{Ztv50)Gv#aDpX5naDXr)is_OP*%n)>UA2D!fwm6RG9?AVl9DgI^}yBIG66)e+~bX3qg`> zj9B{Xjr$>tQ#an{8KW)Bqopf9ÐyU)G>1LGb&!spo;&B?6g;0t$7%Ln6WS9HY&M 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 literal 0 HcmV?d00001 diff --git a/Assets/OP-Blue-Text.svg b/Assets/OP-Blue-Text.svg new file mode 100644 index 0000000..6b5b3c9 --- /dev/null +++ b/Assets/OP-Blue-Text.svg @@ -0,0 +1,13 @@ + + + OP-Blue-Text + + + + + + OP +OP + + + \ No newline at end of file diff --git a/Assets/OP-Blue.svg b/Assets/OP-Blue.svg new file mode 100644 index 0000000..e8aaef7 --- /dev/null +++ b/Assets/OP-Blue.svg @@ -0,0 +1,5 @@ + + + OP-Blue + + \ No newline at end of file diff --git a/Assets/OP-Gradient-Text.png b/Assets/OP-Gradient-Text.png new file mode 100644 index 0000000000000000000000000000000000000000..44753209916500c2cc003060f6093bcdf114e10b GIT binary patch literal 266769 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�za>>Gi4ip2}S^kkf^f{kbV_F6I0B89nRO%gc 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%0AwP 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"-N-@W#faQZEraqG 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" -as [xml]).article.innerText + + # set the text content + if ($innerText) { + $standardSiteDocument.textContent = $innerText + } + + + # Make it an object, and emit the standard site document. + [PSCustomObject]$standardSiteDocument + # Then continue to the next input. + continue nextInput + } + + if ($in -is [IO.Packaging.PackagePart] -and + $in.Uri -match '(?>\.json)$' + ) { + # If there is no reader + if (-not $in.Reader) { + # write a warning and continue to the next input. + Write-Warning "No reader for '$($in.Uri)'" + continue nextInput + } + + foreach ($value in $in.Read()) { + if ($value.'$type' -eq 'site.standard.document') { + $value + } + elseif ($value.value.'$type' -eq 'site.standard.document') { + $value.value + } + elseif ($value.record.'$type' -eq 'site.standard.document') { + $value.record + } + } + } +} \ No newline at end of file From 5ae1e0c4230caf0fde4bb447b8c14c295b608b23 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 19:01:44 -0700 Subject: [PATCH 684/724] feat: `OpenPackage.Part.get_GitDate` ( Fixes #202 ) --- Types/OpenPackage.Part/Alias.psd1 | 1 + Types/OpenPackage.Part/get_GitDate.ps1 | 33 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 Types/OpenPackage.Part/get_GitDate.ps1 diff --git a/Types/OpenPackage.Part/Alias.psd1 b/Types/OpenPackage.Part/Alias.psd1 index 31eeb36..5104857 100644 --- a/Types/OpenPackage.Part/Alias.psd1 +++ b/Types/OpenPackage.Part/Alias.psd1 @@ -1,4 +1,5 @@ @{ Get = "Read" Set = "Write" + GitDates = 'GitDate' } \ No newline at end of file diff --git a/Types/OpenPackage.Part/get_GitDate.ps1 b/Types/OpenPackage.Part/get_GitDate.ps1 new file mode 100644 index 0000000..eccf6f6 --- /dev/null +++ b/Types/OpenPackage.Part/get_GitDate.ps1 @@ -0,0 +1,33 @@ +<# +.SYNOPSIS + Gets Package Part Git Dates +.DESCRIPTION + Gets any Git commit Dates associated with a package part. +.NOTES + The package must have two relationships for this to work: + + 1. A `git` `repository`, to indicate the git repository + 2. A `source` `directory`, to indicate the source directory. +#> + +if (-not $this.Package.RelationshipExists) { return } +$repoExists = $this.Package.RelationshipExists('repository') +if (-not $repoExists) { return } + +$sourceExists = $this.Package.RelationshipExists('directory') +if (-not $sourceExists) { return } + +$sourceLocation = $this.Package.GetRelationship('directory').TargetUri -replace '^file://' +$directory = Get-Item -LiteralPath $sourceLocation -ErrorAction Ignore +if (-not $directory) { return } + +$filePath = Join-Path $directory $this.Uri +$file = Get-Item -LiteralPath $filePath -ErrorAction Ignore + +if (-not $file) { return } + +$gitApp = $executionContext.SessionState.InvokeCommand.GetCommand('git', 'application') + +if (-not $gitApp) { return } + +@(& $gitApp log --follow --format=%ci --date default $file.FullName *>&1) -as [datetime[]] From 5f784d0ff97eaa0c9302b3df30dbbb1cd55df8b5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sat, 11 Apr 2026 02:02:03 +0000 Subject: [PATCH 685/724] feat: `OpenPackage.Part.get_GitDate` ( Fixes #202 ) --- OP.types.ps1xml | 194 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 0a84413..e0e1844 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -2455,6 +2455,10 @@ When provided a dictionary mapping extension to content type, will change the co Get Read + + GitDates + GitDate + Set Write @@ -4138,6 +4142,45 @@ if (-not $this.Uri) { return } @($this.Uri -split '\.' -ne '')[-1] + + GitDate + + <# +.SYNOPSIS + Gets Package Part Git Dates +.DESCRIPTION + Gets any Git commit Dates associated with a package part. +.NOTES + The package must have two relationships for this to work: + + 1. A `git` `repository`, to indicate the git repository + 2. A `source` `directory`, to indicate the source directory. +#> + +if (-not $this.Package.RelationshipExists) { return } +$repoExists = $this.Package.RelationshipExists('repository') +if (-not $repoExists) { return } + +$sourceExists = $this.Package.RelationshipExists('directory') +if (-not $sourceExists) { return } + +$sourceLocation = $this.Package.GetRelationship('directory').TargetUri -replace '^file://' +$directory = Get-Item -LiteralPath $sourceLocation -ErrorAction Ignore +if (-not $directory) { return } + +$filePath = Join-Path $directory $this.Uri +$file = Get-Item -LiteralPath $filePath -ErrorAction Ignore + +if (-not $file) { return } + +$gitApp = $executionContext.SessionState.InvokeCommand.GetCommand('git', 'application') + +if (-not $gitApp) { return } + +@(& $gitApp log --follow --format=%ci --date default $file.FullName *>&1) -as [datetime[]] + + + Hash @@ -8419,6 +8462,157 @@ foreach ($in in $allInputAndArgs) { ) tags = @($in.Keywords -split '\s+') } +} + + + + site.standard.document + From 8c103db713b7fa6fd170860569beea69ed71c060 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 19:45:11 -0700 Subject: [PATCH 686/724] feat: `Copy-OpenPackage` ( Fixes #7 ) Adding -Merge, creating new file by default --- Commands/Copy-OpenPackage.ps1 | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Commands/Copy-OpenPackage.ps1 b/Commands/Copy-OpenPackage.ps1 index a974e6d..38ba044 100644 --- a/Commands/Copy-OpenPackage.ps1 +++ b/Commands/Copy-OpenPackage.ps1 @@ -65,6 +65,10 @@ function Copy-OpenPackage [string[]] $ExcludeContentType, + # If set, will merge contents into an existing file. + [switch] + $Merge, + # The input object [Parameter(ValueFromPipeline)] [PSObject] @@ -106,18 +110,20 @@ function Copy-OpenPackage # If it does and we are not using the -Force if ($fileExists -and -not $force) { # write an error - Write-Error "$unresolvedDestionation already exists, use -Force to update" -Category ResourceExists + Write-Error "$unresolvedDestionation already exists, use -Force to overwrite" -Category ResourceExists return } # If it did not exist, create it with New-Item -Force - elseif (-not $fileExists) + elseif (-not $Merge) { # this will create intermediate paths. - $newFile = New-Item -ItemType File -Path $unresolvedDestination -Force + $packageFile = New-Item -ItemType File -Path $unresolvedDestination -Force if (-not $newFile) { return } + } elseif ($Merge) { + $packageFile = Get-Item -ItemType File -Path $unresolvedDestination } # Try to open or create our package for read and write. - [IO.Packaging.Package]::Open($unresolvedDestination, 'OpenOrCreate', 'ReadWrite') + [IO.Packaging.Package]::Open($packageFile.FullName, 'OpenOrCreate', 'ReadWrite') } else { $memoryStream = [IO.MemoryStream]::new() [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') | @@ -150,8 +156,8 @@ function Copy-OpenPackage # For each part in the input foreach ($inputPart in $inputPackageParts) { - # Create or open a part in the destination - $destinationPart = + # Create or open a part in the destination + $destinationPart = if (-not $destinationPackage.PartExists($inputPart.Uri)) { $destinationPackage.CreatePart($inputPart.Uri, $inputPart.ContentType, $inputPart.CompressionOption) } else { @@ -199,16 +205,15 @@ function Copy-OpenPackage $inputRelationship.relationshipType, $inputRelationship.id ) - } + } } } if ($destination -and $Destination -isnot [IO.Packaging.Package]) { # We can now close our package, writing the file. $destinationPackage.Close() - # We want to open it right back up again as we output the updated file. - Get-OpenPackage -FilePath $unresolvedDestination + Get-OpenPackage -FilePath $unresolvedDestination } else { $destinationPackage } From 6f4a3e3b39de0ffa6e055253878d060ce5e5b38e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 12 Apr 2026 13:15:23 -0700 Subject: [PATCH 687/724] feat: `OpenPackage.View.FeatherIcon.svg` ( Fixes #203 ) --- Types/OpenPackage.View/FeatherIcon.svg.ps1 | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Types/OpenPackage.View/FeatherIcon.svg.ps1 diff --git a/Types/OpenPackage.View/FeatherIcon.svg.ps1 b/Types/OpenPackage.View/FeatherIcon.svg.ps1 new file mode 100644 index 0000000..75eecdc --- /dev/null +++ b/Types/OpenPackage.View/FeatherIcon.svg.ps1 @@ -0,0 +1,30 @@ +<# +.SYNOPSIS + Views Feather Icons +.DESCRIPTION + Shows a feather icon. +.NOTES + Icons will be cached in memory to avoid repeated CDN requests. +.EXAMPLE + $site.Includes.Feather "clipboard" +.LINK + https://feathericons.com/ +#> +param( +[string] +$Icon = 'chevron-right', + +[uri] +$FeatherCDN = "https://cdn.jsdelivr.net/gh/feathericons/feather@latest/icons/" +) + +if (-not $script:FeatherIconCache) { + $script:FeatherIconCache = [Ordered]@{} +} +$icon = $icon.ToLower() -replace '\.svg$' + +if (-not $script:FeatherIconCache[$icon]) { + $script:FeatherIconCache[$icon] = Invoke-RestMethod "$FeatherCDN/$Icon.svg" +} + +$script:FeatherIconCache[$icon].OuterXml From 6584f670e64d995a937cb466da10e6f09b7120cd Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 12 Apr 2026 20:15:41 +0000 Subject: [PATCH 688/724] feat: `OpenPackage.View.FeatherIcon.svg` ( Fixes #203 ) --- OP.types.ps1xml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index e0e1844..9e0ab90 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -8201,6 +8201,42 @@ filter at.markpub.markdown { } + + FeatherIcon.svg + + Markdown.html + + Help.md + + Markdown.html +"@ \ No newline at end of file From 8898a358658aa49fd1dde487a2e92fbbb0728139 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 12 Apr 2026 20:47:40 +0000 Subject: [PATCH 692/724] feat: `OpenPackage.View.Help.html` ( Fixes #205 ) --- OP.types.ps1xml | 292 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 72c1069..d6acc37 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -8237,6 +8237,298 @@ $script:FeatherIconCache[$icon].OuterXml + + Help.html + + Help.md + + + GitTag + From 07508fab6c6cc55a74f2014d2919469aa0683a47 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Sun, 12 Apr 2026 17:22:51 -0700 Subject: [PATCH 696/724] feat: `OpenPackage.Source.Directory` ( Fixes #116 ) Removing source directory relationship (potential for information disclosure outweighs potential for additional safety) --- Types/OpenPackage.Part/get_GitDate.ps1 | 16 ++++++++++------ Types/OpenPackage.Source/Directory.ps1 | 10 +--------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Types/OpenPackage.Part/get_GitDate.ps1 b/Types/OpenPackage.Part/get_GitDate.ps1 index eccf6f6..ddf9c31 100644 --- a/Types/OpenPackage.Part/get_GitDate.ps1 +++ b/Types/OpenPackage.Part/get_GitDate.ps1 @@ -3,6 +3,9 @@ Gets Package Part Git Dates .DESCRIPTION Gets any Git commit Dates associated with a package part. + + This must be run from within a git repository, + and the part source file must exist in the repository. .NOTES The package must have two relationships for this to work: @@ -17,8 +20,13 @@ if (-not $repoExists) { return } $sourceExists = $this.Package.RelationshipExists('directory') if (-not $sourceExists) { return } -$sourceLocation = $this.Package.GetRelationship('directory').TargetUri -replace '^file://' -$directory = Get-Item -LiteralPath $sourceLocation -ErrorAction Ignore +$gitApp = $executionContext.SessionState.InvokeCommand.GetCommand('git', 'application') + +if (-not $gitApp) { return } + +$gitRoot = try { & $gitApp rev-parse --show-toplevel *&>1} catch {$LASTEXITCODE = 0} + +$directory = Get-Item -LiteralPath $gitRoot -ErrorAction Ignore if (-not $directory) { return } $filePath = Join-Path $directory $this.Uri @@ -26,8 +34,4 @@ $file = Get-Item -LiteralPath $filePath -ErrorAction Ignore if (-not $file) { return } -$gitApp = $executionContext.SessionState.InvokeCommand.GetCommand('git', 'application') - -if (-not $gitApp) { return } - @(& $gitApp log --follow --format=%ci --date default $file.FullName *>&1) -as [datetime[]] diff --git a/Types/OpenPackage.Source/Directory.ps1 b/Types/OpenPackage.Source/Directory.ps1 index 50ee3f7..3e0ea6c 100644 --- a/Types/OpenPackage.Source/Directory.ps1 +++ b/Types/OpenPackage.Source/Directory.ps1 @@ -149,15 +149,7 @@ foreach ($resolvedItem in $resolvedItems) { $relation.TargetUri ) as [$($relation.RelationshipType)]$($relation.id)" } - } - - $sourceDirectoryUri = - "file://$($resolvedItem.FullName -replace '[\\/]', '/')" - - $relation = $package.Relate($sourceDirectoryUri,'source', 'directory') - Write-Verbose "Related $( - $relation.TargetUri - ) as [$($relation.RelationshipType)]$($relation.id)" + } # So declare an oldest created file and newest write time. $oldestCreationTime = [DateTime]::Now From 77766fffb65fe2b2b29bde12d8a67ac694389587 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 13 Apr 2026 00:23:11 +0000 Subject: [PATCH 697/724] feat: `OpenPackage.Source.Directory` ( Fixes #116 ) Removing source directory relationship (potential for information disclosure outweighs potential for additional safety) --- OP.types.ps1xml | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index b6cb29f..39288a0 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4150,6 +4150,9 @@ if (-not $this.Uri) { return } Gets Package Part Git Dates .DESCRIPTION Gets any Git commit Dates associated with a package part. + + This must be run from within a git repository, + and the part source file must exist in the repository. .NOTES The package must have two relationships for this to work: @@ -4164,8 +4167,13 @@ if (-not $repoExists) { return } $sourceExists = $this.Package.RelationshipExists('directory') if (-not $sourceExists) { return } -$sourceLocation = $this.Package.GetRelationship('directory').TargetUri -replace '^file://' -$directory = Get-Item -LiteralPath $sourceLocation -ErrorAction Ignore +$gitApp = $executionContext.SessionState.InvokeCommand.GetCommand('git', 'application') + +if (-not $gitApp) { return } + +$gitRoot = try { & $gitApp rev-parse --show-toplevel *&>1} catch {$LASTEXITCODE = 0} + +$directory = Get-Item -LiteralPath $gitRoot -ErrorAction Ignore if (-not $directory) { return } $filePath = Join-Path $directory $this.Uri @@ -4173,10 +4181,6 @@ $file = Get-Item -LiteralPath $filePath -ErrorAction Ignore if (-not $file) { return } -$gitApp = $executionContext.SessionState.InvokeCommand.GetCommand('git', 'application') - -if (-not $gitApp) { return } - @(& $gitApp log --follow --format=%ci --date default $file.FullName *>&1) -as [datetime[]] @@ -6615,15 +6619,7 @@ foreach ($resolvedItem in $resolvedItems) { $relation.TargetUri ) as [$($relation.RelationshipType)]$($relation.id)" } - } - - $sourceDirectoryUri = - "file://$($resolvedItem.FullName -replace '[\\/]', '/')" - - $relation = $package.Relate($sourceDirectoryUri,'source', 'directory') - Write-Verbose "Related $( - $relation.TargetUri - ) as [$($relation.RelationshipType)]$($relation.id)" + } # So declare an oldest created file and newest write time. $oldestCreationTime = [DateTime]::Now From 6e9f9989eda011195c4db4c63949d3409b240d53 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 13 Apr 2026 11:54:18 -0700 Subject: [PATCH 698/724] feat: `OpenPackage.Source.Url` ( Fixes #119 ) Improving handling of direct package download --- Types/OpenPackage.Source/Url.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Types/OpenPackage.Source/Url.ps1 b/Types/OpenPackage.Source/Url.ps1 index 3855ec6..336a21a 100644 --- a/Types/OpenPackage.Source/Url.ps1 +++ b/Types/OpenPackage.Source/Url.ps1 @@ -163,10 +163,10 @@ foreach ($uri in $url) { # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and # and starts with the magic pair of bytes indicate it might be a zip - ($WebResponse.Content[0] -eq 80 -and $WebResponse.Content[-1] -eq 75) + ($WebResponse.Content[0] -eq 80 -and $WebResponse.Content[1] -eq 75) ) { # Create a stream from the response - $memoryStream = [IO.MemoryStream]::new($downloadPackage.Content) + $memoryStream = [IO.MemoryStream]::new($WebResponse.Content) # and open the package $currentPackage = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') # If that did not work, it will error, From d2ecfbf45d6c55dce438b1bd0ae57d8ecdff09e1 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 13 Apr 2026 18:54:38 +0000 Subject: [PATCH 699/724] feat: `OpenPackage.Source.Url` ( Fixes #119 ) Improving handling of direct package download --- OP.types.ps1xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 39288a0..f2f5628 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -7812,10 +7812,10 @@ foreach ($uri in $url) { # If the web response is a byte array if ($WebResponse.Content -is [byte[]] -and # and starts with the magic pair of bytes indicate it might be a zip - ($WebResponse.Content[0] -eq 80 -and $WebResponse.Content[-1] -eq 75) + ($WebResponse.Content[0] -eq 80 -and $WebResponse.Content[1] -eq 75) ) { # Create a stream from the response - $memoryStream = [IO.MemoryStream]::new($downloadPackage.Content) + $memoryStream = [IO.MemoryStream]::new($WebResponse.Content) # and open the package $currentPackage = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite') # If that did not work, it will error, From 0303d3375d95bbbe9e08a16ad83e59aab8f27af4 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 14 Apr 2026 10:30:08 -0700 Subject: [PATCH 700/724] feat: `Copy-OpenPackage` ( Fixes #7 ) Removing unused variable --- Commands/Copy-OpenPackage.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Commands/Copy-OpenPackage.ps1 b/Commands/Copy-OpenPackage.ps1 index 3785b0c..f48e527 100644 --- a/Commands/Copy-OpenPackage.ps1 +++ b/Commands/Copy-OpenPackage.ps1 @@ -150,7 +150,7 @@ function Copy-OpenPackage $selectSplat[$key] = $PSBoundParameters[$key] } } - $inputPackageParts = $selectedParts = Select-OpenPackage @selectSplat + $inputPackageParts = Select-OpenPackage @selectSplat $inputPackageRelationships = @($InputObject.GetRelationships()) From c830095c9e82e09d7560e3b8c91bac0272f59384 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 14 Apr 2026 10:55:17 -0700 Subject: [PATCH 701/724] feat: `OpenPackage.Publisher.GitTag` ( Fixes #206 ) Trimming whitespace --- Types/OpenPackage.Publisher/GitTag.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Types/OpenPackage.Publisher/GitTag.ps1 b/Types/OpenPackage.Publisher/GitTag.ps1 index 33af530..0fe4f00 100644 --- a/Types/OpenPackage.Publisher/GitTag.ps1 +++ b/Types/OpenPackage.Publisher/GitTag.ps1 @@ -120,6 +120,4 @@ if (-not $githubEvent) { git @gitTagArgs # tag, git push origin --tags # push tags, $LASTEXITCODE = 0 # and set the last exit code. -} - - +} \ No newline at end of file From 6c1bd0595bdbd71558baec23441ef0edf7a098be Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 14 Apr 2026 17:55:38 +0000 Subject: [PATCH 702/724] feat: `OpenPackage.Publisher.GitTag` ( Fixes #206 ) Trimming whitespace --- OP.types.ps1xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index f2f5628..016eb03 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5478,9 +5478,6 @@ if (-not $githubEvent) { git push origin --tags # push tags, $LASTEXITCODE = 0 # and set the last exit code. } - - - From ea3f9db6fbc52e877d9d40347889b92378cff84e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 14 Apr 2026 11:04:01 -0700 Subject: [PATCH 703/724] feat: `OpenPackage.Publisher.GitHubRelease` ( Fixes #207 ) --- Types/OpenPackage.Publisher/GitHubRelease.ps1 | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 Types/OpenPackage.Publisher/GitHubRelease.ps1 diff --git a/Types/OpenPackage.Publisher/GitHubRelease.ps1 b/Types/OpenPackage.Publisher/GitHubRelease.ps1 new file mode 100644 index 0000000..e37cd14 --- /dev/null +++ b/Types/OpenPackage.Publisher/GitHubRelease.ps1 @@ -0,0 +1,224 @@ +<# +.SYNOPSIS + Publishes Releases to GitHub +.DESCRIPTION + Publishes a Release to GitHub, if the version does not already exist. +#> +[CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] +param( +# The package +[Parameter(Mandatory,ValueFromPipeline)] +[IO.Packaging.Package] +$Package, + +# One or more release asssets. These are files that are attached to the package. +[string[]] +$ReleaseAsset, + +# The GitHub Token used to publish the release. +# If there is no GitHubEvent, this is presumed to be a Personal Access Token. +[string] +$GitHubToken = $env:GH_TOKEN, + +# The GitHubOwner. +# If running in a workflow, this should be automatically detected. +# If running outside of a workflow, this must be provided. +[string] +$GitHubOwner = $env:GITHUB_OWNER, + +# The GitHub Repository, in the form of `owner/repo` +# If running in a workflow, this should be automatically detected. +# If running outside of a workflow, this must be provided. +[string] +$GitHubRepository = $env:GITHUB_REPOSITORY +) + +# If there is no package version, throw +if (-not $package.Version) { throw "No Package Version" } + +# If the package has no repository +if (-not $package.RelationshipExists('repository')) { + throw "Package not related to repository" # throw +} + +# Get the git app (as opposed to any git functions) +$gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git', 'application') +# If we could not get git +if (-not $gitApp) { + throw "No Git" # throw. +} + +# Get the repository remote url +$repositoryUrl = @(& $gitApp remote)[0] | + ForEach-Object { + & $gitApp remote get-url $_ + } + +# and throw if we could not +if (-not $repositoryUrl) { + throw "No Repository" +} + +# Make sure this package is related to the repository +$packageGitRepo = $package.GetRelationship('repository').TargetUri +if ($packageGitRepo -ne $repositoryUrl) { + # and throw if that is not the case. + throw "Package unrelated to '$repositoryUrl'" +} + + +# Get the github event +$gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json +} else { $null } + +# Warn about confirmation if we do not have one +if (-not $gitHubEvent) { + Write-Warning "No github event found, prompting for confirmation" +} else { + # If the event is not a merg, warn and return + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping github release" | Out-Host + return + } +} + +# Find the target version +$targetVersion = "v$($package.Version)" + +# Go get all of our releases +$releasesURL = "https://api.github.com/repos/$GitHubRepository/releases" + +# List out our source if we are running in a workflow. +if ($gitHubEvent) { + "Release URL: $releasesURL" | Out-Host +} + +# Go get the releases. +$listOfReleases = + Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{ + "Accept" = "application/vnd.github.v3+json" + "Authorization" = + if ($gitHubEvent) { + "Bearer $GitHubToken" + } elseif ($GitHubToken) { + "Basic $( + [System.Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$GitHubToken")) + )" + } + + } + +# and see if ours already exists +$releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion + +# If it does +if ($releaseExists) { + + if ($gitHubEvent) { + "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host + } else { + Write-Warning "'$($releaseExists.Name )' Already Exists" + } + + # store this to a variable so that we may potentially add assets. + $releasedIt = $releaseExists +} else { + # If no release exists yet, + # prepare our parameters + $releaseParameters = [Ordered]@{ + Uri = $releasesURL + Method = 'POST' + Body = [Ordered]@{ + owner = "$gitHubOwner" + repo = "$GitHubRepository" + tag_name = $targetVersion + name = "$($Package.Identifier) $($package.Version)" + body = + if ($env:RELEASENOTES) { + $env:RELEASENOTES + } elseif ($package.PowerShellManifest.PrivateData.PSData.ReleaseNotes) { + $package.PowerShellManifest.PrivateData.PSData.ReleaseNotes + } else { + "$($package.Identifier) $targetVersion" + } + draft = + if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false } + prerelease = + if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false } + } + } + + # If -WhatIf was passed, return the release parameters + if ($whatIfPreference) { + return $releaseParameters + } + + # If there is no github event, prompt before releasing. + if (-not $gitHubEvent -and -not $psCmdlet.ShouldProcess("Release $($releaseParameters.body.name)")) { + return + } + + # Create the release + $releaseParameters.body = $releaseParameters.body | ConvertTo-Json -Depth 10 + $ReleaseHeaders = @{ + "Accept" = "application/vnd.github.v3+json" + "Content-type" = "application/json" + "Authorization" = if ($gitHubEvent) { + "Bearer $GitHubToken" + } else { + "Basic $( + [System.Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$GitHubToken")) + )" + } + } + + $releasedIt = Invoke-RestMethod @releaseParameters -Headers $releaseHeaders +} + +if (-not $releasedIt) { + throw "Release failed" +} else { + $releasedIt | Out-Host +} + + + +if ($ReleaseAsset) { + $releaseUploadUrl = $releasedIt.upload_url -replace '\{.+$' + + $filesToRelease = + Get-ChildItem -File -Path $ReleaseAsset -ErrorAction Ignore + + $releasedFiles = @{} + foreach ($file in $filesToRelease) { + if ($releasedFiles[$file.Name]) { + Write-Warning "Already attached file $($file.Name)" + continue + } + else { + # If there is no github event, prompt before attaching. + if (-not $gitHubEvent -and -not $psCmdlet.ShouldProcess("Attach $($file.name)")) { + continue + } + $fileBytes = [IO.File]::ReadAllBytes($file.FullName) + $releasedFiles[$file.Name] = + Invoke-RestMethod -Uri "${releaseUploadUrl}?name=$($file.Name)" -Headers @{ + "Accept" = "application/vnd.github+json" + "Authorization" = if ($GitHubEvent) { + "Bearer $GitHubToken" + } else { + "Basic $( + [System.Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$GitHubToken")) + )" + } + } -Body $fileBytes -ContentType Application/octet-stream + $releasedFiles[$file.Name] + } + } + + if ($gitHubEvent) { + "Attached $($releasedFiles.Count) file(s) to release" | Out-Host + } +} \ No newline at end of file From 6ac175fb886f75a87584993fdeac0de5eef5168e Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Tue, 14 Apr 2026 18:04:19 +0000 Subject: [PATCH 704/724] feat: `OpenPackage.Publisher.GitHubRelease` ( Fixes #207 ) --- OP.types.ps1xml | 229 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 016eb03..ac3fb85 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -5352,6 +5352,235 @@ npx '@11ty/eleventy' Pop-Location + + GitHubRelease + + GitTag + + PowerShellGallery + + Site

    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 Z7 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 V?Rpbk*9(7fi5fOKqA=XL=KpHBecAv3 literal 0 HcmV?d00001 diff --git a/Assets/OP-Gradient-Text.svg b/Assets/OP-Gradient-Text.svg new file mode 100644 index 0000000..95f7ebd --- /dev/null +++ b/Assets/OP-Gradient-Text.svg @@ -0,0 +1,20 @@ + + + + + + + + + + OP-Gradient-Text + + + + + + OP +OP + + + \ No newline at end of file diff --git a/Assets/OP-Gradient.svg b/Assets/OP-Gradient.svg new file mode 100644 index 0000000..3420d4f --- /dev/null +++ b/Assets/OP-Gradient.svg @@ -0,0 +1,12 @@ + + + + + + + + + + OP-Gradient + + \ No newline at end of file diff --git a/Assets/OP-Text.png b/Assets/OP-Text.png new file mode 100644 index 0000000000000000000000000000000000000000..20fbb5a054b02efd51410889d3aa3e5e507580b7 GIT binary patch literal 146316 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 + + OP-Text + + + + + + OP +OP + + + \ No newline at end of file diff --git a/Assets/OP.png b/Assets/OP.png new file mode 100644 index 0000000000000000000000000000000000000000..f76b435b96760060a186e19dd23d0fcbbb4342f0 GIT binary patch literal 134267 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 literal 0 HcmV?d00001 diff --git a/Assets/OP.svg b/Assets/OP.svg new file mode 100644 index 0000000..895db17 --- /dev/null +++ b/Assets/OP.svg @@ -0,0 +1,5 @@ + + + OP + + \ No newline at end of file diff --git a/OP.turtle.ps1 b/OP.turtle.ps1 new file mode 100644 index 0000000..58357b6 --- /dev/null +++ b/OP.turtle.ps1 @@ -0,0 +1,97 @@ +#requires -Module Turtle +param( +[double] +$Size = 1080, + +[string[]] +$Variants = @( + '' + 'Text' + 'Animated' + 'Animated-Text' + 'Gradient' + 'Gradient-Text' + 'Animated-Gradient' + 'Animated-Gradient-Text' + 'Blue' + 'Blue-Text' + 'Animated-Blue' + 'Animated-Blue-Text' +), + +[string[]] +$PngVariants = @( + '' + 'Text' + 'Blue-Text' + 'Gradient-Text' +), + +[string] +$Destination = './Assets' +) + + +$halfSize = $size / 2 + +if ($psScriptRoot) { + Push-Location $PSScriptRoot +} + +if (-not (Test-Path $Destination)) { + $null = New-Item -ItemType Directory -Path $Destination +} + + +foreach ($variant in $variants) { + $fileName = "OP-$variant" -replace '-$' + + $Logo = + 🐢 id $fileName title $fileName Arcygon $size $halfSize 4 StrokeWidth '1%' + + if ($variant -match 'Animated') { + $logo = $logo | 🐢 duration '00:00:42' morph @( + 🐢 @('circleArc', $halfSize, 90, 'forward', $size, 'rotate',90 * 4) + 🐢 @('circleArc', $halfSize, -90, 'forward', $size, 'rotate',90 * 4) + 🐢 @('circleArc', $halfSize, 90, 'forward', $size, 'rotate',90 * 4) + ) + } + + if ($variant -match 'Text') { + $logo = $logo | + 🐢 turtles @( + 🐢 viewbox $halfSize text OP FontSize ($size/4) FontFamily 'sans-serif' @( + if ($variant -match 'Text') { + 'stroke', '#4488ff' + } + elseif ($variant -match 'Gradient') { + 'stroke', '#224488', '#4488ff', '#224488' + } + ) + ) + } + + if ($variant -match 'Blue') { + $logo.Stroke = '#4488ff' + } + + if ($variant -match 'Gradient') { + $logo.Stroke = '#224488', '#4488ff', '#224488' + } + + + $fileName = "OP-$variant" -replace '-$' + + $logo | Save-Turtle -FilePath ( + Join-Path $destination "./$fileName.svg" + ) + + if ($pngVariants -contains $variant) { + $logo | Save-Turtle -FilePath ( + Join-Path $destination "./$fileName.png" + ) + } +} + +return + From 09186b699df348e9f2c081db6198514ade665e72 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 12:29:56 -0700 Subject: [PATCH 665/724] feat: `OpenPackage.Publisher.com.atproto.server.createSession` ( Fixes #197 ) --- Types/OpenPackage.Publisher/Alias.psd1 | 2 + .../com.atproto.server.createSession.ps1 | 107 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 Types/OpenPackage.Publisher/com.atproto.server.createSession.ps1 diff --git a/Types/OpenPackage.Publisher/Alias.psd1 b/Types/OpenPackage.Publisher/Alias.psd1 index 9c41bf9..6311d18 100644 --- a/Types/OpenPackage.Publisher/Alias.psd1 +++ b/Types/OpenPackage.Publisher/Alias.psd1 @@ -1,3 +1,5 @@ @{ "11ty" = "Eleventy" + 'connectAt' = 'com.atproto.server.createSession' + # 'atRecord' = 'com.atproto.repo.createRecord' } \ No newline at end of file diff --git a/Types/OpenPackage.Publisher/com.atproto.server.createSession.ps1 b/Types/OpenPackage.Publisher/com.atproto.server.createSession.ps1 new file mode 100644 index 0000000..ab2b8e6 --- /dev/null +++ b/Types/OpenPackage.Publisher/com.atproto.server.createSession.ps1 @@ -0,0 +1,107 @@ +<# +.SYNOPSIS + Creates an at protocol server session +.DESCRIPTION + Creates an at protocol server session. +.NOTES + This can be used by other publishers in order to connect to at protocol. +.LINK + https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/server/createSession.json +#> +[CmdletBinding(PositionalBinding=$false,SupportsShouldProcess)] +param( +# Handle or other identifier supported by the server for the authenticating user. +[Parameter(Mandatory,ParameterSetName='IdentifierAndPassword')] +[string] +$Identifier, + +# The app password or account password. +[Parameter(Mandatory,ParameterSetName='IdentifierAndAppPassword')] +[string] +$AppPassword, + +# A credential used to connect. +# The username will be treated as the `-Identifier`. +# The password will be treated as the `-AppPassword` +[Parameter(Mandatory,ParameterSetName='Credential')] +[Management.Automation.PSCredential] +$Credential, + +# The personal data server used for the connection. +[Alias('PersonalDataServer')] +[string] +$PDS = "https://bsky.social/" +) + +# Declare the namespace ID +$NamespaceID = 'com.atproto.server.createSession' +# and the HTTP method used to connect +$httpMethod = 'POST' + +# Create a full url using the PDS and NamespaceId +$authenicationUrl = "$( + # If the pds started with https:// + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls. + $pds -replace '/$' + } else { + # Prefix anything else by https:// + "https://$pds" -replace '/$' +})/xrpc/$NamespaceID" + +# Prepare our parameters to Invoke-RestMethod +$atSplat = [Ordered]@{ + Uri = $authenicationUrl + Method = $httpMethod + ContentType='application/json' + Body = [Ordered]@{} +} + +# Set our identifier and password +$atSplat.body.identifier, $atSplat.body.password = + if ($Identifier -and $AppPassword) { + $Identifier, $AppPassword + } + elseif ($Credential) { + $Credential.UserName + $Credential.GetNetworkCredential().Password + } else { + Write-Warning "Missing authentication details. Provide a -Credential or -Identity and -AppPassword." + return + } + +# and convert our body into json. +$atSplat.body = $atSplat.body | ConvertTo-Json -Depth 4 + +# If `-WhatIf` was passed +if ($WhatIfPreference) { + # remove sensitive information + $atSplat.Remove('Body') + return $atSplat # and return the splat. +} + +# Otherwise, connect. +$authenticated = Invoke-RestMethod @atSplat + +# If the connection does not have an accessJwt, return +if (-not $authenticated.accessJwt) { return } + +# Force `atproto.session` objects to only display the handle and did by default +$updateTypeDataSplat = [Ordered]@{ + Force=$true + TypeName='atproto.session' + DefaultDisplayPropertySet = 'handle','did' +} + +# This prevents sensitive information from being displayed by default +# (like the email or the accessJwt) +Update-TypeData @updateTypeDataSplat + +# Decorate our return data +$authenticated.pstypenames.add('atproto.session') +# and return our authenticated object. +return $authenticated + + + + From 58adc55be69c5279c85ecf292dc209506d900999 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 19:30:19 +0000 Subject: [PATCH 666/724] feat: `OpenPackage.Publisher.com.atproto.server.createSession` ( Fixes #197 ) --- OP.types.ps1xml | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 711bcf5..a7c1d32 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4583,6 +4583,10 @@ Reader 11ty Eleventy + + connectAt + com.atproto.server.createSession + at.markpub.markdown + + + com.atproto.server.createSession + From a54c6f7804d79406f67eb4232641882bf50a02df Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 12:34:22 -0700 Subject: [PATCH 667/724] feat: `OpenPackage.Publisher.com.atproto.server.createSession` ( Fixes #197 ) Adding ScriptAnalyzer exception and justification --- .../com.atproto.server.createSession.ps1 | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Types/OpenPackage.Publisher/com.atproto.server.createSession.ps1 b/Types/OpenPackage.Publisher/com.atproto.server.createSession.ps1 index ab2b8e6..5fdf2ba 100644 --- a/Types/OpenPackage.Publisher/com.atproto.server.createSession.ps1 +++ b/Types/OpenPackage.Publisher/com.atproto.server.createSession.ps1 @@ -9,6 +9,14 @@ https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/server/createSession.json #> [CmdletBinding(PositionalBinding=$false,SupportsShouldProcess)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + "PSAvoidUsingPlainTextForPassword", + "", + Justification=" + SecureStrings are not actually more secure. + Use -Credential to avoid potential information disclosure in Windows event logs. + " +)] param( # Handle or other identifier supported by the server for the authenticating user. [Parameter(Mandatory,ParameterSetName='IdentifierAndPassword')] @@ -100,8 +108,4 @@ Update-TypeData @updateTypeDataSplat # Decorate our return data $authenticated.pstypenames.add('atproto.session') # and return our authenticated object. -return $authenticated - - - - +return $authenticated \ No newline at end of file From be1e5c983c32fe02835ccaa502a1af67980afc09 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 19:34:43 +0000 Subject: [PATCH 668/724] feat: `OpenPackage.Publisher.com.atproto.server.createSession` ( Fixes #197 ) Adding ScriptAnalyzer exception and justification --- OP.types.ps1xml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index a7c1d32..1beb410 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4668,6 +4668,14 @@ New-Item -ItemType File -Path $openPackageXrpcPath -Value ( https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/server/createSession.json #> [CmdletBinding(PositionalBinding=$false,SupportsShouldProcess)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + "PSAvoidUsingPlainTextForPassword", + "", + Justification=" + SecureStrings are not actually more secure. + Use -Credential to avoid potential information disclosure in Windows event logs. + " +)] param( # Handle or other identifier supported by the server for the authenticating user. [Parameter(Mandatory,ParameterSetName='IdentifierAndPassword')] @@ -4760,11 +4768,6 @@ Update-TypeData @updateTypeDataSplat $authenticated.pstypenames.add('atproto.session') # and return our authenticated object. return $authenticated - - - - - From 350f6b177639aace35564f528b5ac2f02dece59d Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 12:50:30 -0700 Subject: [PATCH 669/724] feat: `Publish-OpenPackage` ( Fixes #172 ) Proxying -WhatIf and -Confirm --- Commands/Publish-OpenPackage.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Commands/Publish-OpenPackage.ps1 b/Commands/Publish-OpenPackage.ps1 index ccfafd0..c3446f1 100644 --- a/Commands/Publish-OpenPackage.ps1 +++ b/Commands/Publish-OpenPackage.ps1 @@ -124,6 +124,10 @@ function Publish-OpenPackage { $validParameterNames = @( $commandMetaData.Parameters.Keys $commandMetaData.Parameters.Values.Aliases + if ($commandMetaData.SupportsShouldProcess) { + 'WhatIf' + 'Confirm' + } ) # We want to be forgiving with input, so copy the options From 05d7d27e85304f870d180266427ad5c3bb656f84 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 13:14:02 -0700 Subject: [PATCH 670/724] feat: `OpenPackage.Publisher.com.atproto.repo.createRecord` ( Fixes #198 ) --- Types/OpenPackage.Publisher/Alias.psd1 | 4 +- .../com.atproto.repo.createRecord.ps1 | 212 ++++++++++++++++++ 2 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 Types/OpenPackage.Publisher/com.atproto.repo.createRecord.ps1 diff --git a/Types/OpenPackage.Publisher/Alias.psd1 b/Types/OpenPackage.Publisher/Alias.psd1 index 6311d18..83757d9 100644 --- a/Types/OpenPackage.Publisher/Alias.psd1 +++ b/Types/OpenPackage.Publisher/Alias.psd1 @@ -1,5 +1,5 @@ @{ "11ty" = "Eleventy" - 'connectAt' = 'com.atproto.server.createSession' - # 'atRecord' = 'com.atproto.repo.createRecord' + 'atSession' = 'com.atproto.server.createSession' + 'atRecord' = 'com.atproto.repo.createRecord' } \ No newline at end of file diff --git a/Types/OpenPackage.Publisher/com.atproto.repo.createRecord.ps1 b/Types/OpenPackage.Publisher/com.atproto.repo.createRecord.ps1 new file mode 100644 index 0000000..4df84ed --- /dev/null +++ b/Types/OpenPackage.Publisher/com.atproto.repo.createRecord.ps1 @@ -0,0 +1,212 @@ +<# +.SYNOPSIS + Publishes Records to At Protocol +.DESCRIPTION + Publishes one or more records to the at protocol. +.LINK + https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/repo/createRecord.json +#> +[CmdletBinding(PositionalBinding=$false,SupportsShouldProcess)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + "PSAvoidUsingPlainTextForPassword", + "", + Justification=" + SecureStrings are not actually more secure. + Use -Credential to avoid potential information disclosure in Windows event logs. + " +)] +param( +# Handle or other identifier supported by the server for the authenticating user. +[Parameter(Mandatory,ParameterSetName='IdentifierAndPassword')] +[string] +$Identifier, + +# The app password or account password. +[Parameter(Mandatory,ParameterSetName='IdentifierAndAppPassword')] +[string] +$AppPassword, + +# A credential used to connect. +# The username will be treated as the `-Identifier`. +# The password will be treated as the `-AppPassword` +[Parameter(Mandatory,ParameterSetName='Credential')] +[Management.Automation.PSCredential] +$Credential, + +# The personal data server used for the connection. +[Alias('PersonalDataServer')] +[string] +$PDS = "https://bsky.social/", + +# The record key. +# This does not need to be provided. +# If no record key is provided, +# records will be created with a TimeStamp Identifier (`tid`) +# See https://atproto.com/specs/tid +# Individual records may also contain a `rkey`. +# If the record includes an `rkey`, this will be used. +[Alias('RecordKey')] +[string] +$RKey, + +# Compare and swap with the previous commit by CID. +[string] +$SwapCommmit, + +# Can be set to 'false' to skip Lexicon schema validation of record data, +# 'true' to require it, or leave unset to validate only for known Lexicons. +[switch] +$Validate, + +# Any input to post. +# Only input with a `$type` property will be posted. +[Parameter(ValueFromPipeline)] +[Alias('Package')] +[PSObject[]] +$InputObject +) + +# Quick collect all piped input +$allInput = @($input) + +# If that had nothing, add any non-piped input objects +if (-not $allInput) { + $allInput += $InputObject +} + +# If we still have no input, +if (-not $allInput) { + # error out. + Write-Error "No input to publish" + return +} + +# Declare a filter to create our records. +filter createRecord { + $NamespaceID = 'com.atproto.repo.createRecord' + $httpMethod = 'POST' + $createAtRecord = $_ + # If the record we want to create has no `$type' + if (-not $createAtRecord.'$type') { + # warn and return + Write-Warning 'No $type, will not createRecord' + return + } + + # Construct our create url + $createUrl = "$( + # If the PDS was `https://`, + if ($pds -like 'https://*') { + # just trim trailing slashes from https urls + $pds -replace '/$' + } else { + # Otherwise prefix it by https:// + "https://$pds" -replace '/$' + })/xrpc/$NamespaceID" + + # Prepare our parameters + $invokeSplat = [Ordered]@{ + Uri = $createUrl + Body = [Ordered]@{} + Method = $httpMethod + ContentType='application/json' + } + + # If there was no connection + if (-not $atConnection) { + Write-Warning "Not connected!" # warn them + return $invokeSplat # and return our parameters. + } + + # If the connection had a did + if ($atConnection.did) { + # This becomes the `repo` + $invokeSplat.Body['repo'] = $atConnection.did + } + + # The collection will always be the record `$type` + $invokeSplat.Body.collection = $createAtRecord.'$type' + + # If we have explicitly provided an rkey + if ($RKey) { + $invokeSplat.Body.rkey = $RKey # use that + } + # Otherwise, if the record explicitly provides an rkey + elseif ($createAtRecord.rkey) { + # use that instead. + $invokeSplat.Body.rkey = $createAtRecord.rkey + } + + # If we want to validate, or the record indicates it should + if ($validate -or $createAtRecord.validate) { + # then we will validate. + $invokeSplat.Body.validate = $true + } + + # If we explicitly provide a swap commit + if ($SwapCommmit) { + # use it. + $invokeSplat.Body.swapCommit = $SwapCommmit + } + # If the record provides a swap commit + elseif ($createAtRecord.swapCommit) { + # use that instead + $invokeSplat.Body.swapCommit = $createAtRecord.swapCommit + } + + # Last but not least, put our record into `.record`. + $invokeSplat.Body.record = $createAtRecord + + # If -WhatIf was passed + if ($WhatIfPreference) { + # return the splat. + return $invokeSplat + } + + # Convert the body to json, + $invokeSplat.Body = $invokeSplat.Body | + ConvertTo-Json -Depth 100 + + # add our bearer token + $invokeSplat.Headers = [Ordered]@{Authorization="Bearer $($atConnection.accessJwt)"} + + # and create the record. + Invoke-RestMethod @invokeSplat +} + + +# Reset any potential connection. +$atConnection = $null +# and then see if we have enough data to connect. +if (-not $atConnection -and ( + ($Identifier -and $appPassword) -or + ($Credential) +)) { + # If we do, prepare a splat + $connectionSplat = [Ordered]@{} + if ($identifier -and $AppPassword) { + $connectionSplat.Identifier = $Identifier + $connectionSplat.AppPassword = $AppPassword + } + else { + $connectionSplat.Credential = $Credential + } + + # and connect. + $atConnection = Publish-OpenPackage -Publisher com.atproto.server.createSession -Option $connectionSplat +} + + +#region Create Records +$InputNumber = 0 +:nextInput foreach ($in in $allInput) { + if ($in.'$type') { + $in | createRecord + } else { + Write-Warning "Input # $InputNumber is missing a '`$type'" + } + $InputNumber++ +} +#endregion Create Records + +$atConnection = $null \ No newline at end of file From 6dcfee3bf4bf1e445fbd9d712f0dd8b5155659e0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 20:14:21 +0000 Subject: [PATCH 671/724] feat: `OpenPackage.Publisher.com.atproto.repo.createRecord` ( Fixes #198 ) --- OP.types.ps1xml | 223 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 222 insertions(+), 1 deletion(-) diff --git a/OP.types.ps1xml b/OP.types.ps1xml index 1beb410..de176a9 100644 --- a/OP.types.ps1xml +++ b/OP.types.ps1xml @@ -4584,7 +4584,11 @@ Reader Eleventy - connectAt + atRecord + com.atproto.repo.createRecord + + + atSession com.atproto.server.createSession @@ -4654,6 +4658,223 @@ New-Item -ItemType File -Path $openPackageXrpcPath -Value ( ) -Force + + com.atproto.repo.createRecord + + com.atproto.server.createSession + + com.atproto.repo.uploadBlob + + com.atproto.server.createSession From 0b5e6224ae8fadc9f4ea135e861b60536a9ec83c Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Fri, 10 Apr 2026 16:40:07 -0700 Subject: [PATCH 683/724] feat: `OpenPackage.View.site.standard.document` ( Fixes #200 ) --- .../site.standard.document.ps1 | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 Types/OpenPackage.View/site.standard.document.ps1 diff --git a/Types/OpenPackage.View/site.standard.document.ps1 b/Types/OpenPackage.View/site.standard.document.ps1 new file mode 100644 index 0000000..40eba93 --- /dev/null +++ b/Types/OpenPackage.View/site.standard.document.ps1 @@ -0,0 +1,146 @@ +<# +.SYNOPSIS + Views input as `standard.site.document` +.DESCRIPTION + Views any applicable input object as a `standard.site.document` object +.INPUTS + OpenPackage +.INPUTS + OpenPackage.Part +.INPUTS + string +.LINK + https://standard.site/ +#> +param() + +$allInput = @($input) + @($args) + +# Make one quick pass over all input +$allInput = @( + foreach ($in in $allInput) { + # and expand any packages we find into their parts. + if ($in -is [IO.Packaging.Package]) { + $in.GetParts() + } + elseif ($in.Package -is [IO.Packaging.Package]) { + $in.Package.GetParts() + } + else { + $in + } + } +) + +# Declare a filter + +# Now, let's go over all input +:nextInput foreach ($in in $allInput) { + # If the input is a string + if ($in -is [string]) { + # Just take the markdown + # and put it in an at.markpub.markdown object + + # and continue to the next input. + $in | at.markpub.markdown + continue nextInput + } + + # If the input has a `$type`, and it is `site.standard.document` + if ($in.'$type' -eq 'site.standard.document') { + $in # pass the input thru + continue nextInput # and continue to the next input. + } + + # If the input is a package part, we can do more + if ( + $in -is [IO.Packaging.PackagePart] -and + # (if it is not a markdown file, we should ignore it). + $in.Uri -match '(?>\.md|\.markdown)$' + ) { + # Get our markdown files as `at.markpub.markdown` + $atMarkPub = $in | Format-OpenPackage -View at.markpub.markdown + # and create a standard site document to hold them. + $standardSiteDocument = [Ordered]@{'$type' = 'site.standard.document'} + # the content is the `at.markpub.markdown` + $standardSiteDocument.content = $atMarkPub + # and we want to propagate various front-matter into the site: + if ($atMarkPub.frontMatter) { + # * `title` + if ($atMarkPub.frontMatter.title) { + $standardSiteDocument.title = $atMarkPub.frontMatter.title + } + + # * `description` + if ($atMarkPub.frontMatter.description) { + $standardSiteDocument.title = $atMarkPub.frontMatter.description + } + + # * `tags` + if ($atMarkPub.frontMatter.tags) { + $standardSiteDocument.tags = @( + $atMarkPub.frontMatter.tags + ) + } + + # * `path` + if ($atMarkPub.frontMatter.path) { + $standardSiteDocument.path = + $atMarkPub.frontMatter.path -replace '^/?', '/' + } + + # * `image` to `coverImage` + if ($atMarkPub.frontMatter.image) { + # * Please note that at this point our image should be a url. + $standardSiteDocument.coverImage = $atMarkPub.frontMatter.image + } + + # * `bskyPostRef` + if ($atMarkPub.frontMatter.bskyPostRef) { + $standardSiteDocument.bskyPostRef = $atMarkPub.frontMatter.bskyPostRef + } + } + + # Now get our inner text + $innerText = + ("