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..2a9b0303
--- /dev/null
+++ b/packages/targets/pkg-chocolatey/src/index.test.ts
@@ -0,0 +1,116 @@
+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/');
+ });
+
+ 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
new file mode 100644
index 00000000..a91e0c81
--- /dev/null
+++ b/packages/targets/pkg-chocolatey/src/index.ts
@@ -0,0 +1,175 @@
+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');
+}
+
+// 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') {
+ return [
+ `$ErrorActionPreference = 'Stop'`,
+ `$toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition`,
+ `$packageArgs = @{`,
+ ` packageName = '${psEscape(config.packageId)}'`,
+ ` unzipLocation = $toolsDir`,
+ ` url = '${psEscape(config.installerUrl)}'`,
+ ` checksum = '${psEscape(config.checksum)}'`,
+ ` checksumType = '${checksumType}'`,
+ `}`,
+ `Install-ChocolateyZipPackage @packageArgs`,
+ '',
+ ].join('\n');
+ }
+ return [
+ `$ErrorActionPreference = 'Stop'`,
+ `$packageArgs = @{`,
+ ` packageName = '${psEscape(config.packageId)}'`,
+ ` fileType = '${type}'`,
+ ` url = '${psEscape(config.installerUrl)}'`,
+ ` checksum = '${psEscape(config.checksum)}'`,
+ ` checksumType = '${checksumType}'`,
+ ` silentArgs = '${psEscape(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), '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) } };
+ }
+ // 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('@');
+ 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':