fix(cli): bundle pi-claude-cli into published package

Drops @fusion/pi-claude-cli from runtime dependencies so
`pnpm install -g @runfusion/fusion` no longer 404s on the unpublishable
private workspace package. The pi extension is staged into
dist/pi-claude-cli/ at build time and resolved relative to the running
module, with the workspace require.resolve preserved as a dev fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-24 15:34:06 -07:00
parent a6dd98957c
commit 39deb052f4
3 changed files with 50 additions and 15 deletions

View File

@@ -33,6 +33,7 @@
"dist/**/*.d.ts.map", "dist/**/*.d.ts.map",
"dist/**/*.js.map", "dist/**/*.js.map",
"dist/client/**", "dist/client/**",
"dist/pi-claude-cli/**",
"skill/**", "skill/**",
"README.md" "README.md"
], ],
@@ -46,7 +47,6 @@
"test:build-exe": "FUSION_TEST_BUILD_EXE=1 vitest run --config vitest.build-exe.config.ts --silent=passed-only --reporter=dot" "test:build-exe": "FUSION_TEST_BUILD_EXE=1 vitest run --config vitest.build-exe.config.ts --silent=passed-only --reporter=dot"
}, },
"dependencies": { "dependencies": {
"@fusion/pi-claude-cli": "workspace:*",
"@mariozechner/pi-ai": "^0.70.0", "@mariozechner/pi-ai": "^0.70.0",
"@mariozechner/pi-coding-agent": "^0.70.0", "@mariozechner/pi-coding-agent": "^0.70.0",
"express": "^5.1.0", "express": "^5.1.0",
@@ -73,6 +73,7 @@
"@fusion/core": "workspace:*", "@fusion/core": "workspace:*",
"@fusion/dashboard": "workspace:*", "@fusion/dashboard": "workspace:*",
"@fusion/engine": "workspace:*", "@fusion/engine": "workspace:*",
"@fusion/pi-claude-cli": "workspace:*",
"typebox": "^1.0.0", "typebox": "^1.0.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@vitest/coverage-v8": "^3.1.0", "@vitest/coverage-v8": "^3.1.0",

View File

@@ -43,22 +43,33 @@ export type ClaudeCliExtensionResolution =
/** /**
* Resolve the absolute path to `@fusion/pi-claude-cli`'s pi extension entry file. * Resolve the absolute path to `@fusion/pi-claude-cli`'s pi extension entry file.
* *
* Implementation notes: * The package is bundled into the published @runfusion/fusion as
* - `require.resolve("@fusion/pi-claude-cli/package.json")` is the canonical way * `dist/pi-claude-cli/` (see tsup.config.ts) so it is not a runtime npm
* to find a package's root from a dependent module without importing the * dependency. We look for that bundled copy first by walking up from this
* package itself. It respects pnpm's strict layout. * module's location, and fall back to `require.resolve` for monorepo
* - We read pi.extensions[0] from the package.json rather than assuming * dev/test runs where this file executes from `src/` rather than `dist/`.
* a fixed filename; if upstream renames the entry we still work.
* - `createRequire(import.meta.url)` anchors resolution to this module's
* physical location, not `process.cwd()`, so the dep is found wherever
* `@runfusion/fusion` itself is installed.
*/ */
export function resolveClaudeCliExtension(): ClaudeCliExtensionResolution { export function resolveClaudeCliExtension(): ClaudeCliExtensionResolution {
let pkgJsonPath: string; let pkgJsonPath: string | undefined;
try {
pkgJsonPath = require_.resolve("@fusion/pi-claude-cli/package.json"); // Bundled lookup: when running from dist/, sibling dir dist/pi-claude-cli/
} catch { // holds the staged extension. Walk up a few levels to also catch nested
return { status: "not-installed" }; // layouts (e.g. dist/commands/foo.js) without hard-coding depth.
const here = dirname(fileURLToPath(import.meta.url));
for (const rel of ["pi-claude-cli", "../pi-claude-cli", "../../pi-claude-cli"]) {
const candidate = resolve(here, rel, "package.json");
if (existsSync(candidate)) {
pkgJsonPath = candidate;
break;
}
}
if (!pkgJsonPath) {
try {
pkgJsonPath = require_.resolve("@fusion/pi-claude-cli/package.json");
} catch {
return { status: "not-installed" };
}
} }
let pkgJson: { pi?: { extensions?: unknown }; version?: string }; let pkgJson: { pi?: { extensions?: unknown }; version?: string };

View File

@@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const dashboardClientSrc = join(__dirname, "..", "dashboard", "dist", "client"); const dashboardClientSrc = join(__dirname, "..", "dashboard", "dist", "client");
const dashboardClientDest = join(__dirname, "dist", "client"); const dashboardClientDest = join(__dirname, "dist", "client");
const piClaudeCliSrc = join(__dirname, "..", "pi-claude-cli");
const piClaudeCliDest = join(__dirname, "dist", "pi-claude-cli");
const dashboardClientStub = `<!doctype html> const dashboardClientStub = `<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -38,6 +40,27 @@ export default defineConfig({
js: 'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);', js: 'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
}, },
onSuccess: async () => { onSuccess: async () => {
// Stage the vendored pi-claude-cli pi extension into dist/. It can't
// be bundled by esbuild because pi loads extensions as separate files
// at runtime via jiti, so we ship the raw .ts source. This also lets
// us drop @fusion/pi-claude-cli from the published package's
// dependencies — the workspace package is private and would 404 on
// `pnpm install` of @runfusion/fusion otherwise.
if (existsSync(piClaudeCliDest)) {
rmSync(piClaudeCliDest, { recursive: true, force: true });
}
if (existsSync(piClaudeCliSrc)) {
mkdirSync(piClaudeCliDest, { recursive: true });
cpSync(join(piClaudeCliSrc, "index.ts"), join(piClaudeCliDest, "index.ts"));
cpSync(join(piClaudeCliSrc, "src"), join(piClaudeCliDest, "src"), { recursive: true });
cpSync(join(piClaudeCliSrc, "package.json"), join(piClaudeCliDest, "package.json"));
console.log("Copied pi-claude-cli extension to dist/pi-claude-cli/");
} else {
console.warn(
`WARNING: pi-claude-cli source not found at ${piClaudeCliSrc}; useClaudeCli will not work in the published package.`,
);
}
if (existsSync(dashboardClientDest)) { if (existsSync(dashboardClientDest)) {
rmSync(dashboardClientDest, { recursive: true, force: true }); rmSync(dashboardClientDest, { recursive: true, force: true });
} }