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 50e25128f6
commit eb5c575688
3 changed files with 50 additions and 15 deletions

View File

@@ -33,6 +33,7 @@
"dist/**/*.d.ts.map",
"dist/**/*.js.map",
"dist/client/**",
"dist/pi-claude-cli/**",
"skill/**",
"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"
},
"dependencies": {
"@fusion/pi-claude-cli": "workspace:*",
"@mariozechner/pi-ai": "^0.70.0",
"@mariozechner/pi-coding-agent": "^0.70.0",
"express": "^5.1.0",
@@ -73,6 +73,7 @@
"@fusion/core": "workspace:*",
"@fusion/dashboard": "workspace:*",
"@fusion/engine": "workspace:*",
"@fusion/pi-claude-cli": "workspace:*",
"typebox": "^1.0.0",
"@types/node": "^22.0.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.
*
* Implementation notes:
* - `require.resolve("@fusion/pi-claude-cli/package.json")` is the canonical way
* to find a package's root from a dependent module without importing the
* package itself. It respects pnpm's strict layout.
* - We read pi.extensions[0] from the package.json rather than assuming
* 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.
* The package is bundled into the published @runfusion/fusion as
* `dist/pi-claude-cli/` (see tsup.config.ts) so it is not a runtime npm
* dependency. We look for that bundled copy first by walking up from this
* module's location, and fall back to `require.resolve` for monorepo
* dev/test runs where this file executes from `src/` rather than `dist/`.
*/
export function resolveClaudeCliExtension(): ClaudeCliExtensionResolution {
let pkgJsonPath: string;
try {
pkgJsonPath = require_.resolve("@fusion/pi-claude-cli/package.json");
} catch {
return { status: "not-installed" };
let pkgJsonPath: string | undefined;
// Bundled lookup: when running from dist/, sibling dir dist/pi-claude-cli/
// holds the staged extension. Walk up a few levels to also catch nested
// 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 };

View File

@@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const dashboardClientSrc = join(__dirname, "..", "dashboard", "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>
<html lang="en">
<head>
@@ -38,6 +40,27 @@ export default defineConfig({
js: 'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
},
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)) {
rmSync(dashboardClientDest, { recursive: true, force: true });
}