#!/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 are BUNDLED into the CLI. // Note: "server" is excluded — it's published separately as a dependency. const workspacePaths = [ "cli", "packages/db", "packages/shared", "packages/adapter-utils", "packages/adapters/claude-local", "packages/adapters/codex-local", "packages/adapters/opencode-local", "packages/adapters/openclaw-gateway", ]; // Workspace packages that are NOT bundled and must stay as npm dependencies. // These get published separately via Changesets and resolved at runtime. const externalWorkspacePackages = new Set([ "@paperclipai/server", ]); // 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/") && !externalWorkspacePackages.has(name)) continue; // For external workspace packages, read their version directly if (externalWorkspacePackages.has(name)) { const pkgDirMap = { "@paperclipai/server": "server" }; const wsPkg = readPkg(pkgDirMap[name]); allDeps[name] = `^${wsPkg.version}`; continue; } // 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}`);