diff --git a/.mise.toml b/.mise.toml index 0a67c7c..3601cb9 100644 --- a/.mise.toml +++ b/.mise.toml @@ -27,3 +27,9 @@ run = "mise run install && bun scripts/render-winget-manifest.ts" [tasks.docker-smoke] run = "mise run build -- --targets=linux-x64-musl,linux-arm64-musl && docker build --platform=linux/amd64 -t skillet:local . && docker run --rm --platform=linux/amd64 skillet:local --help" + +[tasks.build-npm] +run = "mise run install && bun scripts/build-npm-cli.ts" + +[tasks.npm-smoke] +run = "mise run build-npm && npm pack >/tmp/skillet-npm-pack.log && PACKAGE=$(tail -n 1 /tmp/skillet-npm-pack.log) && npx --yes --package \"./$PACKAGE\" skillet --help && REPO_DIR=\"$PWD\" && TMP_DIR=$(mktemp -d) && (cd \"$TMP_DIR\" && bun init -y >/dev/null 2>&1 && bun add \"$REPO_DIR/$PACKAGE\" >/dev/null && bunx --bun skillet --help) && rm -rf \"$TMP_DIR\" \"$PACKAGE\"" diff --git a/README.md b/README.md index 58e44d3..ce2b4ba 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Skillet installs, discovers, and updates `SKILL.md`-based skills across supporte | Homebrew | `brew install skillet` | Configured | | Chocolatey | `choco install skillet` | Configured | | winget | `winget install skillet` | Configured | -| npm / npx | `npx skillet ...` | Planned | +| npm / npx | `npx skillet ...` | Configured | | Docker | `docker run ... skillet ...` | Configured | | Local dev | `mise run dev -- --help` | Available | @@ -145,3 +145,4 @@ Distribution docs: - Chocolatey: `docs/distribution/chocolatey.md` - winget: `docs/distribution/winget.md` - Docker: `docs/distribution/docker.md` +- npm: `docs/distribution/npm.md` diff --git a/docs/distribution/npm.md b/docs/distribution/npm.md new file mode 100644 index 0000000..0c010b9 --- /dev/null +++ b/docs/distribution/npm.md @@ -0,0 +1,27 @@ +# npm Distribution + +Skillet publishes an npm package so users can run: + +- `npx skillet ...` +- `bunx skillet ...` + +## Packaging Model + +- `src/cli.ts` is bundled to `dist/npm/cli.js` targeting Node. +- `package.json` `bin.skillet` points to `dist/npm/cli.js`. +- `prepack` rebuilds the npm CLI bundle automatically. + +## Local Validation + +```bash +mise run build-npm +npm pack +npx --yes --package ./skillet-.tgz skillet --help +bunx --bun --package ./skillet-.tgz skillet --help +``` + +## Publish + +```bash +npm publish --access public +``` diff --git a/package.json b/package.json index c9adc04..90ddeaa 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,28 @@ { "name": "skillet", "version": "0.0.0", - "private": true, + "private": false, "type": "module", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/npm", + "README.md" + ], "bin": { - "skillet": "src/cli.ts" + "skillet": "dist/npm/cli.js" }, "scripts": { "dev": "bun src/cli.ts", "test": "vitest run", "build": "bun scripts/build-release.ts", + "build:npm": "bun scripts/build-npm-cli.ts", "render:homebrew": "bun scripts/render-homebrew-formula.ts", "render:choco": "bun scripts/render-chocolatey-package.ts", "render:winget": "bun scripts/render-winget-manifest.ts", - "docker:smoke": "mise run docker-smoke" + "docker:smoke": "mise run docker-smoke", + "prepack": "bun scripts/build-npm-cli.ts" }, "dependencies": { "cac": "^6.7.14", diff --git a/scripts/build-npm-cli.ts b/scripts/build-npm-cli.ts new file mode 100644 index 0000000..1e9a564 --- /dev/null +++ b/scripts/build-npm-cli.ts @@ -0,0 +1,25 @@ +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; + +function main(): void { + const repoRoot = process.cwd(); + const outputPath = path.join(repoRoot, "dist", "npm", "cli.js"); + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + + execFileSync( + "bun", + ["build", "src/cli.ts", "--bundle", "--target=node", `--outfile=${outputPath}`], + { cwd: repoRoot, stdio: ["ignore", "inherit", "inherit"] }, + ); + + const output = fs.readFileSync(outputPath, "utf8"); + if (!output.startsWith("#!/usr/bin/env node")) { + fs.writeFileSync(outputPath, `#!/usr/bin/env node\n${output}`); + } + + fs.chmodSync(outputPath, 0o755); + console.log(`Wrote npm CLI bundle to ${outputPath}`); +} + +main(); diff --git a/src/cli.ts b/src/cli.ts index 57e7cd4..9643d51 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env bun +#!/usr/bin/env node import { cac } from "cac"; import { BUILD_INFO, buildMetadataString } from "./build-info"; import { runAddCommand } from "./commands/add"; diff --git a/tests/distribution/npm-package.test.ts b/tests/distribution/npm-package.test.ts new file mode 100644 index 0000000..173e887 --- /dev/null +++ b/tests/distribution/npm-package.test.ts @@ -0,0 +1,22 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +describe("npm package bundle", () => { + it("builds a node-compatible cli bundle", () => { + execFileSync("bun", ["scripts/build-npm-cli.ts"], { + cwd: process.cwd(), + stdio: ["ignore", "pipe", "pipe"], + }); + + const bundledCli = path.resolve(process.cwd(), "dist/npm/cli.js"); + const result = spawnSync("node", [bundledCli, "--help"], { + cwd: process.cwd(), + encoding: "utf8", + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("Usage:"); + expect(result.stdout).toContain("$ skillet [options]"); + }); +});