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

@@ -1,126 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# bump-and-publish.sh — One-command version bump, build, publish, and cleanup.
#
# Usage:
# ./scripts/bump-and-publish.sh patch # 0.1.1 → 0.1.2
# ./scripts/bump-and-publish.sh minor # 0.1.1 → 0.2.0
# ./scripts/bump-and-publish.sh major # 0.1.1 → 1.0.0
# ./scripts/bump-and-publish.sh 2.0.0 # set explicit version
# ./scripts/bump-and-publish.sh patch --dry-run # everything except npm publish
#
# Steps:
# 1. Bump version (cli/package.json + cli/src/index.ts)
# 2. Build for npm (token check, typecheck, esbuild, publishable package.json)
# 3. Preview (npm pack --dry-run)
# 4. Publish to npm (unless --dry-run)
# 5. Restore dev package.json
# 6. Commit and tag
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CLI_DIR="$REPO_ROOT/cli"
# ── Parse args ────────────────────────────────────────────────────────────────
dry_run=false
bump_type=""
for arg in "$@"; do
case "$arg" in
--dry-run) dry_run=true ;;
*) bump_type="$arg" ;;
esac
done
if [ -z "$bump_type" ]; then
echo "Usage: $0 <patch|minor|major|X.Y.Z> [--dry-run]"
exit 1
fi
# ── Preflight checks ─────────────────────────────────────────────────────────
if [ "$dry_run" = false ]; then
if ! npm whoami &>/dev/null; then
echo "Error: Not logged in to npm. Run 'npm login' first."
exit 1
fi
fi
# Check for uncommitted changes (version bump and build will create changes)
if ! git -C "$REPO_ROOT" diff --quiet || ! git -C "$REPO_ROOT" diff --cached --quiet; then
echo "Error: Working tree has uncommitted changes. Commit or stash them first."
exit 1
fi
# ── Step 1: Version bump ─────────────────────────────────────────────────────
echo ""
echo "==> Step 1/6: Bumping version..."
"$REPO_ROOT/scripts/version-bump.sh" "$bump_type"
# Read the new version
NEW_VERSION=$(node -e "console.log(require('$CLI_DIR/package.json').version)")
# ── Step 2: Build ─────────────────────────────────────────────────────────────
echo ""
echo "==> Step 2/6: Building for npm..."
"$REPO_ROOT/scripts/build-npm.sh"
# ── Step 3: Preview ───────────────────────────────────────────────────────────
echo ""
echo "==> Step 3/6: Preview..."
cd "$CLI_DIR"
npm pack --dry-run
cd "$REPO_ROOT"
# ── Step 4: Publish ───────────────────────────────────────────────────────────
if [ "$dry_run" = true ]; then
echo ""
echo "==> Step 4/6: Skipping publish (--dry-run)"
else
echo ""
echo "==> Step 4/6: Publishing to npm..."
cd "$CLI_DIR"
npm publish --access public
cd "$REPO_ROOT"
echo " ✓ Published paperclipai@$NEW_VERSION"
fi
# ── Step 5: Restore dev package.json ──────────────────────────────────────────
echo ""
echo "==> Step 5/6: Restoring dev package.json..."
mv "$CLI_DIR/package.dev.json" "$CLI_DIR/package.json"
echo " ✓ Restored workspace:* dependencies"
# ── Step 6: Commit and tag ────────────────────────────────────────────────────
echo ""
echo "==> Step 6/6: Committing and tagging..."
cd "$REPO_ROOT"
git add cli/package.json cli/src/index.ts
git commit -m "chore: bump version to $NEW_VERSION"
git tag "v$NEW_VERSION"
echo " ✓ Committed and tagged v$NEW_VERSION"
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
if [ "$dry_run" = true ]; then
echo "Dry run complete for v$NEW_VERSION."
echo " - Version bumped, built, and previewed"
echo " - Dev package.json restored"
echo " - Commit and tag created (locally)"
echo ""
echo "To actually publish, run:"
echo " cd cli && npm publish --access public"
else
echo "Published paperclipai@$NEW_VERSION"
echo ""
echo "To push:"
echo " git push && git push origin v$NEW_VERSION"
fi

View File

@@ -23,10 +23,10 @@ function readPkg(relativePath) {
return JSON.parse(readFileSync(resolve(repoRoot, relativePath, "package.json"), "utf8"));
}
// Read all workspace packages that the CLI depends on
// Read all workspace packages that are BUNDLED into the CLI.
// Note: "server" is excluded — it's published separately as a dependency.
const workspacePaths = [
"cli",
"server",
"packages/db",
"packages/shared",
"packages/adapter-utils",
@@ -35,6 +35,12 @@ const workspacePaths = [
"packages/adapters/openclaw",
];
// 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 = {};
@@ -45,7 +51,14 @@ for (const pkgPath of workspacePaths) {
const optDeps = pkg.optionalDependencies || {};
for (const [name, version] of Object.entries(deps)) {
if (name.startsWith("@paperclipai/")) continue; // skip workspace refs
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;

203
scripts/release.sh Executable file
View File

@@ -0,0 +1,203 @@
#!/usr/bin/env bash
set -euo pipefail
# release.sh — One-command version bump, build, and publish via Changesets.
#
# Usage:
# ./scripts/release.sh patch # 0.2.0 → 0.2.1
# ./scripts/release.sh minor # 0.2.0 → 0.3.0
# ./scripts/release.sh major # 0.2.0 → 1.0.0
# ./scripts/release.sh patch --dry-run # everything except npm publish
#
# Steps:
# 1. Preflight checks (clean tree, npm login)
# 2. Auto-create a changeset for all public packages
# 3. Run changeset version (bumps versions, generates CHANGELOGs)
# 4. Build all packages
# 5. Build CLI bundle (esbuild)
# 6. Publish to npm via changeset publish (unless --dry-run)
# 7. Commit and tag
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CLI_DIR="$REPO_ROOT/cli"
# ── Parse args ────────────────────────────────────────────────────────────────
dry_run=false
bump_type=""
for arg in "$@"; do
case "$arg" in
--dry-run) dry_run=true ;;
*) bump_type="$arg" ;;
esac
done
if [ -z "$bump_type" ]; then
echo "Usage: $0 <patch|minor|major> [--dry-run]"
exit 1
fi
if [[ ! "$bump_type" =~ ^(patch|minor|major)$ ]]; then
echo "Error: bump type must be patch, minor, or major (got '$bump_type')"
exit 1
fi
# ── Step 1: Preflight checks ─────────────────────────────────────────────────
echo ""
echo "==> Step 1/7: Preflight checks..."
if [ "$dry_run" = false ]; then
if ! npm whoami &>/dev/null; then
echo "Error: Not logged in to npm. Run 'npm login' first."
exit 1
fi
echo " ✓ Logged in to npm as $(npm whoami)"
fi
if ! git -C "$REPO_ROOT" diff --quiet || ! git -C "$REPO_ROOT" diff --cached --quiet; then
echo "Error: Working tree has uncommitted changes. Commit or stash them first."
exit 1
fi
echo " ✓ Working tree is clean"
# ── Step 2: Auto-create changeset ────────────────────────────────────────────
echo ""
echo "==> Step 2/7: Creating changeset ($bump_type bump for all packages)..."
# Get all publishable (non-private) package names
PACKAGES=$(node -e "
const { readdirSync, readFileSync } = require('fs');
const { resolve } = require('path');
const root = '$REPO_ROOT';
const wsYaml = readFileSync(resolve(root, 'pnpm-workspace.yaml'), 'utf8');
const dirs = ['packages/shared', 'packages/adapter-utils', 'packages/db',
'packages/adapters/claude-local', 'packages/adapters/codex-local', 'packages/adapters/openclaw',
'server', 'cli'];
const names = [];
for (const d of dirs) {
try {
const pkg = JSON.parse(readFileSync(resolve(root, d, 'package.json'), 'utf8'));
if (!pkg.private) names.push(pkg.name);
} catch {}
}
console.log(names.join('\n'));
")
# Write a changeset file
CHANGESET_FILE="$REPO_ROOT/.changeset/release-bump.md"
{
echo "---"
while IFS= read -r pkg; do
echo "\"$pkg\": $bump_type"
done <<< "$PACKAGES"
echo "---"
echo ""
echo "Version bump ($bump_type)"
} > "$CHANGESET_FILE"
echo " ✓ Created changeset for $(echo "$PACKAGES" | wc -l | xargs) packages"
# ── Step 3: Version packages ─────────────────────────────────────────────────
echo ""
echo "==> Step 3/7: Running changeset version..."
cd "$REPO_ROOT"
npx changeset version
echo " ✓ Versions bumped and CHANGELOGs generated"
# Read the new version from the CLI package
NEW_VERSION=$(node -e "console.log(require('$CLI_DIR/package.json').version)")
echo " New version: $NEW_VERSION"
# Update the version string in cli/src/index.ts
CURRENT_VERSION_IN_SRC=$(grep -oP '\.version\("\K[^"]+' "$CLI_DIR/src/index.ts" || echo "")
if [ -n "$CURRENT_VERSION_IN_SRC" ] && [ "$CURRENT_VERSION_IN_SRC" != "$NEW_VERSION" ]; then
sed -i '' "s/\.version(\"$CURRENT_VERSION_IN_SRC\")/\.version(\"$NEW_VERSION\")/" "$CLI_DIR/src/index.ts"
echo " ✓ Updated cli/src/index.ts version to $NEW_VERSION"
fi
# ── Step 4: Build packages ───────────────────────────────────────────────────
echo ""
echo "==> Step 4/7: Building all packages..."
cd "$REPO_ROOT"
# Build packages in dependency order (excluding UI and CLI)
pnpm --filter @paperclipai/shared build
pnpm --filter @paperclipai/adapter-utils build
pnpm --filter @paperclipai/db build
pnpm --filter @paperclipai/adapter-claude-local build
pnpm --filter @paperclipai/adapter-codex-local build
pnpm --filter @paperclipai/adapter-openclaw build
pnpm --filter @paperclipai/server build
echo " ✓ All packages built"
# ── Step 5: Build CLI bundle ─────────────────────────────────────────────────
echo ""
echo "==> Step 5/7: Building CLI bundle..."
cd "$REPO_ROOT"
"$REPO_ROOT/scripts/build-npm.sh" --skip-checks
echo " ✓ CLI bundled"
# ── Step 6: Publish ──────────────────────────────────────────────────────────
if [ "$dry_run" = true ]; then
echo ""
echo "==> Step 6/7: Skipping publish (--dry-run)"
echo ""
echo " Preview what would be published:"
for dir in packages/shared packages/adapter-utils packages/db \
packages/adapters/claude-local packages/adapters/codex-local packages/adapters/openclaw \
server cli; do
echo " --- $dir ---"
cd "$REPO_ROOT/$dir"
npm pack --dry-run 2>&1 | tail -3
done
cd "$REPO_ROOT"
else
echo ""
echo "==> Step 6/7: Publishing to npm..."
cd "$REPO_ROOT"
npx changeset publish
echo " ✓ Published all packages"
fi
# ── Step 7: Restore CLI dev package.json and commit ──────────────────────────
echo ""
echo "==> Step 7/7: Restoring dev package.json, committing, and tagging..."
cd "$REPO_ROOT"
# Restore the dev package.json (build-npm.sh backs it up)
if [ -f "$CLI_DIR/package.dev.json" ]; then
mv "$CLI_DIR/package.dev.json" "$CLI_DIR/package.json"
echo " ✓ Restored workspace dependencies in cli/package.json"
fi
# Commit all changes
git add -A
git commit -m "chore: release v$NEW_VERSION"
git tag "v$NEW_VERSION"
echo " ✓ Committed and tagged v$NEW_VERSION"
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
if [ "$dry_run" = true ]; then
echo "Dry run complete for v$NEW_VERSION."
echo " - Versions bumped, built, and previewed"
echo " - Dev package.json restored"
echo " - Commit and tag created (locally)"
echo ""
echo "To actually publish, run:"
echo " ./scripts/release.sh $bump_type"
else
echo "Published all packages at v$NEW_VERSION"
echo ""
echo "To push:"
echo " git push && git push origin v$NEW_VERSION"
fi

View File

@@ -1,71 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# version-bump.sh — Bump the version of the paperclipai CLI package.
#
# Usage:
# ./scripts/version-bump.sh patch # 0.1.0 → 0.1.1
# ./scripts/version-bump.sh minor # 0.1.0 → 0.2.0
# ./scripts/version-bump.sh major # 0.1.0 → 1.0.0
# ./scripts/version-bump.sh 1.2.3 # set explicit version
#
# Updates version in:
# - cli/package.json (source of truth)
# - cli/src/index.ts (commander .version())
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CLI_PKG="$REPO_ROOT/cli/package.json"
CLI_INDEX="$REPO_ROOT/cli/src/index.ts"
if [ $# -lt 1 ]; then
echo "Usage: $0 <patch|minor|major|X.Y.Z>"
exit 1
fi
BUMP_TYPE="$1"
# Read current version
CURRENT=$(node -e "console.log(require('$CLI_PKG').version)")
# Calculate new version
case "$BUMP_TYPE" in
patch|minor|major)
IFS='.' read -r major minor patch <<< "$CURRENT"
case "$BUMP_TYPE" in
patch) patch=$((patch + 1)) ;;
minor) minor=$((minor + 1)); patch=0 ;;
major) major=$((major + 1)); minor=0; patch=0 ;;
esac
NEW_VERSION="$major.$minor.$patch"
;;
*)
# Validate explicit version format
if ! echo "$BUMP_TYPE" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Error: Invalid version format '$BUMP_TYPE'. Expected X.Y.Z"
exit 1
fi
NEW_VERSION="$BUMP_TYPE"
;;
esac
echo "Bumping version: $CURRENT$NEW_VERSION"
# Update cli/package.json
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('$CLI_PKG', 'utf8'));
pkg.version = '$NEW_VERSION';
fs.writeFileSync('$CLI_PKG', JSON.stringify(pkg, null, 2) + '\n');
"
echo " ✓ Updated cli/package.json"
# Update cli/src/index.ts — the .version("X.Y.Z") call
sed -i '' "s/\.version(\"$CURRENT\")/\.version(\"$NEW_VERSION\")/" "$CLI_INDEX"
echo " ✓ Updated cli/src/index.ts"
echo ""
echo "Version bumped to $NEW_VERSION"
echo "Run ./scripts/build-npm.sh to build, then commit and tag:"
echo " git add cli/package.json cli/src/index.ts"
echo " git commit -m \"chore: bump version to $NEW_VERSION\""
echo " git tag v$NEW_VERSION"