Merge branch 'main' into gsxdsm/compound
This commit is contained in:
20
packages/cli/bin.mjs
Executable file
20
packages/cli/bin.mjs
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { constants } from "node:fs";
|
||||
import { access } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const packageDir = dirname(fileURLToPath(import.meta.url));
|
||||
const distEntry = resolve(packageDir, "dist", "bin.js");
|
||||
|
||||
try {
|
||||
await access(distEntry, constants.F_OK);
|
||||
} catch {
|
||||
globalThis.console.error(
|
||||
`Fusion CLI build output is missing at ${distEntry}. Run \`pnpm build\` before invoking this source checkout.`,
|
||||
);
|
||||
globalThis.process.exit(1);
|
||||
}
|
||||
|
||||
await import(pathToFileURL(distEntry).href);
|
||||
@@ -12,8 +12,8 @@
|
||||
"pi-package"
|
||||
],
|
||||
"bin": {
|
||||
"fn": "./dist/bin.js",
|
||||
"fusion": "./dist/bin.js"
|
||||
"fn": "./bin.mjs",
|
||||
"fusion": "./bin.mjs"
|
||||
},
|
||||
"pi": {
|
||||
"extensions": [
|
||||
@@ -28,6 +28,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"bin.mjs",
|
||||
"dist/**/*.js",
|
||||
"dist/**/*.d.ts",
|
||||
"dist/**/*.d.ts.map",
|
||||
|
||||
85
packages/cli/src/__tests__/bin-targets.test.ts
Normal file
85
packages/cli/src/__tests__/bin-targets.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join, normalize } from "node:path";
|
||||
import fg from "fast-glob";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const cliRoot = join(__dirname, "..", "..");
|
||||
const workspaceRoot = join(cliRoot, "..", "..");
|
||||
|
||||
type PackageManifest = {
|
||||
name?: string;
|
||||
bin?: string | Record<string, string>;
|
||||
};
|
||||
|
||||
type WorkspacePackage = {
|
||||
dir: string;
|
||||
manifestPath: string;
|
||||
manifest: PackageManifest;
|
||||
};
|
||||
|
||||
function loadWorkspacePatterns(): string[] {
|
||||
const workspaceManifestPath = join(workspaceRoot, "pnpm-workspace.yaml");
|
||||
const workspaceManifest = parse(readFileSync(workspaceManifestPath, "utf-8")) as {
|
||||
packages?: string[];
|
||||
};
|
||||
return workspaceManifest.packages ?? [];
|
||||
}
|
||||
|
||||
function listWorkspacePackages(): WorkspacePackage[] {
|
||||
const packageJsonPaths = fg
|
||||
.sync(loadWorkspacePatterns().map((pattern) => `${pattern}/package.json`), {
|
||||
cwd: workspaceRoot,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
unique: true,
|
||||
})
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
return packageJsonPaths.map((manifestPath) => ({
|
||||
dir: dirname(manifestPath),
|
||||
manifestPath,
|
||||
manifest: JSON.parse(readFileSync(manifestPath, "utf-8")) as PackageManifest,
|
||||
}));
|
||||
}
|
||||
|
||||
function listBins(manifest: PackageManifest): Array<[string, string]> {
|
||||
if (!manifest.bin) return [];
|
||||
if (typeof manifest.bin === "string") {
|
||||
const fallbackName = manifest.name ?? "<anonymous-bin>";
|
||||
return [[fallbackName, manifest.bin]];
|
||||
}
|
||||
return Object.entries(manifest.bin);
|
||||
}
|
||||
|
||||
describe("workspace bin targets", () => {
|
||||
const packagesWithBins = listWorkspacePackages().filter((pkg) => listBins(pkg.manifest).length > 0);
|
||||
|
||||
it("covers all workspace packages that declare bins", () => {
|
||||
const packageNames = packagesWithBins.map((pkg) => pkg.manifest.name).sort();
|
||||
expect(packageNames).toEqual([
|
||||
"@runfusion/fusion",
|
||||
"runfusion.ai",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(
|
||||
packagesWithBins.flatMap((pkg) =>
|
||||
listBins(pkg.manifest).map(([binName, target]) => ({
|
||||
packageName: pkg.manifest.name ?? pkg.manifestPath,
|
||||
packageDir: pkg.dir,
|
||||
binName,
|
||||
target,
|
||||
})),
|
||||
),
|
||||
)(
|
||||
'$packageName bin "$binName" points at a committed non-dist file',
|
||||
({ packageDir, target }) => {
|
||||
const normalizedTarget = normalize(target).replace(/^\.([/\\])/, "");
|
||||
const resolvedTarget = join(packageDir, normalizedTarget);
|
||||
|
||||
expect(normalizedTarget).not.toMatch(/^dist(?:[/\\]|$)/);
|
||||
expect(existsSync(resolvedTarget)).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1132,9 +1132,10 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
|
||||
});
|
||||
|
||||
describe("fn_mission_show", () => {
|
||||
it("returns mission with hierarchy", async () => {
|
||||
// Create mission
|
||||
it("returns mission with hierarchy and linked goals", async () => {
|
||||
const createTool = api.tools.get("fn_mission_create")!;
|
||||
const goalTool = api.tools.get("fn_goal_create")!;
|
||||
const linkTool = api.tools.get("fn_mission_link_goal")!;
|
||||
const created = await createTool.execute(
|
||||
"c1",
|
||||
{ title: "Test Mission" },
|
||||
@@ -1142,6 +1143,20 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
const goal = await goalTool.execute(
|
||||
"g1",
|
||||
{ title: "Connect mission work to goals" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
await linkTool.execute(
|
||||
"link-1",
|
||||
{ missionId: created.details.missionId, goalId: goal.details.goalId },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
const showTool = api.tools.get("fn_mission_show")!;
|
||||
const result = await showTool.execute(
|
||||
@@ -1154,6 +1169,11 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
|
||||
|
||||
expect(result.details.mission).toBeDefined();
|
||||
expect(result.content[0].text).toContain("Test Mission");
|
||||
expect(result.content[0].text).toContain("Linked Goals:");
|
||||
expect(result.content[0].text).toContain(`- ${goal.details.goalId}: Connect mission work to goals`);
|
||||
expect(result.details.mission.linkedGoals).toEqual([
|
||||
expect.objectContaining({ id: goal.details.goalId, title: "Connect mission work to goals" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders acceptanceCriteria / verification for milestones, slices, and features", async () => {
|
||||
@@ -1212,6 +1232,30 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
|
||||
expect(result.details.mission.milestones[0].acceptanceCriteria).toBe(longValue);
|
||||
});
|
||||
|
||||
it("renders an empty linked goals state when no goals are linked", async () => {
|
||||
const createTool = api.tools.get("fn_mission_create")!;
|
||||
const created = await createTool.execute(
|
||||
"c1",
|
||||
{ title: "Mission Without Goals" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
const showTool = api.tools.get("fn_mission_show")!;
|
||||
const result = await showTool.execute(
|
||||
"call-1",
|
||||
{ id: created.details.missionId },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
expect(result.content[0].text).toContain("Linked Goals:");
|
||||
expect(result.content[0].text).toContain("No linked goals.");
|
||||
expect(result.details.mission.linkedGoals).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns error when mission not found", async () => {
|
||||
const showTool = api.tools.get("fn_mission_show")!;
|
||||
const result = await showTool.execute(
|
||||
|
||||
@@ -36,14 +36,16 @@ describe("CLI package.json publishing config", () => {
|
||||
const pkg = loadPackageJson("cli");
|
||||
const prepackScript = loadCliPrepackScript();
|
||||
|
||||
it('has "bin" field with fn pointing to ./dist/bin.js', () => {
|
||||
it('has "bin" field with fn/fusion pointing to committed launcher', () => {
|
||||
expect(pkg.bin).toBeDefined();
|
||||
expect(pkg.bin.fn).toBe("./dist/bin.js");
|
||||
expect(pkg.bin.fn).toBe("./bin.mjs");
|
||||
expect(pkg.bin.fusion).toBe("./bin.mjs");
|
||||
});
|
||||
|
||||
it('has "files" array with refined globs for dist output', () => {
|
||||
it('has "files" array with committed launcher and refined globs for dist output', () => {
|
||||
expect(pkg.files).toBeDefined();
|
||||
expect(Array.isArray(pkg.files)).toBe(true);
|
||||
expect(pkg.files).toContain("bin.mjs");
|
||||
expect(pkg.files).toContain("dist/**/*.js");
|
||||
expect(pkg.files).toContain("dist/**/*.d.ts");
|
||||
expect(pkg.files).toContain("dist/**/*.d.ts.map");
|
||||
|
||||
@@ -2644,6 +2644,16 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("Linked Goals:");
|
||||
if ((mission.linkedGoals?.length ?? 0) === 0) {
|
||||
lines.push("No linked goals.");
|
||||
} else {
|
||||
for (const goal of mission.linkedGoals ?? []) {
|
||||
lines.push(`- ${goal.id}: ${goal.title}`);
|
||||
}
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
if (mission.milestones.length === 0) {
|
||||
lines.push("No milestones yet.");
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user