< Summary - Kestrun — Combined Coverage

Information
Class: Public.Service.New-KrServicePackage
Assembly: Kestrun.PowerShell.Public
File(s): /home/runner/work/Kestrun/Kestrun/src/PowerShell/Kestrun/Public/Service/New-KrServicePackage.ps1
Tag: Kestrun/Kestrun@6135d944f8787fb570e4dfbacac6e80312799a86
Line coverage
89%
Covered lines: 144
Uncovered lines: 17
Coverable lines: 161
Total lines: 509
Line coverage: 89.4%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 03/26/2026 - 03:54:59 Line coverage: 89.7% (79/88) Total lines: 272 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d04/19/2026 - 15:52:57 Line coverage: 89.4% (144/161) Total lines: 509 Tag: Kestrun/Kestrun@765a8f13c573c01494250a29d6392b6037f087c9

Coverage delta

Coverage delta 1 -1

Metrics

File(s)

/home/runner/work/Kestrun/Kestrun/src/PowerShell/Kestrun/Public/Service/New-KrServicePackage.ps1

#LineLine coverage
 1<#
 2.SYNOPSIS
 3    Creates a Kestrun service package (.krpack).
 4.DESCRIPTION
 5    Creates a .krpack archive from either:
 6    - A source folder that already contains Service.psd1 (validated before packaging), or
 7    - A script file plus Name/Version metadata (a Service.psd1 descriptor is generated automatically).
 8
 9    For generated descriptors, FormatVersion is set to '1.0' and EntryPoint is set to the script file name.
 10.PARAMETER SourceFolder
 11    Folder to package. Must contain a valid Service.psd1 descriptor.
 12.PARAMETER ScriptPath
 13    Script file to package. A Service.psd1 descriptor is generated automatically.
 14.PARAMETER Name
 15    Service name used when generating Service.psd1 from ScriptPath.
 16    If omitted, defaults to the script filename without extension.
 17.PARAMETER Version
 18    Service version used when generating Service.psd1 from ScriptPath.
 19.PARAMETER Description
 20    Optional description used when generating Service.psd1 from ScriptPath.
 21    Defaults to Name.
 22.PARAMETER ServiceLogPath
 23    Optional ServiceLogPath written to generated Service.psd1.
 24.PARAMETER PreservePaths
 25    Optional relative file/folder paths to preserve during service update.
 26.PARAMETER ApplicationDataFolders
 27    Optional relative application-data folders to preserve during service update.
 28.PARAMETER ExcludeApplicationDataFolders
 29    In SourceFolder mode, excludes files under descriptor ApplicationDataFolders from the package archive.
 30    The descriptor values are kept unchanged so those folders can still be preserved during service update.
 31.PARAMETER ExcludePaths
 32    In SourceFolder mode, excludes specific relative files or folders from the package archive.
 33    Paths must stay under SourceFolder and cannot exclude Service.psd1 or the EntryPoint file.
 34.PARAMETER OutputPath
 35    Output .krpack path.
 36    Defaults:
 37    - SourceFolder mode: <SourceFolderName>.krpack in current directory
 38    - ScriptPath mode: <Name>-<Version>.krpack in current directory
 39.PARAMETER Force
 40    Overwrite an existing output file.
 41.PARAMETER WhatIf
 42    Shows what would happen if the cmdlet runs. The cmdlet is not executed.
 43.PARAMETER Confirm
 44    Prompts for confirmation before running the cmdlet.
 45.EXAMPLE
 46    New-KrServicePackage -SourceFolder .\my-service -OutputPath .\my-service.krpack
 47.EXAMPLE
 48    New-KrServicePackage -ScriptPath .\server.ps1 -Name demo -Version 1.2.0 -OutputPath .\demo.krpack
 49.EXAMPLE
 50    New-KrServicePackage -ScriptPath .\server.ps1 -Version 1.2.0
 51.EXAMPLE
 52    New-KrServicePackage -SourceFolder .\my-service -ExcludeApplicationDataFolders -ExcludePaths @('secrets/dev.json', '
 53#>
 54function New-KrServicePackage {
 55    [KestrunRuntimeApi('Everywhere')]
 56    [CmdletBinding(DefaultParameterSetName = 'FromFolder', SupportsShouldProcess)]
 57    [OutputType([pscustomobject])]
 58    param(
 59        [Parameter(Mandatory, ParameterSetName = 'FromFolder')]
 60        [ValidateNotNullOrEmpty()]
 61        [string]$SourceFolder,
 62
 63        [Parameter(Mandatory, ParameterSetName = 'FromScript')]
 64        [ValidateNotNullOrEmpty()]
 65        [string]$ScriptPath,
 66
 67        [Parameter(ParameterSetName = 'FromScript')]
 68        [ValidateNotNullOrEmpty()]
 69        [string]$Name,
 70
 71        [Parameter(Mandatory, ParameterSetName = 'FromScript')]
 72        [ValidateNotNullOrEmpty()]
 73        [version]$Version,
 74
 75        [Parameter(ParameterSetName = 'FromScript')]
 76        [string]$Description,
 77
 78        [Parameter(ParameterSetName = 'FromScript')]
 79        [string]$ServiceLogPath,
 80
 81        [Parameter(ParameterSetName = 'FromScript')]
 82        [string[]]$PreservePaths,
 83
 84        [Parameter(ParameterSetName = 'FromScript')]
 85        [string[]]$ApplicationDataFolders,
 86
 87        [Parameter(ParameterSetName = 'FromFolder')]
 88        [switch]$ExcludeApplicationDataFolders,
 89
 90        [Parameter(ParameterSetName = 'FromFolder')]
 91        [string[]]$ExcludePaths,
 92
 93        [Parameter()]
 94        [ValidateNotNullOrEmpty()]
 95        [string]$OutputPath,
 96
 97        [Parameter()]
 98        [switch]$Force
 99    )
 100
 101    <#
 102    .SYNOPSIS
 103        Resolves the output path for the .krpack file.
 104    .DESCRIPTION
 105        Determines the full path for the output .krpack file based on the provided path or a default base name.
 106    .PARAMETER ProvidedOutputPath
 107        The user-provided output path.
 108    .PARAMETER DefaultBaseName
 109        The default base name to use if no output path is provided.
 110    .EXAMPLE
 111        Get-KrResolvedOutputPath -ProvidedOutputPath .\output.krpack -DefaultBaseName demo
 112    .EXAMPLE
 113        Get-KrResolvedOutputPath -ProvidedOutputPath '' -DefaultBaseName demo
 114    .EXAMPLE
 115        Get-KrResolvedOutputPath -ProvidedOutputPath $null -DefaultBaseName demo
 116    .OUTPUTS
 117        [string] The resolved full path for the .krpack file.
 118    #>
 119    function Get-KrResolvedOutputPath {
 120        param(
 121            [string]$ProvidedOutputPath,
 122            [string]$DefaultBaseName
 123        )
 124
 1125        $resolved = if ([string]::IsNullOrWhiteSpace($ProvidedOutputPath)) {
 2126            [System.IO.Path]::Combine((Get-Location).Path, "$DefaultBaseName.krpack")
 127        } else {
 1128            [System.IO.Path]::GetFullPath($ProvidedOutputPath)
 129        }
 130
 1131        if (-not $resolved.EndsWith('.krpack', [System.StringComparison]::OrdinalIgnoreCase)) {
 0132            $resolved = "$resolved.krpack"
 133        }
 134
 1135        return $resolved
 136    }
 137
 138    <#
 139    .SYNOPSIS
 140        Validates and resolves a relative package path.
 141    .DESCRIPTION
 142        Resolves a relative path under the package root and optionally requires that the path exists.
 143    .PARAMETER PackageRoot
 144        The package root directory.
 145    .PARAMETER RelativePath
 146        The relative file or folder path to validate.
 147    .PARAMETER PathLabel
 148        Label used in error messages.
 149    .PARAMETER RequireExisting
 150        Requires the path to exist under the package root.
 151    .PARAMETER RejectPackageRoot
 152        Rejects paths that resolve to the package root itself.
 153    .OUTPUTS
 154        [pscustomobject] with FullPath, RelativePath, and IsDirectory properties.
 155    #>
 156    function Get-KrPackagePathInfo {
 157        param(
 158            [string]$PackageRoot,
 159            [string]$RelativePath,
 160            [string]$PathLabel,
 161            [switch]$RequireExisting,
 162            [switch]$RejectPackageRoot
 163        )
 164
 1165        if ([string]::IsNullOrWhiteSpace($RelativePath)) {
 0166            throw "$PathLabel cannot contain empty values."
 167        }
 168
 1169        if ([System.IO.Path]::IsPathRooted($RelativePath)) {
 0170            throw "$PathLabel entry '$RelativePath' must be a relative path."
 171        }
 172
 1173        $packageRootFullPath = [System.IO.Path]::GetFullPath($PackageRoot)
 1174        $packageRootNormalized = [System.IO.Path]::TrimEndingDirectorySeparator($packageRootFullPath)
 1175        $combinedPath = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($packageRootFullPath, $RelativePath))
 1176        $relativeToRoot = [System.IO.Path]::GetRelativePath($packageRootNormalized, $combinedPath)
 177
 1178        if ([System.IO.Path]::IsPathRooted($relativeToRoot) -or
 179            [string]::Equals($relativeToRoot, '..', [System.StringComparison]::Ordinal) -or
 1180            $relativeToRoot.StartsWith("..$([System.IO.Path]::DirectorySeparatorChar)", [System.StringComparison]::Ordin
 1181            $relativeToRoot.StartsWith("..$([System.IO.Path]::AltDirectorySeparatorChar)", [System.StringComparison]::Or
 0182            throw "$PathLabel entry '$RelativePath' escapes the package root."
 183        }
 184
 1185        if ($RejectPackageRoot -and [string]::Equals($relativeToRoot, '.', [System.StringComparison]::Ordinal)) {
 0186            throw "$PathLabel entry '$RelativePath' resolves to the package root and cannot be excluded."
 187        }
 188
 1189        $pathExists = Test-Path -LiteralPath $combinedPath
 1190        if ($RequireExisting -and -not $pathExists) {
 0191            throw "$PathLabel entry '$RelativePath' was not found under '$PackageRoot'."
 192        }
 193
 1194        $isDirectory = if ($pathExists) {
 1195            Test-Path -LiteralPath $combinedPath -PathType Container
 196        } else {
 0197            $RelativePath.EndsWith([System.IO.Path]::DirectorySeparatorChar) -or
 198            $RelativePath.EndsWith([System.IO.Path]::AltDirectorySeparatorChar)
 199        }
 200
 1201        [pscustomobject]@{
 1202            FullPath = $combinedPath
 1203            RelativePath = $relativeToRoot -replace '\\', '/'
 1204            IsDirectory = $isDirectory
 205        }
 206    }
 207
 208    <#
 209    .SYNOPSIS
 210        Tests whether a path is covered by package exclusions.
 211    .DESCRIPTION
 212        Matches both exact file exclusions and directory exclusions that cover descendant files.
 213    .PARAMETER Path
 214        The file path to test.
 215    .PARAMETER ExcludedEntries
 216        Exclusion entries returned by Get-KrPackagePathInfo.
 217    .OUTPUTS
 218        [bool] True when the path should be excluded.
 219    #>
 220    function Test-KrPathIsExcluded {
 221        param(
 222            [string]$Path,
 223            [object[]]$ExcludedEntries
 224        )
 225
 1226        if ($null -eq $ExcludedEntries -or $ExcludedEntries.Count -eq 0) {
 1227            return $false
 228        }
 229
 1230        $candidatePath = [System.IO.Path]::TrimEndingDirectorySeparator([System.IO.Path]::GetFullPath($Path))
 1231        foreach ($entry in $ExcludedEntries) {
 1232            $excludedPath = [System.IO.Path]::TrimEndingDirectorySeparator([System.IO.Path]::GetFullPath($entry.FullPath
 1233            $relativeToExcluded = [System.IO.Path]::GetRelativePath($excludedPath, $candidatePath)
 234
 1235            if ($entry.IsDirectory) {
 1236                if ([string]::Equals($relativeToExcluded, '.', [System.StringComparison]::Ordinal)) {
 0237                    return $true
 238                }
 239
 1240                if (-not [System.IO.Path]::IsPathRooted($relativeToExcluded) -and
 241                    -not [string]::Equals($relativeToExcluded, '..', [System.StringComparison]::Ordinal) -and
 1242                    -not $relativeToExcluded.StartsWith("..$([System.IO.Path]::DirectorySeparatorChar)", [System.StringC
 1243                    -not $relativeToExcluded.StartsWith("..$([System.IO.Path]::AltDirectorySeparatorChar)", [System.Stri
 1244                    return $true
 245                }
 246
 247                continue
 248            }
 249
 1250            if ([string]::Equals($candidatePath, $excludedPath, [System.StringComparison]::OrdinalIgnoreCase)) {
 1251                return $true
 252            }
 253        }
 254
 1255        return $false
 256    }
 257
 258    <#
 259    .SYNOPSIS
 260        Builds normalized package exclusion entries.
 261    .DESCRIPTION
 262        Combines ApplicationDataFolders and explicit ExcludePaths into a deduplicated exclusion list.
 263    .PARAMETER PackageRoot
 264        The package root directory.
 265    .PARAMETER DescriptorInfo
 266        The validated descriptor info object.
 267    .PARAMETER DescriptorPath
 268        The Service.psd1 path.
 269    .PARAMETER ExcludeApplicationDataFolders
 270        Excludes descriptor ApplicationDataFolders from the archive.
 271    .PARAMETER ExcludePaths
 272        Additional relative file or folder paths to exclude.
 273    .OUTPUTS
 274        An array of exclusion entry objects.
 275    #>
 276    function Get-KrPackageExclusion {
 277        param(
 278            [string]$PackageRoot,
 279            [pscustomobject]$DescriptorInfo,
 280            [string]$DescriptorPath,
 281            [switch]$ExcludeApplicationDataFolders,
 282            [string[]]$ExcludePaths
 283        )
 284
 1285        $entries = [System.Collections.Generic.List[object]]::new()
 1286        $seenKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
 287
 1288        $addEntry = {
 289            param([pscustomobject]$Entry)
 290
 2291            $entryKey = '{0}|{1}' -f ([System.IO.Path]::TrimEndingDirectorySeparator($Entry.FullPath)), $Entry.IsDirecto
 1292            if ($seenKeys.Add($entryKey)) {
 1293                $entries.Add($Entry)
 294            }
 295        }
 296
 2297        if ($ExcludeApplicationDataFolders -and ($null -ne $DescriptorInfo.ApplicationDataFolders)) {
 2298            foreach ($applicationDataFolder in @($DescriptorInfo.ApplicationDataFolders)) {
 1299                if ([string]::IsNullOrWhiteSpace($applicationDataFolder)) {
 300                    continue
 301                }
 302
 2303                & $addEntry (Get-KrPackagePathInfo -PackageRoot $PackageRoot -RelativePath $applicationDataFolder -PathL
 304            }
 305        }
 306
 1307        if ($null -ne $ExcludePaths) {
 2308            foreach ($excludePath in @($ExcludePaths)) {
 1309                if ([string]::IsNullOrWhiteSpace($excludePath)) {
 310                    continue
 311                }
 312
 2313                & $addEntry (Get-KrPackagePathInfo -PackageRoot $PackageRoot -RelativePath $excludePath -PathLabel 'Excl
 314            }
 315        }
 316
 1317        if ($entries.Count -eq 0) {
 1318            return @()
 319        }
 320
 1321        if (Test-KrPathIsExcluded -Path $DescriptorPath -ExcludedEntries $entries) {
 0322            throw 'Requested package exclusions cannot exclude Service.psd1 because the service descriptor is required.'
 323        }
 324
 1325        $entryPointPath = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PackageRoot, $DescriptorInfo.EntryPoi
 1326        if (Test-KrPathIsExcluded -Path $entryPointPath -ExcludedEntries $entries) {
 2327            throw "Requested package exclusions cannot exclude the EntryPoint '$($DescriptorInfo.EntryPoint)'."
 328        }
 329
 1330        return $entries.ToArray()
 331    }
 332
 333    <#
 334    .SYNOPSIS
 335        Creates a zip archive from a directory.
 336    .DESCRIPTION
 337        Compresses the contents of a directory into a .krpack file.
 338    .PARAMETER DirectoryPath
 339        The path of the directory to compress.
 340    .PARAMETER DestinationPath
 341        The path of the resulting .krpack file.
 342    .EXAMPLE
 343        Invoke-KrZipFromDirectory -DirectoryPath .\my-service -DestinationPath .\my-service.krpack
 344    .OUTPUTS
 345        None. Creates a .krpack file at the specified destination.
 346    #>
 347    function Invoke-KrZipFromDirectory {
 348        param(
 349            [string]$DirectoryPath,
 350            [string]$DestinationPath,
 351            [object[]]$ExcludedEntries = @()
 352        )
 353
 1354        Add-Type -AssemblyName System.IO.Compression
 1355        Add-Type -AssemblyName System.IO.Compression.FileSystem
 356
 1357        $destinationDirectory = [System.IO.Path]::GetDirectoryName($DestinationPath)
 2358        if (-not [string]::IsNullOrWhiteSpace($destinationDirectory) -and -not (Test-Path -LiteralPath $destinationDirec
 0359            $null = New-Item -ItemType Directory -Path $destinationDirectory -Force
 360        }
 361
 1362        if (Test-Path -LiteralPath $DestinationPath -PathType Leaf) {
 0363            Remove-Item -LiteralPath $DestinationPath -Force
 364        }
 365
 1366        $zip = [System.IO.Compression.ZipFile]::Open($DestinationPath, [System.IO.Compression.ZipArchiveMode]::Create)
 367        try {
 2368            foreach ($file in (Get-ChildItem -LiteralPath $DirectoryPath -File -Recurse -Force)) {
 1369                if (Test-KrPathIsExcluded -Path $file.FullName -ExcludedEntries $ExcludedEntries) {
 370                    continue
 371                }
 372
 1373                $relativePath = [System.IO.Path]::GetRelativePath($DirectoryPath, $file.FullName)
 1374                $normalizedRelativePath = $relativePath -replace '\\', '/'
 2375                [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $file.FullName, $normalizedRelative
 376            }
 377        } finally {
 1378            $zip.Dispose()
 379        }
 380    }
 381
 1382    $stagingRoot = $null
 1383    $packageRoot = $null
 1384    $descriptorInfo = $null
 1385    $packageExclusions = @()
 386
 387    try {
 1388        if ($PSCmdlet.ParameterSetName -eq 'FromFolder') {
 1389            $packageRoot = [System.IO.Path]::GetFullPath($SourceFolder)
 2390            if (-not (Test-Path -LiteralPath $packageRoot -PathType Container)) {
 0391                throw "Source folder not found: $packageRoot"
 392            }
 393
 1394            $descriptorPath = [System.IO.Path]::Combine($packageRoot, 'Service.psd1')
 2395            if (-not (Test-Path -LiteralPath $descriptorPath -PathType Leaf)) {
 1396                throw "Service descriptor not found: $descriptorPath"
 397            }
 398
 1399            $descriptorData = Import-PowerShellDataFile -LiteralPath $descriptorPath
 1400            $descriptorInfo = Test-KrServiceDescriptorData -Descriptor $descriptorData -DescriptorPath $descriptorPath -
 1401            $packageExclusions = Get-KrPackageExclusion -PackageRoot $packageRoot -DescriptorInfo $descriptorInfo `
 402                -DescriptorPath $descriptorPath -ExcludeApplicationDataFolders:$ExcludeApplicationDataFolders -ExcludePa
 403
 1404            $defaultBaseName = [System.IO.Path]::GetFileName($packageRoot)
 1405            $resolvedOutputPath = Get-KrResolvedOutputPath -ProvidedOutputPath $OutputPath -DefaultBaseName $defaultBase
 406        } else {
 1407            $resolvedScriptPath = [System.IO.Path]::GetFullPath($ScriptPath)
 2408            if (-not (Test-Path -LiteralPath $resolvedScriptPath -PathType Leaf)) {
 0409                throw "Script file not found: $resolvedScriptPath"
 410            }
 411
 1412            if (-not $resolvedScriptPath.EndsWith('.ps1', [System.StringComparison]::OrdinalIgnoreCase)) {
 0413                throw 'ScriptPath must point to a .ps1 file.'
 414            }
 415
 1416            $scriptFileName = [System.IO.Path]::GetFileName($resolvedScriptPath)
 3417            $effectiveName = if ([string]::IsNullOrWhiteSpace($Name)) { [System.IO.Path]::GetFileNameWithoutExtension($r
 3418            $effectiveDescription = if ([string]::IsNullOrWhiteSpace($Description)) { $effectiveName } else { $Descripti
 419
 2420            $stagingRoot = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "kestrun-krpack-$([Guid]::NewGuid(
 1421            $null = New-Item -ItemType Directory -Path $stagingRoot -Force
 1422            $packageRoot = $stagingRoot
 423
 2424            Copy-Item -LiteralPath $resolvedScriptPath -Destination ([System.IO.Path]::Combine($packageRoot, $scriptFile
 425
 1426            $escapedName = $effectiveName.Replace("'", "''")
 1427            $escapedDescription = $effectiveDescription.Replace("'", "''")
 1428            $escapedVersion = $Version.ToString().Replace("'", "''")
 1429            $escapedEntryPoint = $scriptFileName.Replace("'", "''")
 430
 1431            $descriptorLines = [System.Collections.Generic.List[string]]::new()
 1432            $descriptorLines.Add('@{')
 1433            $descriptorLines.Add("    FormatVersion = '1.0'")
 1434            $descriptorLines.Add("    Name = '$escapedName'")
 1435            $descriptorLines.Add("    Description = '$escapedDescription'")
 1436            $descriptorLines.Add("    Version = '$escapedVersion'")
 1437            $descriptorLines.Add("    EntryPoint = '$escapedEntryPoint'")
 438
 1439            if (-not [string]::IsNullOrWhiteSpace($ServiceLogPath)) {
 0440                $escapedServiceLogPath = $ServiceLogPath.Replace("'", "''")
 0441                $descriptorLines.Add("    ServiceLogPath = '$escapedServiceLogPath'")
 442            }
 443
 1444            if ($null -ne $PreservePaths -and $PreservePaths.Count -gt 0) {
 1445                $descriptorLines.Add('    PreservePaths = @(')
 1446                foreach ($preservePath in $PreservePaths) {
 1447                    if ([string]::IsNullOrWhiteSpace($preservePath)) {
 448                        continue
 449                    }
 450
 1451                    $escapedPreservePath = $preservePath.Replace("'", "''")
 1452                    $descriptorLines.Add("        '$escapedPreservePath'")
 453                }
 454
 1455                $descriptorLines.Add('    )')
 456            }
 457
 1458            if ($null -ne $ApplicationDataFolders -and $ApplicationDataFolders.Count -gt 0) {
 1459                $descriptorLines.Add('    ApplicationDataFolders = @(')
 1460                foreach ($applicationDataFolder in $ApplicationDataFolders) {
 1461                    if ([string]::IsNullOrWhiteSpace($applicationDataFolder)) {
 462                        continue
 463                    }
 464
 1465                    $escapedApplicationDataFolder = $applicationDataFolder.Replace("'", "''")
 1466                    $descriptorLines.Add("        '$escapedApplicationDataFolder'")
 467                }
 468
 1469                $descriptorLines.Add('    )')
 470            }
 471
 1472            $descriptorLines.Add('}')
 473
 1474            $descriptorPath = [System.IO.Path]::Combine($packageRoot, 'Service.psd1')
 2475            Set-Content -LiteralPath $descriptorPath -Value ($descriptorLines -join [Environment]::NewLine) -Encoding ut
 476
 1477            $descriptorData = Import-PowerShellDataFile -LiteralPath $descriptorPath
 1478            $descriptorInfo = Test-KrServiceDescriptorData -Descriptor $descriptorData -DescriptorPath $descriptorPath -
 479
 2480            $defaultBaseName = "$effectiveName-$($Version.ToString())"
 1481            $resolvedOutputPath = Get-KrResolvedOutputPath -ProvidedOutputPath $OutputPath -DefaultBaseName $defaultBase
 482        }
 483
 2484        if ((Test-Path -LiteralPath $resolvedOutputPath -PathType Leaf) -and -not $Force) {
 0485            throw "Output package already exists: $resolvedOutputPath. Use -Force to overwrite."
 486        }
 487
 1488        if ($PSCmdlet.ShouldProcess($resolvedOutputPath, 'Create .krpack package')) {
 1489            Invoke-KrZipFromDirectory -DirectoryPath $packageRoot -DestinationPath $resolvedOutputPath -ExcludedEntries 
 490        }
 491
 2492        [pscustomobject]([ordered]@{
 1493                PackagePath = $resolvedOutputPath
 1494                Name = $descriptorInfo.Name
 1495                FormatVersion = $descriptorInfo.FormatVersion
 1496                EntryPoint = $descriptorInfo.EntryPoint
 1497                Description = $descriptorInfo.Description
 1498                Version = $descriptorInfo.Version
 1499                ServiceLogPath = $descriptorInfo.ServiceLogPath
 2500                PreservePaths = @($descriptorInfo.PreservePaths)
 2501                ApplicationDataFolders = @($descriptorInfo.ApplicationDataFolders)
 502            }
 503        )
 504    } finally {
 2505        if (-not [string]::IsNullOrWhiteSpace($stagingRoot) -and (Test-Path -LiteralPath $stagingRoot -PathType Containe
 1506            Remove-Item -LiteralPath $stagingRoot -Recurse -Force -ErrorAction SilentlyContinue
 507        }
 508    }
 509}