From 2d5e8d82c0e8a616739b430eff32b07e819376e8 Mon Sep 17 00:00:00 2001 From: Alexander-Sorrell-IT <284331358+Alexander-Sorrell-IT@users.noreply.github.com> Date: Fri, 29 May 2026 14:23:04 -0500 Subject: [PATCH 1/2] feat(targets): add pkg-chocolatey target (Chocolatey Community Repository) Adds the Chocolatey distribution target for Windows. - build() generates a real .nuspec + tools/chocolateyinstall.ps1 (exe/msi/zip), with XML escaping and checksum-verified install scripts. - ship() is dry-run-safe and surfaces the exact choco pack/apikey/push commands; live path requires CHOCOLATEY_API_KEY + the choco CLI on Windows. - Register pkg-chocolatey in cli adapter-registry; TARGETS.md row added. - manualSetup notes the community moderation/VirusTotal review gate. --- TARGETS.md | 1 + packages/cli/src/adapter-registry.ts | 2 +- packages/targets/pkg-chocolatey/README.md | 43 +++++ packages/targets/pkg-chocolatey/package.json | 25 +++ .../targets/pkg-chocolatey/src/index.test.ts | 87 +++++++++ packages/targets/pkg-chocolatey/src/index.ts | 171 ++++++++++++++++++ packages/targets/pkg-chocolatey/tsconfig.json | 5 + pnpm-lock.yaml | 6 + 8 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 packages/targets/pkg-chocolatey/README.md create mode 100644 packages/targets/pkg-chocolatey/package.json create mode 100644 packages/targets/pkg-chocolatey/src/index.test.ts create mode 100644 packages/targets/pkg-chocolatey/src/index.ts create mode 100644 packages/targets/pkg-chocolatey/tsconfig.json diff --git a/TARGETS.md b/TARGETS.md index 2b7f76d1..78ff1d68 100644 --- a/TARGETS.md +++ b/TARGETS.md @@ -19,6 +19,7 @@ Each row is a **target adapter** — one plugin in `packages/targets/*`. Status | `pkg-homebrew` | Homebrew tap | ✅ | | `pkg-winget` | Microsoft winget | 🚧 | | `pkg-scoop` | Scoop bucket | 🚧 | +| `pkg-chocolatey` | Chocolatey Community Repository | ✅ | | `pkg-apt` | apt repo / PPA | 🚧 | | `pkg-snap` | Snapcraft | 🚧 | | `pkg-flatpak` | Flathub | ✅ | diff --git a/packages/cli/src/adapter-registry.ts b/packages/cli/src/adapter-registry.ts index 6f78ff5d..b9aff481 100644 --- a/packages/cli/src/adapter-registry.ts +++ b/packages/cli/src/adapter-registry.ts @@ -162,7 +162,7 @@ export const CATEGORIES: readonly AdapterCategory[] = [ id: 'targets', pkgPrefix: '@profullstack/sh1pt-target', description: 'Distribution targets — stores, registries, CDNs, deploy platforms', - adapters: ['browser-chrome', 'browser-edge', 'browser-firefox', 'browser-safari', 'chat-discord', 'chat-signal', 'chat-slack', 'chat-telegram', 'chat-whatsapp', 'console-steam', 'deploy-coolify', 'deploy-denodeploy', 'deploy-firebase', 'deploy-fly', 'deploy-lambda', 'deploy-netlify', 'deploy-railway', 'deploy-render', 'deploy-vercel', 'deploy-workers', 'desktop-linux', 'desktop-mac', 'desktop-steamos', 'desktop-win', 'exe-dev', 'mobile-android', 'mobile-expo', 'mobile-ios', 'payment-adyen', 'payment-coinpay', 'payment-paypal', 'payment-square', 'payment-stripe', 'pkg-apt', 'pkg-aube', 'pkg-aur', 'pkg-cdn', 'pkg-deno', 'pkg-docker', 'pkg-fdroid', 'pkg-flatpak', 'pkg-ghpackages', 'pkg-homebrew', 'pkg-jsr', 'pkg-nix', 'pkg-npm', 'pkg-perry', 'pkg-scoop', 'pkg-snap', 'pkg-winget', 'plugin-jetbrains', 'plugin-vscode', 'qa-geisterhand', 'sdk-pypi', 'tv-androidtv', 'tv-firetv', 'tv-roku', 'tv-tvos', 'tv-webos', 'web-static', 'xr-meta-quest', 'xr-pico', 'xr-sidequest', 'xr-steamvr', 'xr-visionos', 'xr-webxr'], + adapters: ['browser-chrome', 'browser-edge', 'browser-firefox', 'browser-safari', 'chat-discord', 'chat-signal', 'chat-slack', 'chat-telegram', 'chat-whatsapp', 'console-steam', 'deploy-coolify', 'deploy-denodeploy', 'deploy-firebase', 'deploy-fly', 'deploy-lambda', 'deploy-netlify', 'deploy-railway', 'deploy-render', 'deploy-vercel', 'deploy-workers', 'desktop-linux', 'desktop-mac', 'desktop-steamos', 'desktop-win', 'exe-dev', 'mobile-android', 'mobile-expo', 'mobile-ios', 'payment-adyen', 'payment-coinpay', 'payment-paypal', 'payment-square', 'payment-stripe', 'pkg-apt', 'pkg-aube', 'pkg-aur', 'pkg-cdn', 'pkg-chocolatey', 'pkg-deno', 'pkg-docker', 'pkg-fdroid', 'pkg-flatpak', 'pkg-ghpackages', 'pkg-homebrew', 'pkg-jsr', 'pkg-nix', 'pkg-npm', 'pkg-perry', 'pkg-scoop', 'pkg-snap', 'pkg-winget', 'plugin-jetbrains', 'plugin-vscode', 'qa-geisterhand', 'sdk-pypi', 'tv-androidtv', 'tv-firetv', 'tv-roku', 'tv-tvos', 'tv-webos', 'web-static', 'xr-meta-quest', 'xr-pico', 'xr-sidequest', 'xr-steamvr', 'xr-visionos', 'xr-webxr'], }, { id: 'vcs', diff --git a/packages/targets/pkg-chocolatey/README.md b/packages/targets/pkg-chocolatey/README.md new file mode 100644 index 00000000..7144136d --- /dev/null +++ b/packages/targets/pkg-chocolatey/README.md @@ -0,0 +1,43 @@ +# Chocolatey Community Repository + +Provides the Chocolatey Community Repository sh1pt target adapter for build, publish, or deployment workflows. + +## What it does + +- Registers a target surface that sh1pt can build, publish, or deploy to. +- Provides adapter metadata and lifecycle hooks used by the CLI. +- Includes setup guidance for required credentials or provider configuration. +- Implements build behavior used by sh1pt target workflows. + +## Package + +- Name: `@profullstack/sh1pt-target-pkg-chocolatey` +- Path: `packages/targets/pkg-chocolatey` +- Adapter ID: `pkg-chocolatey` +- Homepage: https://sh1pt.com + +## Scripts + +- `build`: `tsc -p tsconfig.json` +- `prepublishOnly`: `pnpm build` +- `typecheck`: `tsc -p tsconfig.json --noEmit` + +## Usage + +```bash +pnpm add @profullstack/sh1pt-target-pkg-chocolatey +``` + +## Development + +```bash +pnpm --filter @profullstack/sh1pt-target-pkg-chocolatey typecheck +``` + +Run tests from the repository root when this module includes a test file: + +```bash +pnpm vitest run packages/targets/pkg-chocolatey/src/index.test.ts +``` + + diff --git a/packages/targets/pkg-chocolatey/package.json b/packages/targets/pkg-chocolatey/package.json new file mode 100644 index 00000000..019dcb6a --- /dev/null +++ b/packages/targets/pkg-chocolatey/package.json @@ -0,0 +1,25 @@ +{ + "name": "@profullstack/sh1pt-target-pkg-chocolatey", + "version": "0.1.15", + "type": "module", + "main": "./src/index.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "prepublishOnly": "pnpm build" + }, + "dependencies": { + "@profullstack/sh1pt-core": "workspace:*" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/profullstack/sh1pt.git", + "directory": "packages/targets/pkg-chocolatey" + }, + "homepage": "https://sh1pt.com", + "bugs": "https://github.com/profullstack/sh1pt/issues", + "files": [ + "dist" + ] +} diff --git a/packages/targets/pkg-chocolatey/src/index.test.ts b/packages/targets/pkg-chocolatey/src/index.test.ts new file mode 100644 index 00000000..ef9d6fe0 --- /dev/null +++ b/packages/targets/pkg-chocolatey/src/index.test.ts @@ -0,0 +1,87 @@ +import { fakeBuildContext, fakeShipContext, smokeTest } from '@profullstack/sh1pt-core/testing'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import adapter from './index.js'; + +smokeTest(adapter, { idPrefix: 'pkg', requireKind: true }); + +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +describe('chocolatey package generation', () => { + it('writes a nuspec and an exe install script with checksum', async () => { + const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-choco-')); + tempDirs.push(outDir); + + const result = await adapter.build(fakeBuildContext({ outDir, version: '1.4.0' }) as any, { + packageId: 'mytool', + packageTitle: 'My Tool', + authors: 'Acme', + projectUrl: 'https://example.com/mytool', + licenseUrl: 'https://example.com/license', + tags: ['cli', 'release'], + summary: 'A release tool', + installerUrl: 'https://downloads.example.com/mytool-1.4.0.exe', + installerType: 'exe', + checksum: 'a'.repeat(64), + }); + + const dir = join(outDir, 'chocolatey', 'mytool'); + expect(result.artifact).toBe(dir); + + const nuspec = await readFile(join(dir, 'mytool.nuspec'), 'utf-8'); + expect(nuspec).toContain('mytool'); + expect(nuspec).toContain('1.4.0'); + expect(nuspec).toContain('My Tool'); + expect(nuspec).toContain('Acme'); + expect(nuspec).toContain('https://example.com/mytool'); + expect(nuspec).toContain('cli release'); + + const install = await readFile(join(dir, 'tools', 'chocolateyinstall.ps1'), 'utf-8'); + expect(install).toContain("packageName = 'mytool'"); + expect(install).toContain("fileType = 'exe'"); + expect(install).toContain("url = 'https://downloads.example.com/mytool-1.4.0.exe'"); + expect(install).toContain(`checksum = '${'a'.repeat(64)}'`); + expect(install).toContain("checksumType = 'sha256'"); + expect(install).toContain("silentArgs = '/S'"); + expect(install).toContain('Install-ChocolateyPackage @packageArgs'); + }); + + it('escapes XML and emits a zip install script for zip installers', async () => { + const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-choco-')); + tempDirs.push(outDir); + + const result = await adapter.build(fakeBuildContext({ outDir, version: '2.0.0' }) as any, { + packageId: 'ziptool', + packageTitle: 'Zip & Tool ', + installerUrl: 'https://downloads.example.com/ziptool-2.0.0.zip', + installerType: 'zip', + checksum: 'b'.repeat(64), + checksumType: 'sha512', + }); + + const dir = join(outDir, 'chocolatey', 'ziptool'); + const nuspec = await readFile(join(dir, 'ziptool.nuspec'), 'utf-8'); + expect(nuspec).toContain('Zip & Tool <Pro>'); + + const install = await readFile(join(dir, 'tools', 'chocolateyinstall.ps1'), 'utf-8'); + expect(install).toContain('Install-ChocolateyZipPackage @packageArgs'); + expect(install).toContain("unzipLocation = $toolsDir"); + expect(install).toContain("checksumType = 'sha512'"); + }); + + it('keeps dry-run shipping side-effect free and surfaces the push commands', async () => { + const ship = await adapter.ship(fakeShipContext({ version: '1.4.0', dryRun: true }) as any, { + packageId: 'mytool', + installerUrl: 'https://downloads.example.com/mytool-1.4.0.exe', + checksum: 'c'.repeat(64), + }); + expect(ship.id).toBe('dry-run'); + expect(ship.meta?.commands).toContain('choco push mytool.1.4.0.nupkg --source https://push.chocolatey.org/'); + }); +}); diff --git a/packages/targets/pkg-chocolatey/src/index.ts b/packages/targets/pkg-chocolatey/src/index.ts new file mode 100644 index 00000000..190de8cf --- /dev/null +++ b/packages/targets/pkg-chocolatey/src/index.ts @@ -0,0 +1,171 @@ +import { defineTarget, manualSetup } from '@profullstack/sh1pt-core'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +interface Config { + packageId: string; // e.g. "mytool" (lowercase, the community-repo id) + packageTitle?: string; // human title, e.g. "My Tool" + authors?: string; // defaults to packageId + owners?: string; + projectUrl?: string; // homepage + licenseUrl?: string; + iconUrl?: string; + tags?: string[]; + summary?: string; + description?: string; + installerUrl: string; // download URL for the installer/zip + installerType?: 'exe' | 'msi' | 'zip'; + checksum: string; // checksum of the installer at installerUrl + checksumType?: 'sha256' | 'sha512'; + silentArgs?: string; // override the default silent-install args +} + +const TYPE_DEFAULT_SILENT_ARGS: Record, string> = { + exe: '/S', + msi: '/qn /norestart', + zip: '', +}; + +function xmlEscape(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function packageDir(outDir: string, packageId: string): string { + return join(outDir, 'chocolatey', packageId); +} + +function silentArgsFor(config: Config): string { + return config.silentArgs ?? TYPE_DEFAULT_SILENT_ARGS[config.installerType ?? 'exe']; +} + +function renderNuspec(config: Config, version: string): string { + const authors = config.authors ?? config.packageId; + const title = config.packageTitle ?? config.packageId; + const summary = config.summary ?? `${title} release`; + const description = config.description ?? summary; + const lines = [ + '', + '', + ' ', + ` ${xmlEscape(config.packageId)}`, + ` ${xmlEscape(version)}`, + ` ${xmlEscape(title)}`, + ` ${xmlEscape(authors)}`, + ` ${xmlEscape(config.owners ?? authors)}`, + ]; + if (config.projectUrl) lines.push(` ${xmlEscape(config.projectUrl)}`); + if (config.licenseUrl) lines.push(` ${xmlEscape(config.licenseUrl)}`); + if (config.iconUrl) lines.push(` ${xmlEscape(config.iconUrl)}`); + lines.push(` ${xmlEscape(summary)}`); + lines.push(` ${xmlEscape(description)}`); + if (config.tags?.length) lines.push(` ${xmlEscape(config.tags.join(' '))}`); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(''); + lines.push(''); + return lines.join('\n'); +} + +function renderInstallScript(config: Config, version: string): string { + const type = config.installerType ?? 'exe'; + const checksumType = config.checksumType ?? 'sha256'; + if (type === 'zip') { + return [ + `$ErrorActionPreference = 'Stop'`, + `$toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition`, + `$packageArgs = @{`, + ` packageName = '${config.packageId}'`, + ` unzipLocation = $toolsDir`, + ` url = '${config.installerUrl}'`, + ` checksum = '${config.checksum}'`, + ` checksumType = '${checksumType}'`, + `}`, + `Install-ChocolateyZipPackage @packageArgs`, + '', + ].join('\n'); + } + return [ + `$ErrorActionPreference = 'Stop'`, + `$packageArgs = @{`, + ` packageName = '${config.packageId}'`, + ` fileType = '${type}'`, + ` url = '${config.installerUrl}'`, + ` checksum = '${config.checksum}'`, + ` checksumType = '${checksumType}'`, + ` silentArgs = '${silentArgsFor(config)}'`, + ` validExitCodes = @(0)`, + `}`, + `Install-ChocolateyPackage @packageArgs`, + '', + ].join('\n'); +} + +function publishCommands(config: Config, version: string): string[] { + return [ + `choco pack ${config.packageId}.nuspec --version ${version}`, + `choco apikey --api-key --source https://push.chocolatey.org/`, + `choco push ${config.packageId}.${version}.nupkg --source https://push.chocolatey.org/`, + ]; +} + +export default defineTarget({ + id: 'pkg-chocolatey', + kind: 'package-manager', + label: 'Chocolatey Community Repository', + async build(ctx, config) { + if (!config.installerUrl) throw new Error('pkg-chocolatey requires installerUrl'); + if (!config.checksum) throw new Error('pkg-chocolatey requires checksum (the community repo rejects un-checksummed remote downloads)'); + const dir = packageDir(ctx.outDir, config.packageId); + const toolsDir = join(dir, 'tools'); + ctx.log(`generate chocolatey package for ${config.packageId} v${ctx.version}`); + await mkdir(toolsDir, { recursive: true }); + await Promise.all([ + writeFile(join(dir, `${config.packageId}.nuspec`), renderNuspec(config, ctx.version), 'utf-8'), + writeFile(join(toolsDir, 'chocolateyinstall.ps1'), renderInstallScript(config, ctx.version), 'utf-8'), + ]); + return { + artifact: dir, + meta: { + nuspec: join(dir, `${config.packageId}.nuspec`), + installScript: join(toolsDir, 'chocolateyinstall.ps1'), + commands: publishCommands(config, ctx.version), + }, + }; + }, + async ship(ctx, config) { + ctx.log(`push ${config.packageId}@${ctx.version} to community.chocolatey.org`); + if (ctx.dryRun) { + return { id: 'dry-run', meta: { commands: publishCommands(config, ctx.version) } }; + } + const apiKey = ctx.secret('CHOCOLATEY_API_KEY'); + if (!apiKey) { + throw new Error('CHOCOLATEY_API_KEY not in vault — run: sh1pt secret set CHOCOLATEY_API_KEY '); + } + // TODO: choco pack + choco push using CHOCOLATEY_API_KEY (Windows + choco CLI required). + return { + id: `${config.packageId}@${ctx.version}`, + url: `https://community.chocolatey.org/packages/${config.packageId}/${ctx.version}`, + }; + }, + async status(id) { + const [pkgId] = id.split('@'); + return { state: 'in-review', url: `https://community.chocolatey.org/packages/${pkgId}` }; + }, + setup: manualSetup({ + label: 'Chocolatey Community Repository', + vendorDocUrl: 'https://docs.chocolatey.org/en-us/create/create-packages/', + steps: [ + 'Create an account at https://community.chocolatey.org and confirm your email.', + 'Copy your API key from https://community.chocolatey.org/account', + 'Run: sh1pt secret set CHOCOLATEY_API_KEY ', + 'Packaging + push require Windows with the choco CLI installed (choco pack / choco push).', + 'Note: community submissions pass through automated validation, verification, and VirusTotal scanning before they go live.', + ], + }), +}); diff --git a/packages/targets/pkg-chocolatey/tsconfig.json b/packages/targets/pkg-chocolatey/tsconfig.json new file mode 100644 index 00000000..cf441478 --- /dev/null +++ b/packages/targets/pkg-chocolatey/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { "outDir": "dist", "rootDir": "src" }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5378866..d11ff256 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1826,6 +1826,12 @@ importers: specifier: workspace:* version: link:../../core + packages/targets/pkg-chocolatey: + dependencies: + '@profullstack/sh1pt-core': + specifier: workspace:* + version: link:../../core + packages/targets/pkg-deno: dependencies: '@profullstack/sh1pt-core': From d33f823eb8c41dda1701927457012b75ac5b7074 Mon Sep 17 00:00:00 2001 From: Alexander-Sorrell-IT <284331358+Alexander-Sorrell-IT@users.noreply.github.com> Date: Fri, 29 May 2026 14:35:49 -0500 Subject: [PATCH 2/2] fix(pkg-chocolatey): address Greptile review (P1 + P2s) - Live ship() now throws 'not implemented' instead of returning a false success after the secret check (P1). - Escape single quotes in interpolated PowerShell fields via psEscape() so a quote in installerUrl/silentArgs/packageId can't break the .ps1 (P2). - Drop the unused 'version' param from renderInstallScript (P2). - Add tests for quote-escaping and the live not-implemented throw. --- .../targets/pkg-chocolatey/src/index.test.ts | 29 ++++++++++++++ packages/targets/pkg-chocolatey/src/index.ts | 40 ++++++++++--------- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/packages/targets/pkg-chocolatey/src/index.test.ts b/packages/targets/pkg-chocolatey/src/index.test.ts index ef9d6fe0..2a9b0303 100644 --- a/packages/targets/pkg-chocolatey/src/index.test.ts +++ b/packages/targets/pkg-chocolatey/src/index.test.ts @@ -84,4 +84,33 @@ describe('chocolatey package generation', () => { expect(ship.id).toBe('dry-run'); expect(ship.meta?.commands).toContain('choco push mytool.1.4.0.nupkg --source https://push.chocolatey.org/'); }); + + it("escapes single quotes in user-overridable PowerShell fields", async () => { + const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-choco-')); + tempDirs.push(outDir); + + await adapter.build(fakeBuildContext({ outDir, version: '1.0.0' }) as any, { + packageId: 'mytool', + installerUrl: "https://downloads.example.com/o'reilly-1.0.0.exe", + installerType: 'exe', + checksum: 'd'.repeat(64), + silentArgs: "/VERYSILENT /DIR='C:\\Program Files'", + }); + + const install = await readFile(join(outDir, 'chocolatey', 'mytool', 'tools', 'chocolateyinstall.ps1'), 'utf-8'); + // Single quotes are doubled so the generated .ps1 stays valid PowerShell. + expect(install).toContain("url = 'https://downloads.example.com/o''reilly-1.0.0.exe'"); + expect(install).toContain("silentArgs = '/VERYSILENT /DIR=''C:\\Program Files'''"); + // Every literal quote is doubled, so the count of ' is even on each value line. + const silentLine = install.split('\n').find((l) => l.includes('silentArgs')) ?? ''; + expect((silentLine.match(/'/g) ?? []).length % 2).toBe(0); + }); + + it('throws (no false success) on live, unimplemented publish', async () => { + await expect(adapter.ship(fakeShipContext({ version: '1.0.0', dryRun: false }) as any, { + packageId: 'mytool', + installerUrl: 'https://downloads.example.com/mytool-1.0.0.exe', + checksum: 'e'.repeat(64), + })).rejects.toThrow(/not implemented/i); + }); }); diff --git a/packages/targets/pkg-chocolatey/src/index.ts b/packages/targets/pkg-chocolatey/src/index.ts index 190de8cf..a91e0c81 100644 --- a/packages/targets/pkg-chocolatey/src/index.ts +++ b/packages/targets/pkg-chocolatey/src/index.ts @@ -72,7 +72,13 @@ function renderNuspec(config: Config, version: string): string { return lines.join('\n'); } -function renderInstallScript(config: Config, version: string): string { +// Escape a value for a single-quoted PowerShell string literal: a literal ' +// must be doubled ('') or it terminates the string and breaks the .ps1. +function psEscape(value: string): string { + return value.replace(/'/g, "''"); +} + +function renderInstallScript(config: Config): string { const type = config.installerType ?? 'exe'; const checksumType = config.checksumType ?? 'sha256'; if (type === 'zip') { @@ -80,10 +86,10 @@ function renderInstallScript(config: Config, version: string): string { `$ErrorActionPreference = 'Stop'`, `$toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition`, `$packageArgs = @{`, - ` packageName = '${config.packageId}'`, + ` packageName = '${psEscape(config.packageId)}'`, ` unzipLocation = $toolsDir`, - ` url = '${config.installerUrl}'`, - ` checksum = '${config.checksum}'`, + ` url = '${psEscape(config.installerUrl)}'`, + ` checksum = '${psEscape(config.checksum)}'`, ` checksumType = '${checksumType}'`, `}`, `Install-ChocolateyZipPackage @packageArgs`, @@ -93,12 +99,12 @@ function renderInstallScript(config: Config, version: string): string { return [ `$ErrorActionPreference = 'Stop'`, `$packageArgs = @{`, - ` packageName = '${config.packageId}'`, + ` packageName = '${psEscape(config.packageId)}'`, ` fileType = '${type}'`, - ` url = '${config.installerUrl}'`, - ` checksum = '${config.checksum}'`, + ` url = '${psEscape(config.installerUrl)}'`, + ` checksum = '${psEscape(config.checksum)}'`, ` checksumType = '${checksumType}'`, - ` silentArgs = '${silentArgsFor(config)}'`, + ` silentArgs = '${psEscape(silentArgsFor(config))}'`, ` validExitCodes = @(0)`, `}`, `Install-ChocolateyPackage @packageArgs`, @@ -127,7 +133,7 @@ export default defineTarget({ await mkdir(toolsDir, { recursive: true }); await Promise.all([ writeFile(join(dir, `${config.packageId}.nuspec`), renderNuspec(config, ctx.version), 'utf-8'), - writeFile(join(toolsDir, 'chocolateyinstall.ps1'), renderInstallScript(config, ctx.version), 'utf-8'), + writeFile(join(toolsDir, 'chocolateyinstall.ps1'), renderInstallScript(config), 'utf-8'), ]); return { artifact: dir, @@ -143,15 +149,13 @@ export default defineTarget({ if (ctx.dryRun) { return { id: 'dry-run', meta: { commands: publishCommands(config, ctx.version) } }; } - const apiKey = ctx.secret('CHOCOLATEY_API_KEY'); - if (!apiKey) { - throw new Error('CHOCOLATEY_API_KEY not in vault — run: sh1pt secret set CHOCOLATEY_API_KEY '); - } - // TODO: choco pack + choco push using CHOCOLATEY_API_KEY (Windows + choco CLI required). - return { - id: `${config.packageId}@${ctx.version}`, - url: `https://community.chocolatey.org/packages/${config.packageId}/${ctx.version}`, - }; + // Live publish (choco pack + choco push) is not implemented yet — fail + // loudly rather than return a false success. Needs Windows + the choco CLI + // and CHOCOLATEY_API_KEY in the vault. + throw new Error( + 'pkg-chocolatey live publish is not implemented yet — use dryRun to preview ' + + 'the choco pack/push commands. (Requires Windows + choco CLI + CHOCOLATEY_API_KEY.)', + ); }, async status(id) { const [pkgId] = id.split('@');