feat: integrate Changesets for multi-package npm publishing

Migrate from single-bundle CLI publishing to publishing all @paperclipai/*
packages individually via Changesets. This fixes the "Cannot find package
@paperclipai/server" error when installing from npm.

Changes:
- Add @changesets/cli with fixed versioning (all packages share version)
- Make 7 packages publishable (shared, adapter-utils, db, 3 adapters, server)
- Add build scripts, publishConfig, and files fields to all packages
- Mark @paperclipai/server as external in CLI esbuild config
- Simplify CLI importServerEntry() to use string-literal dynamic import
- Add generate-npm-package-json support for external workspace packages
- Create scripts/release.sh for one-command releases
- Remove old bump-and-publish.sh and version-bump.sh
- All packages start at version 0.2.0

Usage: ./scripts/release.sh patch|minor|major [--dry-run]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dotta
2026-03-03 14:46:16 -06:00
parent 6e7f948314
commit defccdd4d9
20 changed files with 1197 additions and 256 deletions

View File

@@ -12,10 +12,10 @@ import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, "..");
// Workspace packages whose code should be bundled into the CLI
// Workspace packages whose code should be bundled into the CLI.
// Note: "server" is excluded — it's published separately and resolved at runtime.
const workspacePaths = [
"cli",
"server",
"packages/db",
"packages/shared",
"packages/adapter-utils",
@@ -24,17 +24,31 @@ const workspacePaths = [
"packages/adapters/openclaw",
];
// Workspace packages that should NOT be bundled — they'll be published
// to npm and resolved at runtime (e.g. @paperclipai/server uses dynamic import).
const externalWorkspacePackages = new Set([
"@paperclipai/server",
]);
// Collect all external (non-workspace) npm package names
const externals = new Set();
for (const p of workspacePaths) {
const pkg = JSON.parse(readFileSync(resolve(repoRoot, p, "package.json"), "utf8"));
for (const name of Object.keys(pkg.dependencies || {})) {
if (!name.startsWith("@paperclipai/")) externals.add(name);
if (externalWorkspacePackages.has(name)) {
externals.add(name);
} else if (!name.startsWith("@paperclipai/")) {
externals.add(name);
}
}
for (const name of Object.keys(pkg.optionalDependencies || {})) {
externals.add(name);
}
}
// Also add all published workspace packages as external
for (const name of externalWorkspacePackages) {
externals.add(name);
}
/** @type {import('esbuild').BuildOptions} */
export default {

View File

@@ -1,6 +1,6 @@
{
"name": "paperclipai",
"version": "0.1.2",
"version": "0.2.0",
"description": "Paperclip CLI — orchestrate AI agent teams to run a business",
"type": "module",
"bin": {
@@ -23,9 +23,13 @@
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"build": "node --input-type=module -e \"import esbuild from 'esbuild'; import config from './esbuild.config.mjs'; await esbuild.build(config);\" && chmod +x dist/index.js",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {

View File

@@ -93,41 +93,23 @@ function maybeEnableUiDevMiddleware(entrypoint: string): void {
}
async function importServerEntry(): Promise<void> {
// Dev mode: try local workspace path (monorepo with tsx)
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const fileCandidates = [
path.resolve(projectRoot, "server/src/index.ts"),
path.resolve(projectRoot, "server/dist/index.js"),
];
const existingFileCandidates = fileCandidates.filter((filePath) => fs.existsSync(filePath));
if (existingFileCandidates.length > 0) {
for (const filePath of existingFileCandidates) {
try {
maybeEnableUiDevMiddleware(filePath);
await import(pathToFileURL(filePath).href);
return;
} catch (err) {
throw new Error(`Failed to start Paperclip server from ${filePath}: ${formatError(err)}`);
}
}
const devEntry = path.resolve(projectRoot, "server/src/index.ts");
if (fs.existsSync(devEntry)) {
maybeEnableUiDevMiddleware(devEntry);
await import(pathToFileURL(devEntry).href);
return;
}
const specifierCandidates: string[] = ["@paperclipai/server/dist/index.js", "@paperclipai/server/src/index.ts"];
const missingErrors: string[] = [];
for (const specifier of specifierCandidates) {
try {
maybeEnableUiDevMiddleware(specifier);
await import(specifier);
return;
} catch (err) {
if (isModuleNotFoundError(err)) {
missingErrors.push(`${specifier}: ${formatError(err)}`);
continue;
}
throw new Error(`Failed to start Paperclip server from ${specifier}: ${formatError(err)}`);
}
// Production mode: import the published @paperclipai/server package
try {
await import("@paperclipai/server");
} catch (err) {
throw new Error(
`Could not locate a Paperclip server entrypoint.\n` +
`Tried: ${devEntry}, @paperclipai/server\n` +
`${formatError(err)}`,
);
}
throw new Error(
`Could not locate a Paperclip server entrypoint. Tried: ${[...fileCandidates, ...specifierCandidates].join(", ")}\n${missingErrors.join("\n")}`,
);
}

View File

@@ -23,7 +23,7 @@ const DATA_DIR_OPTION_HELP =
program
.name("paperclipai")
.description("Paperclip CLI — setup, diagnose, and configure your instance")
.version("0.1.2");
.version("0.2.0");
program.hook("preAction", (_thisCommand, actionCommand) => {
const options = actionCommand.optsWithGlobals() as DataDirOptionLike;