feat: add npm build process, version bump, and forbidden token enforcement

- Add esbuild config to bundle CLI with all workspace code for npm publishing
- Add build-npm.sh script that runs forbidden token check, type-check,
  esbuild bundle, and generates publishable package.json
- Add generate-npm-package-json.mjs to resolve workspace:* refs to actual
  npm dependencies for publishing
- Add version-bump.sh for patch/minor/major/explicit version bumping
- Add check-forbidden-tokens.mjs that scans codebase for forbidden tokens
  (mirrors git hook logic, safe if token list is missing)
- Add esbuild as dev dependency
- Add build:npm, version:bump, check:tokens scripts to root package.json
- Update .gitignore for build artifacts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dotta
2026-03-03 09:25:10 -06:00
parent b6c321f30a
commit 4c6fe04700
10 changed files with 375 additions and 3 deletions

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env node
/**
* generate-npm-package-json.mjs
*
* Reads the dev package.json (which has workspace:* refs) and produces
* a publishable package.json in cli/ with:
* - workspace:* dependencies removed
* - all external dependencies from workspace packages inlined
* - proper metadata for npm
*
* Reads from cli/package.dev.json if it exists (build already ran),
* otherwise from cli/package.json.
*/
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, "..");
function readPkg(relativePath) {
return JSON.parse(readFileSync(resolve(repoRoot, relativePath, "package.json"), "utf8"));
}
// Read all workspace packages that the CLI depends on
const workspacePaths = [
"cli",
"server",
"packages/db",
"packages/shared",
"packages/adapter-utils",
"packages/adapters/claude-local",
"packages/adapters/codex-local",
"packages/adapters/openclaw",
];
// Collect all external dependencies from all workspace packages
const allDeps = {};
const allOptionalDeps = {};
for (const pkgPath of workspacePaths) {
const pkg = readPkg(pkgPath);
const deps = pkg.dependencies || {};
const optDeps = pkg.optionalDependencies || {};
for (const [name, version] of Object.entries(deps)) {
if (name.startsWith("@paperclipai/")) continue; // skip workspace refs
// Keep the more specific (pinned) version if conflict
if (!allDeps[name] || !version.startsWith("^")) {
allDeps[name] = version;
}
}
for (const [name, version] of Object.entries(optDeps)) {
allOptionalDeps[name] = version;
}
}
// Sort alphabetically
const sortedDeps = Object.fromEntries(Object.entries(allDeps).sort(([a], [b]) => a.localeCompare(b)));
const sortedOptDeps = Object.fromEntries(
Object.entries(allOptionalDeps).sort(([a], [b]) => a.localeCompare(b)),
);
// Read the CLI package metadata — prefer the dev backup if it exists
const devPkgPath = resolve(repoRoot, "cli/package.dev.json");
const cliPkg = existsSync(devPkgPath)
? JSON.parse(readFileSync(devPkgPath, "utf8"))
: readPkg("cli");
// Build the publishable package.json
const publishPkg = {
name: cliPkg.name,
version: cliPkg.version,
description: cliPkg.description,
type: cliPkg.type,
bin: cliPkg.bin,
keywords: cliPkg.keywords,
license: cliPkg.license,
repository: cliPkg.repository,
homepage: cliPkg.homepage,
files: cliPkg.files,
engines: { node: ">=20" },
dependencies: sortedDeps,
};
if (Object.keys(sortedOptDeps).length > 0) {
publishPkg.optionalDependencies = sortedOptDeps;
}
const output = JSON.stringify(publishPkg, null, 2) + "\n";
const outPath = resolve(repoRoot, "cli/package.json");
writeFileSync(outPath, output);
console.log(` ✓ Generated publishable package.json (${Object.keys(sortedDeps).length} deps)`);
console.log(` Version: ${cliPkg.version}`);