fix(FN-2897): harden npm bundle packaging and merge recovery flows

- Strip private @fusion/* workspace devDependencies from the published CLI manifest via prepare-publish-manifest and package metadata updates
- Replace cross-spawn usage and add staged bundle layout assertions to verify resolver output in dist packaging
- Add per-task/project model override resolution across core, dashboard settings/task modals, and route coverage with new regression tests
- Strengthen engine merge/recovery handling for paused/interrupted/squash paths and surface merger timeline activity with additional self-healing and merger tests
- Add changesets for npm bundle dependency fixes, project model override stabilization, and FTS5 corruption recovery

Fusion-Task-Id: FN-2897
This commit is contained in:
Fusion
2026-04-28 18:23:13 -07:00
committed by gsxdsm
parent 3f061460b9
commit 7960b3d4c6
10 changed files with 103 additions and 39 deletions

View File

@@ -40,6 +40,8 @@
"scripts": {
"dev": "tsx src/bin.ts",
"prebuild": "node ../../scripts/sync-fusion-skill-tools.mjs",
"prepack": "node ./scripts/prepare-publish-manifest.mjs prepack",
"postpack": "node ./scripts/prepare-publish-manifest.mjs postpack",
"build": "tsup",
"build:exe": "bun run build.ts",
"build:exe:all": "bun run build.ts --all",

View File

@@ -0,0 +1,42 @@
/* global process, URL, console */
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
const mode = process.argv[2];
const packageJsonPath = new URL("../package.json", import.meta.url);
const backupPath = new URL("../package.json.pack-backup", import.meta.url);
if (mode === "prepack") {
if (existsSync(backupPath)) {
// Clean up stale backup from interrupted runs.
unlinkSync(backupPath);
}
const original = readFileSync(packageJsonPath, "utf8");
writeFileSync(backupPath, original, "utf8");
const pkg = JSON.parse(original);
const devDependencies = { ...(pkg.devDependencies || {}) };
delete devDependencies["@fusion/core"];
delete devDependencies["@fusion/dashboard"];
delete devDependencies["@fusion/engine"];
delete devDependencies["@fusion/pi-claude-cli"];
pkg.devDependencies = devDependencies;
writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
process.exit(0);
}
if (mode === "postpack") {
if (!existsSync(backupPath)) {
process.exit(0);
}
const backup = readFileSync(backupPath, "utf8");
writeFileSync(packageJsonPath, backup, "utf8");
unlinkSync(backupPath);
process.exit(0);
}
console.error("Usage: node ./scripts/prepare-publish-manifest.mjs <prepack|postpack>");
process.exit(1);

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, beforeAll } from "vitest";
import { readFileSync, existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import {
buildCliWithRealDashboardAssets,
bundlePath,
@@ -9,6 +10,7 @@ import {
dashboardClientStubMarker,
readClientIndexHtml,
} from "./bundle-output-helpers";
import { resolveClaudeCliExtensionFromModuleUrl } from "../commands/claude-cli-extension";
const tsupConfigPath = join(cliRoot, "tsup.config.ts");
@@ -96,6 +98,41 @@ describe("CLI bundle output", () => {
expect(content).toMatch(/from\s+["']node:path["']/);
});
it("resolveClaudeCliExtension succeeds against the staged dist/ layout", () => {
const result = resolveClaudeCliExtensionFromModuleUrl(pathToFileURL(bundlePath).href);
expect(result.status).toBe("ok");
if (result.status === "ok") {
expect(result.path).toBe(join(cliRoot, "dist", "pi-claude-cli", "index.ts"));
expect(result.packageVersion).toMatch(/\d+\.\d+\.\d+/);
}
});
it("dist/pi-claude-cli/ is staged with correct files", () => {
const stagedRoot = join(cliRoot, "dist", "pi-claude-cli");
expect(existsSync(join(stagedRoot, "package.json"))).toBe(true);
expect(existsSync(join(stagedRoot, "index.ts"))).toBe(true);
expect(existsSync(join(stagedRoot, "src", "process-manager.ts"))).toBe(true);
});
it("pi-claude-cli source does not import cross-spawn", () => {
const processManagerSource = readFileSync(join(cliRoot, "dist", "pi-claude-cli", "src", "process-manager.ts"), "utf-8");
expect(processManagerSource).not.toMatch(/import\s+.*cross-spawn/);
expect(processManagerSource).toMatch(/import\s*\{[^}]*spawn[^}]*\}\s*from\s*["']node:child_process["']/);
});
it("pi-claude-cli package.json has no cross-spawn dependency", () => {
const packageJson = JSON.parse(
readFileSync(join(cliRoot, "dist", "pi-claude-cli", "package.json"), "utf-8"),
) as {
dependencies?: Record<string, string>;
};
expect(packageJson.dependencies?.["cross-spawn"]).toBeUndefined();
});
it("runtime native assets are staged after build:exe", () => {
const runtimeDir = join(cliRoot, "dist", "runtime");
if (!existsSync(runtimeDir)) return;

View File

@@ -49,13 +49,15 @@ export type ClaudeCliExtensionResolution =
* 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 {
export function resolveClaudeCliExtensionFromModuleUrl(
moduleUrl: string,
): ClaudeCliExtensionResolution {
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));
const here = dirname(fileURLToPath(moduleUrl));
for (const rel of ["pi-claude-cli", "../pi-claude-cli", "../../pi-claude-cli"]) {
const candidate = resolve(here, rel, "package.json");
if (existsSync(candidate)) {
@@ -113,6 +115,10 @@ export function resolveClaudeCliExtension(): ClaudeCliExtensionResolution {
};
}
export function resolveClaudeCliExtension(): ClaudeCliExtensionResolution {
return resolveClaudeCliExtensionFromModuleUrl(import.meta.url);
}
/**
* Compute the paths to append to `discoverAndLoadExtensions`' configuredPaths
* based on the user's `useClaudeCli` setting.