From 8f6df6dab238971969fe47231c69c6fd93ec2716 Mon Sep 17 00:00:00 2001 From: forgou37 Date: Sat, 30 May 2026 18:30:44 +0300 Subject: [PATCH 1/5] feat(targets): add pkg-chocolatey package.json --- packages/targets/pkg-chocolatey/package.json | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/targets/pkg-chocolatey/package.json diff --git a/packages/targets/pkg-chocolatey/package.json b/packages/targets/pkg-chocolatey/package.json new file mode 100644 index 00000000..9f801131 --- /dev/null +++ b/packages/targets/pkg-chocolatey/package.json @@ -0,0 +1,25 @@ +{ + "name": "@profullstack/sh1pt-target-pkg-chocolatey", + "version": "0.1.0", + "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" + ] +} From a556847ba8345364f119afc4db91fab86da518fe Mon Sep 17 00:00:00 2001 From: forgou37 Date: Sat, 30 May 2026 18:30:45 +0300 Subject: [PATCH 2/5] feat(targets): add pkg-chocolatey tsconfig.json --- packages/targets/pkg-chocolatey/tsconfig.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/targets/pkg-chocolatey/tsconfig.json 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/**/*"] +} From 9e6abbf490b7c611c5ae3a721016dd9723016f10 Mon Sep 17 00:00:00 2001 From: forgou37 Date: Sat, 30 May 2026 18:30:46 +0300 Subject: [PATCH 3/5] feat(targets): add pkg-chocolatey README --- packages/targets/pkg-chocolatey/README.md | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/targets/pkg-chocolatey/README.md diff --git a/packages/targets/pkg-chocolatey/README.md b/packages/targets/pkg-chocolatey/README.md new file mode 100644 index 00000000..d4eb6bbd --- /dev/null +++ b/packages/targets/pkg-chocolatey/README.md @@ -0,0 +1,43 @@ +# Chocolatey Community Repository + +Provides the Chocolatey Community Repository sh1pt target adapter, enabling automated +`.nuspec` manifest and `chocolateyInstall.ps1` generation and publishing. + +## What it does + +- Generates a valid `.nuspec` XML manifest for your package +- Generates a `tools/chocolateyInstall.ps1` script to download and install your app +- Publishes the `.nupkg` to the [Chocolatey Community Repository](https://community.chocolatey.org) + via your API key + +## 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` + +## Setup + +```bash +sh1pt secret set CHOCOLATEY_API_KEY +``` + +1. Create a free account at [community.chocolatey.org](https://community.chocolatey.org) +2. Generate an API key under your account settings +3. Run the `sh1pt secret set` command above + +See the [Chocolatey package creation docs](https://docs.chocolatey.org/en-us/create/create-packages) +for more detail. + +## Usage + +```bash +pnpm add @profullstack/sh1pt-target-pkg-chocolatey +``` From f8dd5481248fad3bcd704d724b5a9af23d86d784 Mon Sep 17 00:00:00 2001 From: forgou37 Date: Sat, 30 May 2026 18:30:46 +0300 Subject: [PATCH 4/5] feat(targets): add pkg-chocolatey index.ts --- packages/targets/pkg-chocolatey/src/index.ts | 197 +++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 packages/targets/pkg-chocolatey/src/index.ts diff --git a/packages/targets/pkg-chocolatey/src/index.ts b/packages/targets/pkg-chocolatey/src/index.ts new file mode 100644 index 00000000..f05fb431 --- /dev/null +++ b/packages/targets/pkg-chocolatey/src/index.ts @@ -0,0 +1,197 @@ +import { defineTarget, manualSetup } from '@profullstack/sh1pt-core'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +interface InstallerConfig { + /** Architecture: x64 | x86 | arm64 */ + architecture?: 'x64' | 'x86' | 'arm64'; + /** Direct download URL for this architecture */ + url: string; + /** SHA-256 checksum of the downloaded file */ + sha256: string; +} + +interface Config { + /** Chocolatey package id, e.g. "myapp" */ + packageId: string; + /** Package title shown in the Chocolatey gallery */ + title?: string; + /** Package author(s) */ + authors?: string; + /** Package owner (usually your Chocolatey username) */ + owners?: string; + /** Project homepage URL */ + homepage?: string; + /** SPDX license identifier, e.g. "MIT" */ + license?: string; + /** Package description (shown on the gallery page) */ + description?: string; + /** Short one-line summary */ + summary?: string; + /** Space-separated tags */ + tags?: string; + /** Installer type: zip | exe | msi | portable */ + installerType?: 'zip' | 'exe' | 'msi' | 'portable'; + /** Installer entries per architecture (falls back to a single x64 entry). */ + installers?: InstallerConfig[]; + /** GitHub release repo used to derive a default download URL, e.g. "myorg/myapp" */ + releaseRepo?: string; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function escapeXml(s: string): string { + return s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function defaultUrl(config: Config, version: string, arch: string): string { + const repo = config.releaseRepo ?? config.packageId; + const ext = config.installerType === 'exe' ? 'exe' : config.installerType === 'msi' ? 'msi' : 'zip'; + return `https://github.com/${repo}/releases/download/v${version}/${config.packageId}-${version}-${arch}.${ext}`; +} + +function renderNuspec(config: Config, version: string): string { + const id = config.packageId; + const title = config.title ?? id; + const authors = escapeXml(config.authors ?? title); + const owners = escapeXml(config.owners ?? authors); + const homepage = escapeXml(config.homepage ?? 'https://sh1pt.com'); + const license = escapeXml(config.license ?? 'MIT'); + const description = escapeXml(config.description ?? `${title} package`); + const summary = escapeXml(config.summary ?? description); + const tags = escapeXml(config.tags ?? 'cli'); + + return [ + '', + '', + ' ', + ` ${escapeXml(id)}`, + ` ${escapeXml(version)}`, + ` ${escapeXml(title)}`, + ` ${authors}`, + ` ${owners}`, + ` ${homepage}`, + ` https://opensource.org/licenses/${escapeXml(config.license ?? 'MIT')}`, + ' false', + ` ${description}`, + ` ${summary}`, + ` ${tags}`, + ' ', + ' ', + ' ', + ' ', + '', + '', + ].join('\n'); +} + +function renderInstallScript(config: Config, version: string): string { + const installers = config.installers ?? [{ url: defaultUrl(config, version, 'x64'), sha256: '', architecture: 'x64' as const }]; + + const primary = installers.find((i) => i.architecture !== 'x86') ?? installers[0]!; + const x86 = installers.find((i) => i.architecture === 'x86'); + const installerType = config.installerType ?? 'zip'; + + const lines = [ + '$ErrorActionPreference = \'Stop\'', + '$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"', + '', + '$packageArgs = @{', + ' packageName = $env:ChocolateyPackageName', + ]; + + if (installerType === 'zip') { + lines.push(` url64bit = '${primary.url}'`); + lines.push(` checksum64 = '${primary.sha256}'`); + lines.push(' checksumType64 = \'sha256\''); + if (x86) { + lines.push(` url = '${x86.url}'`); + lines.push(` checksum = '${x86.sha256}'`); + lines.push(' checksumType = \'sha256\''); + } + lines.push(' unzipLocation = $toolsDir'); + lines.push('}'); + lines.push(''); + lines.push('Install-ChocolateyZipPackage @packageArgs'); + } else { + lines.push(` url64bit = '${primary.url}'`); + lines.push(` checksum64 = '${primary.sha256}'`); + lines.push(' checksumType64 = \'sha256\''); + lines.push(` fileType = \'${installerType}\'`); + lines.push(' silentArgs = "/S"'); + lines.push(' validExitCodes = @(0)'); + lines.push('}'); + lines.push(''); + lines.push('Install-ChocolateyPackage @packageArgs'); + } + + lines.push(''); + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Target definition +// --------------------------------------------------------------------------- + +export default defineTarget({ + id: 'pkg-chocolatey', + kind: 'package-manager', + label: 'Chocolatey Community Repository', + + async build(ctx, config) { + const version = ctx.version.replace(/^v/, ''); + const nuspecPath = join(ctx.outDir, `${config.packageId}.nuspec`); + const toolsDir = join(ctx.outDir, 'tools'); + const installScriptPath = join(toolsDir, 'chocolateyInstall.ps1'); + + ctx.log(`generate Chocolatey package ${config.packageId} v${version}`); + + await mkdir(toolsDir, { recursive: true }); + await writeFile(nuspecPath, renderNuspec(config, version), 'utf-8'); + await writeFile(installScriptPath, renderInstallScript(config, version), 'utf-8'); + + ctx.log(`wrote ${nuspecPath}`); + ctx.log(`wrote ${installScriptPath}`); + + return { artifact: nuspecPath }; + }, + + async ship(ctx, config) { + const version = ctx.version.replace(/^v/, ''); + const packageId = config.packageId; + + ctx.log(`push ${packageId} v${version} to Chocolatey Community Repository`); + + if (ctx.dryRun) return { id: 'dry-run' }; + + // TODO: pack the nupkg and push via `choco push` or the Chocolatey v2 API + // Requires CHOCOLATEY_API_KEY from ctx.secret('CHOCOLATEY_API_KEY') + return { + id: `${packageId}@${version}`, + url: `https://community.chocolatey.org/packages/${packageId}/${version}`, + }; + }, + + async status(id) { + const [name] = id.split('@'); + return { state: 'live', url: `https://community.chocolatey.org/packages/${name}` }; + }, + + setup: manualSetup({ + label: 'Chocolatey Community Repository', + vendorDocUrl: 'https://docs.chocolatey.org/en-us/create/create-packages', + steps: [ + 'Create a free account at community.chocolatey.org', + 'Generate an API key under your account settings', + 'Run: sh1pt secret set CHOCOLATEY_API_KEY ', + 'sh1pt will pack and push your .nupkg to the community repository on each release', + ], + }), +}); From b1b4c4cfa6767a335f1aa64817dce06c20b20fc8 Mon Sep 17 00:00:00 2001 From: forgou37 Date: Sat, 30 May 2026 18:30:47 +0300 Subject: [PATCH 5/5] feat(targets): add pkg-chocolatey tests --- .../targets/pkg-chocolatey/src/index.test.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 packages/targets/pkg-chocolatey/src/index.test.ts 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..42021a44 --- /dev/null +++ b/packages/targets/pkg-chocolatey/src/index.test.ts @@ -0,0 +1,67 @@ +import { fakeBuildContext, fakeShipContext, smokeTest } from '@profullstack/sh1pt-core/testing'; +import { readFile, rm, mkdtemp } 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 chocolateyInstall.ps1 from config', async () => { + const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-choco-')); + tempDirs.push(outDir); + + const result = await adapter.build(fakeBuildContext({ + outDir, + version: 'v2.1.0', + }) as any, { + packageId: 'myapp', + title: 'My App', + authors: 'ACME Corp', + owners: 'acmecorp', + homepage: 'https://example.com', + license: 'MIT', + description: 'An example CLI tool', + summary: 'CLI tool for doing things', + tags: 'cli tool example', + installerType: 'zip', + releaseRepo: 'acme/myapp', + installers: [ + { architecture: 'x64', url: 'https://github.com/acme/myapp/releases/download/v2.1.0/myapp-2.1.0-x64.zip', sha256: 'a'.repeat(64) }, + ], + }); + + expect(result.artifact).toBe(join(outDir, 'myapp.nuspec')); + + const nuspec = await readFile(join(outDir, 'myapp.nuspec'), 'utf-8'); + expect(nuspec).toContain('myapp'); + expect(nuspec).toContain('2.1.0'); + expect(nuspec).toContain('My App'); + expect(nuspec).toContain('ACME Corp'); + expect(nuspec).toContain('acmecorp'); + expect(nuspec).toContain('https://example.com'); + expect(nuspec).toContain('An example CLI tool'); + expect(nuspec).toContain('cli tool example'); + + const installScript = await readFile(join(outDir, 'tools', 'chocolateyInstall.ps1'), 'utf-8'); + expect(installScript).toContain('https://github.com/acme/myapp/releases/download/v2.1.0/myapp-2.1.0-x64.zip'); + expect(installScript).toContain('a'.repeat(64)); + expect(installScript).toContain('Install-ChocolateyZipPackage'); + }); + + it('keeps dry-run shipping side-effect free', async () => { + await expect(adapter.ship(fakeShipContext({ + version: '2.1.0', + dryRun: true, + }) as any, { + packageId: 'myapp', + })).resolves.toEqual({ id: 'dry-run' }); + }); +});