@fusionkit/cli 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +3 -17
- package/dist/commands/fusion.js +14 -10
- package/dist/cursor-acp.d.ts +20 -0
- package/dist/cursor-acp.js +205 -0
- package/dist/gateway.js +13 -1
- package/dist/test/cli.test.js +24 -139
- package/package.json +9 -9
- package/scope/.next/BUILD_ID +1 -1
- package/scope/.next/app-build-manifest.json +9 -9
- package/scope/.next/app-path-routes-manifest.json +2 -2
- package/scope/.next/build-manifest.json +2 -2
- package/scope/.next/prerender-manifest.json +16 -16
- package/scope/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/_not-found.html +1 -1
- package/scope/.next/server/app/_not-found.rsc +1 -1
- package/scope/.next/server/app/api/environments/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/ingest/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/replay/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/sessions/[traceId]/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/environments/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/environments.html +1 -1
- package/scope/.next/server/app/environments.rsc +1 -1
- package/scope/.next/server/app/index.html +1 -1
- package/scope/.next/server/app/index.rsc +1 -1
- package/scope/.next/server/app/models/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/models.html +1 -1
- package/scope/.next/server/app/models.rsc +1 -1
- package/scope/.next/server/app/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/sessions/[traceId]/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app-paths-manifest.json +2 -2
- package/scope/.next/server/functions-config-manifest.json +1 -1
- package/scope/.next/server/pages/404.html +1 -1
- package/scope/.next/server/pages/500.html +1 -1
- package/scope/.next/server/server-reference-manifest.json +1 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.js +0 -24
- package/dist/commands/lifecycle.d.ts +0 -2
- package/dist/commands/lifecycle.js +0 -124
- package/dist/commands/plane.d.ts +0 -2
- package/dist/commands/plane.js +0 -38
- package/dist/commands/run.d.ts +0 -2
- package/dist/commands/run.js +0 -149
- package/dist/commands/runner.d.ts +0 -2
- package/dist/commands/runner.js +0 -33
- package/dist/commands/secrets.d.ts +0 -2
- package/dist/commands/secrets.js +0 -21
- /package/scope/.next/static/{5tnFLuvnSbNZNtqRgoot8 → vxqImMqlOwssVTua5Facf}/_buildManifest.js +0 -0
- /package/scope/.next/static/{5tnFLuvnSbNZNtqRgoot8 → vxqImMqlOwssVTua5Facf}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ cloud panel (skip with `--yes`). Use `--local` for the on-device MLX panel, or
|
|
|
47
47
|
Tired of long flag lines? Scaffold a committed `fusionkit.json`:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
fusionkit
|
|
50
|
+
fusionkit init
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
It records the panel, judge, default tool, and run defaults so the whole team
|
|
@@ -60,7 +60,7 @@ config and a dry-run preview with `fusionkit status`.
|
|
|
60
60
|
- `fusionkit codex | claude | cursor` — launch that agent backed by the panel.
|
|
61
61
|
- `fusionkit serve` — just run the gateway and print setup snippets for any tool.
|
|
62
62
|
- `fusionkit fusion [tool]` — the generic launcher (interactive picker on a TTY).
|
|
63
|
-
- `fusionkit
|
|
63
|
+
- `fusionkit init` — scaffold `fusionkit.json` for this repo.
|
|
64
64
|
- `fusionkit doctor` — check prerequisites with fix hints.
|
|
65
65
|
- `fusionkit status` — show the effective config and what a run will do.
|
|
66
66
|
|
package/dist/cli.js
CHANGED
|
@@ -5,18 +5,11 @@ import { FUSIONKIT_PYPI_VERSION } from "./fusion-quickstart.js";
|
|
|
5
5
|
import { registerDoctor } from "./commands/doctor.js";
|
|
6
6
|
import { registerEnsemble } from "./commands/ensemble.js";
|
|
7
7
|
import { registerFusion } from "./commands/fusion.js";
|
|
8
|
-
import { registerInit } from "./commands/init.js";
|
|
9
|
-
import { registerLifecycle } from "./commands/lifecycle.js";
|
|
10
8
|
import { registerLocal } from "./commands/local.js";
|
|
11
|
-
import { registerPlane } from "./commands/plane.js";
|
|
12
|
-
import { registerRun } from "./commands/run.js";
|
|
13
|
-
import { registerRunner } from "./commands/runner.js";
|
|
14
|
-
import { registerSecrets } from "./commands/secrets.js";
|
|
15
9
|
/**
|
|
16
|
-
* Build the `fusionkit` command tree.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* global home directory via `program.opts().dir`.
|
|
10
|
+
* Build the `fusionkit` command tree. `enablePositionalOptions` keeps the
|
|
11
|
+
* launcher commands' passthrough unambiguous (fusionkit's own flags must
|
|
12
|
+
* precede the tool name). Each `register*` helper attaches its command(s).
|
|
20
13
|
*/
|
|
21
14
|
function cliVersion() {
|
|
22
15
|
// dist/cli.js -> ../package.json is the published package manifest.
|
|
@@ -34,14 +27,7 @@ export function buildProgram() {
|
|
|
34
27
|
.name("fusionkit")
|
|
35
28
|
.description("real model fusion behind your coding agent (codex, claude, cursor)")
|
|
36
29
|
.version(`@fusionkit/cli ${cliVersion()} (synthesizer: fusionkit@${FUSIONKIT_PYPI_VERSION} from PyPI)`, "-v, --version", "print the CLI (npm) and pinned synthesizer (PyPI) versions")
|
|
37
|
-
.option("-d, --dir <dir>", "fusionkit home (default: ./.fusionkit)")
|
|
38
30
|
.enablePositionalOptions();
|
|
39
|
-
registerInit(program);
|
|
40
|
-
registerPlane(program);
|
|
41
|
-
registerRunner(program);
|
|
42
|
-
registerSecrets(program);
|
|
43
|
-
registerRun(program);
|
|
44
|
-
registerLifecycle(program);
|
|
45
31
|
registerEnsemble(program);
|
|
46
32
|
registerLocal(program);
|
|
47
33
|
registerFusion(program);
|
package/dist/commands/fusion.js
CHANGED
|
@@ -129,24 +129,28 @@ function resolveContext(opts) {
|
|
|
129
129
|
return { options, ...(config?.tool !== undefined ? { configTool: config.tool } : {}) };
|
|
130
130
|
}
|
|
131
131
|
export function registerFusion(program) {
|
|
132
|
+
// Top-level `init` — scaffold a committed fusionkit.json for this repo.
|
|
133
|
+
program
|
|
134
|
+
.command("init")
|
|
135
|
+
.description("scaffold a committed fusionkit.json for this repo")
|
|
136
|
+
.option("--repo <dir>", "coding workspace the panel fuses over")
|
|
137
|
+
.option("--force", "overwrite an existing fusionkit.json")
|
|
138
|
+
.action(async (opts) => {
|
|
139
|
+
const repoRoot = configRepoRoot(resolveOptions(opts));
|
|
140
|
+
const code = await runFusionInit({ repoRoot, force: opts.force === true });
|
|
141
|
+
process.exit(code);
|
|
142
|
+
});
|
|
132
143
|
// Generic `fusion [tool]` — keeps the original surface and interactive pick.
|
|
133
144
|
applyFusionOptions(program
|
|
134
145
|
.command("fusion")
|
|
135
146
|
.description("one command: real model fusion backs a coding agent")
|
|
136
|
-
.argument("[tool]", `${FUSION_TOOLS.join(" | ")} |
|
|
147
|
+
.argument("[tool]", `${FUSION_TOOLS.join(" | ")} | stop (omit on a TTY to pick interactively)`)
|
|
137
148
|
.argument("[args...]", "arguments forwarded to the tool")
|
|
138
|
-
.option("--tool <tool>", `coding agent to launch (${FUSION_TOOLS.join(" | ")})`)
|
|
139
|
-
.option("--force", "overwrite an existing fusionkit.json (with `fusion init`)"))
|
|
149
|
+
.option("--tool <tool>", `coding agent to launch (${FUSION_TOOLS.join(" | ")})`))
|
|
140
150
|
.addHelpText("after", "\nfusionkit's own flags must precede the tool name; everything after the tool is forwarded to it." +
|
|
141
|
-
"\nRun `fusionkit
|
|
151
|
+
"\nRun `fusionkit init` to scaffold a committed fusionkit.json for this repo." +
|
|
142
152
|
"\nRun `fusionkit fusion stop` to reap portless singleton services (router, dashboard, ...).")
|
|
143
153
|
.action(async (positionalTool, args, opts) => {
|
|
144
|
-
// `fusion init` scaffolds the per-repo config instead of launching a tool.
|
|
145
|
-
if (positionalTool === "init") {
|
|
146
|
-
const repoRoot = configRepoRoot(resolveOptions(opts));
|
|
147
|
-
const code = await runFusionInit({ repoRoot, force: opts.force === true });
|
|
148
|
-
process.exit(code);
|
|
149
|
-
}
|
|
150
154
|
// `fusion stop` reaps persistent portless singletons left running by prior
|
|
151
155
|
// runs (the router, dashboard, ...).
|
|
152
156
|
if (positionalTool === "stop") {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Cursor ACP front-door producer. Spawns the Cursorkit bridge (its
|
|
3
|
+
* local-model backend pointed at the running Fusion Harness Gateway) and drives
|
|
4
|
+
* the real cursor-agent CLI in ACP mode, asserting the fusion-synthesized
|
|
5
|
+
* sentinel reaches Cursor via session/update. Returns undefined when the
|
|
6
|
+
* Cursorkit checkout or the cursor-agent CLI are unavailable, so the acceptance
|
|
7
|
+
* suite records the explicit `blocked` / `cursorkit_backend_not_running`
|
|
8
|
+
* outcome instead of a silent pass.
|
|
9
|
+
*/
|
|
10
|
+
import type { FrontDoorOutcomeProducer } from "@fusionkit/model-gateway";
|
|
11
|
+
export type CursorAcpProducerInput = {
|
|
12
|
+
cursorKitDir: string | undefined;
|
|
13
|
+
gatewayUrl: string;
|
|
14
|
+
sentinel: string;
|
|
15
|
+
repo: string;
|
|
16
|
+
command?: string;
|
|
17
|
+
modelName?: string;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function buildCursorAcpProducer(input: CursorAcpProducerInput): FrontDoorOutcomeProducer | undefined;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Cursor ACP front-door producer. Spawns the Cursorkit bridge (its
|
|
3
|
+
* local-model backend pointed at the running Fusion Harness Gateway) and drives
|
|
4
|
+
* the real cursor-agent CLI in ACP mode, asserting the fusion-synthesized
|
|
5
|
+
* sentinel reaches Cursor via session/update. Returns undefined when the
|
|
6
|
+
* Cursorkit checkout or the cursor-agent CLI are unavailable, so the acceptance
|
|
7
|
+
* suite records the explicit `blocked` / `cursorkit_backend_not_running`
|
|
8
|
+
* outcome instead of a silent pass.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { delimiter, join } from "node:path";
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
14
|
+
function commandOnPath(command) {
|
|
15
|
+
if (command.includes("/"))
|
|
16
|
+
return existsSync(command);
|
|
17
|
+
const pathValue = process.env.PATH ?? "";
|
|
18
|
+
return pathValue
|
|
19
|
+
.split(delimiter)
|
|
20
|
+
.filter((entry) => entry.length > 0)
|
|
21
|
+
.some((dir) => existsSync(join(dir, command)));
|
|
22
|
+
}
|
|
23
|
+
function normalizeModelBaseUrl(gatewayUrl) {
|
|
24
|
+
const trimmed = gatewayUrl.replace(/\/+$/, "");
|
|
25
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
26
|
+
}
|
|
27
|
+
export function buildCursorAcpProducer(input) {
|
|
28
|
+
const command = input.command ?? "cursor-agent";
|
|
29
|
+
if (input.cursorKitDir === undefined || input.cursorKitDir.length === 0) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (!existsSync(join(input.cursorKitDir, "dist/src/cli.js"))) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (!commandOnPath(command)) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return () => runCursorAcpOutcome({ ...input, command });
|
|
39
|
+
}
|
|
40
|
+
async function runCursorAcpOutcome(input) {
|
|
41
|
+
const cursorKitDir = input.cursorKitDir;
|
|
42
|
+
const modelName = input.modelName ?? "local-fusion";
|
|
43
|
+
const bridgePort = 9700 + Math.floor(Math.random() * 250);
|
|
44
|
+
const bridgeEnv = {};
|
|
45
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
if (key.startsWith("BRIDGE_") || key.startsWith("MODEL_") || key.startsWith("CURSOR_UPSTREAM")) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
bridgeEnv[key] = value;
|
|
52
|
+
}
|
|
53
|
+
Object.assign(bridgeEnv, {
|
|
54
|
+
BRIDGE_PORT: String(bridgePort),
|
|
55
|
+
BRIDGE_ROUTE_INVENTORY: "true",
|
|
56
|
+
CURSOR_UPSTREAM_BASE_URL: "https://api2.cursor.sh",
|
|
57
|
+
MODEL_BASE_URL: normalizeModelBaseUrl(input.gatewayUrl),
|
|
58
|
+
MODEL_API_KEY: "local",
|
|
59
|
+
MODEL_NAME: modelName,
|
|
60
|
+
MODEL_PROVIDER_MODEL: "fusion-panel",
|
|
61
|
+
MODEL_CONTEXT_TOKEN_LIMIT: "128000"
|
|
62
|
+
});
|
|
63
|
+
let bridgeOut = "";
|
|
64
|
+
const bridge = spawn(process.execPath, ["dist/src/cli.js", "serve"], {
|
|
65
|
+
cwd: cursorKitDir,
|
|
66
|
+
env: bridgeEnv,
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
68
|
+
});
|
|
69
|
+
bridge.stdout.on("data", (chunk) => {
|
|
70
|
+
bridgeOut += chunk.toString("utf8");
|
|
71
|
+
});
|
|
72
|
+
bridge.stderr.on("data", (chunk) => {
|
|
73
|
+
bridgeOut += chunk.toString("utf8");
|
|
74
|
+
});
|
|
75
|
+
const evidence = [];
|
|
76
|
+
try {
|
|
77
|
+
const deadline = Date.now() + 20_000;
|
|
78
|
+
while (!/bridge listening/.test(bridgeOut) && Date.now() < deadline) {
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
80
|
+
}
|
|
81
|
+
if (!/bridge listening/.test(bridgeOut)) {
|
|
82
|
+
return {
|
|
83
|
+
id: "cursor-acp",
|
|
84
|
+
status: "failed",
|
|
85
|
+
reason: "cursorkit_bridge_did_not_start",
|
|
86
|
+
evidence
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const acpText = await driveCursorAgentSentinel({
|
|
90
|
+
command: input.command,
|
|
91
|
+
bridgePort,
|
|
92
|
+
modelName,
|
|
93
|
+
cwd: input.repo,
|
|
94
|
+
sentinel: input.sentinel,
|
|
95
|
+
timeoutMs: input.timeoutMs ?? 120_000
|
|
96
|
+
});
|
|
97
|
+
if (acpText.includes(input.sentinel)) {
|
|
98
|
+
evidence.push(input.sentinel);
|
|
99
|
+
return {
|
|
100
|
+
id: "cursor-acp",
|
|
101
|
+
status: "passed",
|
|
102
|
+
request_path: "/agent.v1.AgentService/Run",
|
|
103
|
+
evidence
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
id: "cursor-acp",
|
|
108
|
+
status: "failed",
|
|
109
|
+
reason: "sentinel_not_observed_in_cursor_session_update",
|
|
110
|
+
evidence
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
id: "cursor-acp",
|
|
116
|
+
status: "failed",
|
|
117
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
118
|
+
evidence
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
bridge.kill("SIGTERM");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function driveCursorAgentSentinel(input) {
|
|
126
|
+
const acp = spawn(input.command, [
|
|
127
|
+
"--endpoint",
|
|
128
|
+
`http://127.0.0.1:${input.bridgePort}`,
|
|
129
|
+
"--model",
|
|
130
|
+
input.modelName,
|
|
131
|
+
"--mode",
|
|
132
|
+
"ask",
|
|
133
|
+
"acp"
|
|
134
|
+
], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
135
|
+
let acpText = "";
|
|
136
|
+
let nextId = 1;
|
|
137
|
+
const pending = new Map();
|
|
138
|
+
const rl = createInterface({ input: acp.stdout });
|
|
139
|
+
const send = (method, params) => {
|
|
140
|
+
const id = nextId++;
|
|
141
|
+
acp.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
142
|
+
return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
|
|
143
|
+
};
|
|
144
|
+
rl.on("line", (line) => {
|
|
145
|
+
let message;
|
|
146
|
+
try {
|
|
147
|
+
message = JSON.parse(line);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (message.id !== undefined && message.method === undefined) {
|
|
153
|
+
const waiter = pending.get(Number(message.id));
|
|
154
|
+
if (waiter === undefined)
|
|
155
|
+
return;
|
|
156
|
+
pending.delete(Number(message.id));
|
|
157
|
+
if (message.error !== undefined)
|
|
158
|
+
waiter.reject(message.error);
|
|
159
|
+
else
|
|
160
|
+
waiter.resolve(message.result);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (message.method !== undefined) {
|
|
164
|
+
if (message.method === "session/update")
|
|
165
|
+
acpText += JSON.stringify(message.params);
|
|
166
|
+
if (message.id !== undefined) {
|
|
167
|
+
acp.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { outcome: { outcome: "skipped", reason: "acceptance" } } })}\n`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
const withTimeout = (promise, ms) => Promise.race([
|
|
172
|
+
promise,
|
|
173
|
+
new Promise((_resolve, reject) => setTimeout(() => reject(new Error("ACP step timed out")), ms))
|
|
174
|
+
]);
|
|
175
|
+
try {
|
|
176
|
+
await withTimeout(send("initialize", {
|
|
177
|
+
protocolVersion: 1,
|
|
178
|
+
clientCapabilities: {
|
|
179
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
180
|
+
terminal: false
|
|
181
|
+
},
|
|
182
|
+
clientInfo: { name: "fusionkit-acceptance", version: "0.1.0" }
|
|
183
|
+
}), 60_000);
|
|
184
|
+
await withTimeout(send("authenticate", { methodId: "cursor_login" }), 60_000);
|
|
185
|
+
const session = (await withTimeout(send("session/new", { cwd: input.cwd, mcpServers: [] }), 60_000));
|
|
186
|
+
const sessionId = session.sessionId ?? session.session?.id;
|
|
187
|
+
if (sessionId === undefined)
|
|
188
|
+
return acpText;
|
|
189
|
+
await withTimeout(send("session/prompt", {
|
|
190
|
+
sessionId,
|
|
191
|
+
prompt: [
|
|
192
|
+
{
|
|
193
|
+
type: "text",
|
|
194
|
+
text: `Reply with exactly this token and nothing else: ${input.sentinel}`
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}), input.timeoutMs);
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
199
|
+
return acpText;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
rl.close();
|
|
203
|
+
acp.kill("SIGTERM");
|
|
204
|
+
}
|
|
205
|
+
}
|
package/dist/gateway.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join, resolve } from "node:path";
|
|
|
10
10
|
import { runFusionPanels, runUnifiedHarnessE2E } from "@fusionkit/ensemble";
|
|
11
11
|
import { emitTrace, newSpanId, newTraceId } from "@fusionkit/protocol";
|
|
12
12
|
import { FusionBackend, installAcpAdapters, runAcpAgent, runFrontDoorAcceptance, startFusionGateway, startGateway } from "@fusionkit/model-gateway";
|
|
13
|
+
import { buildCursorAcpProducer } from "./cursor-acp.js";
|
|
13
14
|
// Once an interactive coding agent owns the terminal, the per-turn panel chatter
|
|
14
15
|
// would corrupt its full-screen TUI. The launcher flips this off before handing
|
|
15
16
|
// over; trace events (for --observe) keep flowing regardless.
|
|
@@ -293,10 +294,21 @@ export async function runGatewayAcceptance(input) {
|
|
|
293
294
|
port: 0
|
|
294
295
|
});
|
|
295
296
|
try {
|
|
297
|
+
const cursorAcp = buildCursorAcpProducer({
|
|
298
|
+
cursorKitDir: input.config.cursorKitDir,
|
|
299
|
+
gatewayUrl: gateway.url(),
|
|
300
|
+
sentinel: input.sentinel,
|
|
301
|
+
repo: input.config.repo,
|
|
302
|
+
...(input.config.models[0]?.id !== undefined
|
|
303
|
+
? { modelName: input.config.models[0].id }
|
|
304
|
+
: {}),
|
|
305
|
+
...(input.config.timeoutMs !== undefined ? { timeoutMs: input.config.timeoutMs } : {})
|
|
306
|
+
});
|
|
296
307
|
const report = await runFrontDoorAcceptance({
|
|
297
308
|
gatewayUrl: gateway.url(),
|
|
298
309
|
sentinel: input.sentinel,
|
|
299
|
-
acpRunner: buildAcpRunner(input.config)
|
|
310
|
+
acpRunner: buildAcpRunner(input.config),
|
|
311
|
+
...(cursorAcp !== undefined ? { cursorAcp } : {})
|
|
300
312
|
});
|
|
301
313
|
mkdirSync(resolve(input.outPath, ".."), { recursive: true });
|
|
302
314
|
writeFileSync(input.outPath, JSON.stringify(report, null, 2) + "\n");
|
package/dist/test/cli.test.js
CHANGED
|
@@ -6,16 +6,14 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import {
|
|
9
|
+
import { test } from "node:test";
|
|
10
10
|
import { MODEL_FUSION_SCHEMA_BUNDLE_HASH } from "@fusionkit/protocol";
|
|
11
|
-
import { makeRepo as makeStackRepo, mockRunRequest, startStack, uploadWorkspace } from "@fusionkit/testkit";
|
|
12
11
|
const CLI = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
13
12
|
const SMOKE_ENV_KEYS = [
|
|
14
13
|
"WARRANT_CLAUDE_SMOKE",
|
|
15
14
|
"WARRANT_CODEX_SMOKE",
|
|
16
15
|
"WARRANT_ENSEMBLE_LIVE_SMOKE"
|
|
17
16
|
];
|
|
18
|
-
let home;
|
|
19
17
|
async function readBody(req) {
|
|
20
18
|
const chunks = [];
|
|
21
19
|
for await (const chunk of req)
|
|
@@ -100,7 +98,7 @@ function warrant(args, options = {}) {
|
|
|
100
98
|
else
|
|
101
99
|
env[key] = value;
|
|
102
100
|
}
|
|
103
|
-
const result = spawnSync(process.execPath, [CLI,
|
|
101
|
+
const result = spawnSync(process.execPath, [CLI, ...args], {
|
|
104
102
|
encoding: "utf8",
|
|
105
103
|
env,
|
|
106
104
|
input: options.input
|
|
@@ -122,7 +120,7 @@ async function warrantAsync(args, options = {}) {
|
|
|
122
120
|
env[key] = value;
|
|
123
121
|
}
|
|
124
122
|
return await new Promise((resolve) => {
|
|
125
|
-
const child = spawn(process.execPath, [CLI,
|
|
123
|
+
const child = spawn(process.execPath, [CLI, ...args], {
|
|
126
124
|
env,
|
|
127
125
|
stdio: ["pipe", "pipe", "pipe"]
|
|
128
126
|
});
|
|
@@ -145,18 +143,11 @@ async function warrantAsync(args, options = {}) {
|
|
|
145
143
|
}
|
|
146
144
|
});
|
|
147
145
|
}
|
|
148
|
-
before(() => {
|
|
149
|
-
home = mkdtempSync(join(tmpdir(), "warrant-cli-test-"));
|
|
150
|
-
rmSync(home, { recursive: true, force: true });
|
|
151
|
-
});
|
|
152
|
-
after(() => {
|
|
153
|
-
rmSync(home, { recursive: true, force: true });
|
|
154
|
-
});
|
|
155
146
|
test("help prints usage and lists the top-level commands", () => {
|
|
156
147
|
const result = warrant(["help"]);
|
|
157
148
|
assert.equal(result.status, 0);
|
|
158
149
|
assert.match(result.stdout, /real model fusion behind your coding agent/);
|
|
159
|
-
for (const command of ["
|
|
150
|
+
for (const command of ["init", "ensemble", "local", "fusion", "codex", "claude", "cursor", "serve"]) {
|
|
160
151
|
assert.match(result.stdout, new RegExp(`\\b${command}\\b`));
|
|
161
152
|
}
|
|
162
153
|
});
|
|
@@ -203,78 +194,29 @@ test("fusion help documents the flags-before-tool contract", () => {
|
|
|
203
194
|
assert.equal(result.status, 0);
|
|
204
195
|
assert.match(result.stdout, /must precede the tool name/);
|
|
205
196
|
});
|
|
206
|
-
test("init
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
assert.match(again.stderr, /already initialized/);
|
|
225
|
-
});
|
|
226
|
-
test("secrets are stored encrypted and listed by name only", () => {
|
|
227
|
-
const set = warrant(["secrets", "set", "NPM_TOKEN", "super-secret-value"]);
|
|
228
|
-
assert.equal(set.status, 0, set.stderr);
|
|
229
|
-
assert.match(set.stdout, /encrypted at rest/);
|
|
230
|
-
const list = warrant(["secrets", "list"]);
|
|
231
|
-
assert.equal(list.status, 0);
|
|
232
|
-
assert.equal(list.stdout.trim(), "NPM_TOKEN");
|
|
233
|
-
const stored = readFileSync(join(home, "secrets.enc"), "utf8");
|
|
234
|
-
assert.ok(!stored.includes("super-secret-value"), "value must be encrypted");
|
|
235
|
-
});
|
|
236
|
-
test("ui prints the control panel address and login token", () => {
|
|
237
|
-
const result = warrant(["ui"]);
|
|
238
|
-
assert.equal(result.status, 0);
|
|
239
|
-
assert.match(result.stdout, /control panel: http:\/\/127\.0\.0\.1:7172\/ui\//);
|
|
240
|
-
assert.match(result.stdout, /login token: {3}\S+/);
|
|
197
|
+
test("init scaffolds a fusionkit.json and refuses to clobber without --force", () => {
|
|
198
|
+
const fixture = makeRepo();
|
|
199
|
+
try {
|
|
200
|
+
const result = warrant(["init", "--repo", fixture.repo]);
|
|
201
|
+
assert.equal(result.status, 0, result.stderr);
|
|
202
|
+
const configPath = join(fixture.repo, "fusionkit.json");
|
|
203
|
+
assert.ok(existsSync(configPath));
|
|
204
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
205
|
+
assert.equal(config.version, "fusionkit.fusion.v1");
|
|
206
|
+
const again = warrant(["init", "--repo", fixture.repo]);
|
|
207
|
+
assert.equal(again.status, 1);
|
|
208
|
+
assert.match(again.stderr, /already exists/);
|
|
209
|
+
const forced = warrant(["init", "--repo", fixture.repo, "--force"]);
|
|
210
|
+
assert.equal(forced.status, 0, forced.stderr);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
fixture.cleanup();
|
|
214
|
+
}
|
|
241
215
|
});
|
|
242
|
-
test("unknown commands
|
|
216
|
+
test("unknown commands fail with guidance", () => {
|
|
243
217
|
const unknown = warrant(["frobnicate"]);
|
|
244
218
|
assert.equal(unknown.status, 1);
|
|
245
219
|
assert.match(unknown.stderr, /unknown command/);
|
|
246
|
-
const missingAgent = warrant(["run", "do things"]);
|
|
247
|
-
assert.equal(missingAgent.status, 1);
|
|
248
|
-
assert.match(missingAgent.stderr, /--agent is required/);
|
|
249
|
-
const missingTask = warrant(["continue", "--agent", "mock"]);
|
|
250
|
-
assert.equal(missingTask.status, 1);
|
|
251
|
-
assert.match(missingTask.stderr, /task prompt is required/);
|
|
252
|
-
const badAgent = warrant(["continue", "--agent", "nonsense", "task"]);
|
|
253
|
-
assert.equal(badAgent.status, 1);
|
|
254
|
-
assert.match(badAgent.stderr, /unknown agent kind/);
|
|
255
|
-
});
|
|
256
|
-
test("verify fails closed on a tampered bundle file", () => {
|
|
257
|
-
const path = join(home, "garbage.bundle.json");
|
|
258
|
-
const fake = {
|
|
259
|
-
version: "warrant.bundle.v1",
|
|
260
|
-
contract: { signatures: [], workspace: { baseRef: "x" } },
|
|
261
|
-
receipt: {
|
|
262
|
-
contractHash: "0".repeat(64),
|
|
263
|
-
signatures: [],
|
|
264
|
-
status: "completed",
|
|
265
|
-
workspaceIn: { baseRef: "y", manifestHash: "z" },
|
|
266
|
-
workspaceOut: { diffHash: "", artifactHashes: [] },
|
|
267
|
-
secretsReleased: [],
|
|
268
|
-
eventsHead: "",
|
|
269
|
-
eventCount: 0
|
|
270
|
-
},
|
|
271
|
-
events: [],
|
|
272
|
-
keys: { planePublicKeyPem: "", runnerPublicKeyPem: "" }
|
|
273
|
-
};
|
|
274
|
-
writeFileSync(path, JSON.stringify(fake));
|
|
275
|
-
const result = warrant(["verify", path]);
|
|
276
|
-
assert.equal(result.status, 1);
|
|
277
|
-
assert.match(result.stderr, /VERIFICATION FAILED/);
|
|
278
220
|
});
|
|
279
221
|
function makeRepo() {
|
|
280
222
|
const root = mkdtempSync(join(tmpdir(), "warrant-ensemble-cli-"));
|
|
@@ -630,7 +572,7 @@ test("ensemble dashboard writes markdown and run-result records", () => {
|
|
|
630
572
|
assert.match(result.stdout, /records: 6/);
|
|
631
573
|
assert.ok(existsSync(join(fixture.output, "dashboard.md")));
|
|
632
574
|
assert.ok(existsSync(join(fixture.output, "harness-run-results", "mock-success.json")));
|
|
633
|
-
assert.ok(existsSync(join(fixture.output, "harness-run-results", "cursor-
|
|
575
|
+
assert.ok(existsSync(join(fixture.output, "harness-run-results", "cursor-skipped.json")));
|
|
634
576
|
const dashboard = readFileSync(join(fixture.output, "dashboard.md"), "utf8");
|
|
635
577
|
assert.match(dashboard, /Capability Matrix/);
|
|
636
578
|
assert.match(dashboard, /command-failure/);
|
|
@@ -808,60 +750,3 @@ test("ensemble gateway test runs the unified front-door acceptance suite", async
|
|
|
808
750
|
fixture.cleanup();
|
|
809
751
|
}
|
|
810
752
|
});
|
|
811
|
-
test("lifecycle commands read a real run from a live plane", async () => {
|
|
812
|
-
const stack = await startStack({
|
|
813
|
-
policy: (policy) => {
|
|
814
|
-
policy.agents.allow = ["mock"];
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
const repo = makeStackRepo({ files: { "README.md": "# cli lifecycle\n" } });
|
|
818
|
-
const liveHome = mkdtempSync(join(tmpdir(), "warrant-cli-live-"));
|
|
819
|
-
rmSync(liveHome, { recursive: true, force: true });
|
|
820
|
-
try {
|
|
821
|
-
// The plane runs in this test process, so every CLI call must use the async
|
|
822
|
-
// spawner: a synchronous spawn would block the event loop and deadlock the
|
|
823
|
-
// in-process plane.
|
|
824
|
-
const init = await warrantAsync(["init"], { dir: liveHome });
|
|
825
|
-
assert.equal(init.status, 0, init.stderr);
|
|
826
|
-
// Point the freshly initialized home at the in-process test stack.
|
|
827
|
-
const configPath = join(liveHome, "config.json");
|
|
828
|
-
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
829
|
-
config.planeUrl = stack.planeUrl;
|
|
830
|
-
config.adminToken = stack.adminToken;
|
|
831
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
832
|
-
// Create one completed run through the SDK so the CLI has something to read.
|
|
833
|
-
const captured = await uploadWorkspace(stack.client, repo);
|
|
834
|
-
const created = await stack.client.requestRun(mockRunRequest({ prompt: "lifecycle probe", pool: stack.pool, workspace: captured.manifest }));
|
|
835
|
-
if (created.status === "awaiting_approval") {
|
|
836
|
-
await stack.client.approve(created.runId, { kind: "human", id: "cli-tester" });
|
|
837
|
-
}
|
|
838
|
-
assert.ok(await stack.runOnce());
|
|
839
|
-
const runs = await warrantAsync(["runs"], { dir: liveHome });
|
|
840
|
-
assert.equal(runs.status, 0, runs.stderr);
|
|
841
|
-
assert.match(runs.stdout, new RegExp(created.runId));
|
|
842
|
-
const receipt = await warrantAsync(["receipt", created.runId], { dir: liveHome });
|
|
843
|
-
assert.equal(receipt.status, 0, receipt.stderr);
|
|
844
|
-
const bundlePath = join(liveHome, "out.bundle.json");
|
|
845
|
-
const bundle = await warrantAsync(["bundle", created.runId, "--out", bundlePath], {
|
|
846
|
-
dir: liveHome
|
|
847
|
-
});
|
|
848
|
-
assert.equal(bundle.status, 0, bundle.stderr);
|
|
849
|
-
assert.match(bundle.stdout, /bundle written to/);
|
|
850
|
-
assert.ok(existsSync(bundlePath));
|
|
851
|
-
// The CLI round-trips its own bundle through offline verification.
|
|
852
|
-
const verify = await warrantAsync(["verify", bundlePath], { dir: liveHome });
|
|
853
|
-
assert.equal(verify.status, 0, verify.stderr);
|
|
854
|
-
assert.match(verify.stdout, /VERIFIED/);
|
|
855
|
-
const exported = await warrantAsync(["export"], { dir: liveHome });
|
|
856
|
-
assert.equal(exported.status, 0, exported.stderr);
|
|
857
|
-
assert.match(exported.stdout, new RegExp(created.runId));
|
|
858
|
-
const pull = await warrantAsync(["pull", created.runId, "--repo", repo], { dir: liveHome });
|
|
859
|
-
assert.equal(pull.status, 0, pull.stderr);
|
|
860
|
-
assert.match(pull.stdout, /applied|nothing to pull|branch/);
|
|
861
|
-
}
|
|
862
|
-
finally {
|
|
863
|
-
await stack.stop();
|
|
864
|
-
rmSync(repo, { recursive: true, force: true });
|
|
865
|
-
rmSync(liveHome, { recursive: true, force: true });
|
|
866
|
-
}
|
|
867
|
-
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusionkit/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/handoffkit.git",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"commander": "14.0.3",
|
|
37
|
-
"@fusionkit/ensemble": "0.1.
|
|
38
|
-
"@fusionkit/
|
|
39
|
-
"@fusionkit/
|
|
40
|
-
"@fusionkit/
|
|
41
|
-
"@fusionkit/
|
|
42
|
-
"@fusionkit/
|
|
43
|
-
"@fusionkit/
|
|
44
|
-
"@fusionkit/
|
|
37
|
+
"@fusionkit/ensemble": "0.1.5",
|
|
38
|
+
"@fusionkit/model-gateway": "0.1.5",
|
|
39
|
+
"@fusionkit/handoff": "0.1.5",
|
|
40
|
+
"@fusionkit/protocol": "0.1.5",
|
|
41
|
+
"@fusionkit/runner": "0.1.5",
|
|
42
|
+
"@fusionkit/plane": "0.1.5",
|
|
43
|
+
"@fusionkit/sdk": "0.1.5",
|
|
44
|
+
"@fusionkit/workspace": "0.1.5"
|
|
45
45
|
},
|
|
46
46
|
"optionalDependencies": {
|
|
47
47
|
"portless": "0.14.0"
|
package/scope/.next/BUILD_ID
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
vxqImMqlOwssVTua5Facf
|
|
@@ -52,36 +52,36 @@
|
|
|
52
52
|
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
53
53
|
"static/chunks/app/api/sessions/route-95e822afbebe548b.js"
|
|
54
54
|
],
|
|
55
|
-
"/api/
|
|
55
|
+
"/api/stream/route": [
|
|
56
56
|
"static/chunks/webpack-4501ec292abda191.js",
|
|
57
57
|
"static/chunks/4bd1b696-409494caf8c83275.js",
|
|
58
58
|
"static/chunks/255-69a4a78fac9becef.js",
|
|
59
59
|
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
60
|
-
"static/chunks/app/api/
|
|
60
|
+
"static/chunks/app/api/stream/route-95e822afbebe548b.js"
|
|
61
61
|
],
|
|
62
|
-
"/api/
|
|
62
|
+
"/api/sessions/[traceId]/route": [
|
|
63
63
|
"static/chunks/webpack-4501ec292abda191.js",
|
|
64
64
|
"static/chunks/4bd1b696-409494caf8c83275.js",
|
|
65
65
|
"static/chunks/255-69a4a78fac9becef.js",
|
|
66
66
|
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
67
|
-
"static/chunks/app/api/
|
|
67
|
+
"static/chunks/app/api/sessions/[traceId]/route-95e822afbebe548b.js"
|
|
68
68
|
],
|
|
69
|
-
"/
|
|
69
|
+
"/models/page": [
|
|
70
70
|
"static/chunks/webpack-4501ec292abda191.js",
|
|
71
71
|
"static/chunks/4bd1b696-409494caf8c83275.js",
|
|
72
72
|
"static/chunks/255-69a4a78fac9becef.js",
|
|
73
73
|
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
74
74
|
"static/chunks/239-1c69ce437d02745f.js",
|
|
75
|
-
"static/chunks/
|
|
75
|
+
"static/chunks/873-9351d1edaa9d58ef.js",
|
|
76
|
+
"static/chunks/app/models/page-d9b7d19485e9a640.js"
|
|
76
77
|
],
|
|
77
|
-
"/
|
|
78
|
+
"/environments/page": [
|
|
78
79
|
"static/chunks/webpack-4501ec292abda191.js",
|
|
79
80
|
"static/chunks/4bd1b696-409494caf8c83275.js",
|
|
80
81
|
"static/chunks/255-69a4a78fac9becef.js",
|
|
81
82
|
"static/chunks/main-app-2a6b1f94de31f96f.js",
|
|
82
83
|
"static/chunks/239-1c69ce437d02745f.js",
|
|
83
|
-
"static/chunks/
|
|
84
|
-
"static/chunks/app/models/page-d9b7d19485e9a640.js"
|
|
84
|
+
"static/chunks/app/environments/page-75403c3640fdf9de.js"
|
|
85
85
|
],
|
|
86
86
|
"/page": [
|
|
87
87
|
"static/chunks/webpack-4501ec292abda191.js",
|