@struktoai/mirage-cli 0.0.7-alpha.2 → 0.0.7-alpha.3
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/LICENSE +201 -0
- package/README.md +28 -29
- package/dist/bin/mirage.js +877 -205
- package/dist/bin/mirage.js.map +1 -1
- package/dist/chunk-IJUWWUKK.js +603 -0
- package/dist/chunk-IJUWWUKK.js.map +1 -0
- package/dist/chunk-VOPUKMVY.js +77 -0
- package/dist/chunk-VOPUKMVY.js.map +1 -0
- package/dist/index.d.ts +53 -2
- package/dist/index.js +6 -4
- package/dist/stream-LJTY7SCX.js +142 -0
- package/dist/stream-LJTY7SCX.js.map +1 -0
- package/package.json +9 -8
- package/dist/chunk-BNUV7A4V.js +0 -428
- package/dist/chunk-BNUV7A4V.js.map +0 -1
package/dist/bin/mirage.js
CHANGED
|
@@ -1,35 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
formatAge,
|
|
7
|
-
formatTable,
|
|
3
|
+
DaemonUnreachable,
|
|
4
|
+
LoginError,
|
|
5
|
+
endsAt,
|
|
8
6
|
getConfig,
|
|
9
|
-
|
|
7
|
+
isLocalUrl,
|
|
8
|
+
jsonBody,
|
|
10
9
|
listConfig,
|
|
11
10
|
loadDaemonSettings,
|
|
12
11
|
makeClient,
|
|
12
|
+
readLogin,
|
|
13
|
+
removeLogin,
|
|
14
|
+
renewBy,
|
|
13
15
|
resolvedConfig,
|
|
14
16
|
setConfig,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
tokenClaims,
|
|
18
|
+
unsetConfig,
|
|
19
|
+
writeLogin
|
|
20
|
+
} from "../chunk-IJUWWUKK.js";
|
|
21
|
+
import {
|
|
22
|
+
emit,
|
|
23
|
+
exitCodeFromResponse,
|
|
24
|
+
fail,
|
|
25
|
+
formatAge,
|
|
26
|
+
formatTable,
|
|
27
|
+
handleResponse
|
|
28
|
+
} from "../chunk-VOPUKMVY.js";
|
|
17
29
|
|
|
18
30
|
// src/bin/mirage.ts
|
|
19
|
-
import { DaemonConfigError as DaemonConfigError2 } from "@struktoai/mirage-server";
|
|
31
|
+
import { DaemonConfigError as DaemonConfigError2 } from "@struktoai/mirage-server/daemon_config";
|
|
20
32
|
|
|
21
33
|
// src/main.ts
|
|
22
|
-
import { readFileSync as
|
|
34
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
23
35
|
import { dirname as dirname2, join } from "path";
|
|
24
36
|
import { fileURLToPath } from "url";
|
|
25
37
|
import { Command } from "commander";
|
|
26
38
|
|
|
27
39
|
// src/config.ts
|
|
28
40
|
import { compareCodePoints } from "@struktoai/mirage-core/utils/sort";
|
|
29
|
-
import { ALLOWED_KEYS, DaemonConfigError } from "@struktoai/mirage-server";
|
|
30
|
-
function mask(key,
|
|
31
|
-
if (key === "auth_token" &&
|
|
32
|
-
return
|
|
41
|
+
import { ALLOWED_KEYS, DaemonConfigError } from "@struktoai/mirage-server/daemon_config";
|
|
42
|
+
function mask(key, value2) {
|
|
43
|
+
if (key === "auth_token" && value2 !== "") return "***";
|
|
44
|
+
return value2;
|
|
33
45
|
}
|
|
34
46
|
function humanTable(table) {
|
|
35
47
|
return Object.entries(table).map(([k, v]) => `${k} = ${v}`).join("\n");
|
|
@@ -46,8 +58,8 @@ function listResolved() {
|
|
|
46
58
|
return;
|
|
47
59
|
}
|
|
48
60
|
const payload = {};
|
|
49
|
-
for (const [key, [
|
|
50
|
-
payload[key] = { value: mask(key,
|
|
61
|
+
for (const [key, [value2, origin]] of Object.entries(resolved)) {
|
|
62
|
+
payload[key] = { value: mask(key, value2), origin };
|
|
51
63
|
}
|
|
52
64
|
emit(payload, humanResolved);
|
|
53
65
|
}
|
|
@@ -76,27 +88,27 @@ function registerConfigCommands(program) {
|
|
|
76
88
|
else listFile();
|
|
77
89
|
});
|
|
78
90
|
config.command("get").argument("<key>").description("Print one [daemon] key.").action((key) => {
|
|
79
|
-
let
|
|
91
|
+
let value2;
|
|
80
92
|
try {
|
|
81
|
-
|
|
93
|
+
value2 = getConfig(key);
|
|
82
94
|
} catch (e) {
|
|
83
95
|
fail(e.message, 2);
|
|
84
96
|
return;
|
|
85
97
|
}
|
|
86
|
-
if (
|
|
98
|
+
if (value2 === void 0) {
|
|
87
99
|
fail(`${key} is not set`, 1);
|
|
88
100
|
return;
|
|
89
101
|
}
|
|
90
|
-
emit({ [key]:
|
|
102
|
+
emit({ [key]: value2 }, () => value2);
|
|
91
103
|
});
|
|
92
|
-
config.command("set").argument("<key>").argument("<value>").description("Write a [daemon] key (path settings apply on next daemon restart).").action((key,
|
|
104
|
+
config.command("set").argument("<key>").argument("<value>").description("Write a [daemon] key (path settings apply on next daemon restart).").action((key, value2) => {
|
|
93
105
|
try {
|
|
94
|
-
setConfig(key,
|
|
106
|
+
setConfig(key, value2);
|
|
95
107
|
} catch (e) {
|
|
96
108
|
fail(e.message, 2);
|
|
97
109
|
return;
|
|
98
110
|
}
|
|
99
|
-
emit({ [key]:
|
|
111
|
+
emit({ [key]: value2, written: true }, () => `${key} = ${value2}`);
|
|
100
112
|
});
|
|
101
113
|
config.command("unset").argument("<key>").description("Remove a [daemon] key.").action((key) => {
|
|
102
114
|
try {
|
|
@@ -111,12 +123,12 @@ function registerConfigCommands(program) {
|
|
|
111
123
|
|
|
112
124
|
// src/daemon.ts
|
|
113
125
|
import { existsSync, readFileSync } from "fs";
|
|
114
|
-
import { pidFilePath } from "@struktoai/mirage-server";
|
|
126
|
+
import { pidFilePath } from "@struktoai/mirage-server/paths";
|
|
115
127
|
function formatStatus(d) {
|
|
116
128
|
if (!d.running) return `Daemon not running. URL: ${d.url}`;
|
|
117
129
|
const parts = [`Running. PID ${d.pid !== null ? String(d.pid) : "?"}`];
|
|
118
|
-
if (d.
|
|
119
|
-
parts.push(`uptime ${formatAge(Date.now() / 1e3 - d.
|
|
130
|
+
if (d.uptime_s !== void 0) {
|
|
131
|
+
parts.push(`uptime ${formatAge(Date.now() / 1e3 - d.uptime_s)}`);
|
|
120
132
|
}
|
|
121
133
|
if (d.workspaces !== void 0) {
|
|
122
134
|
parts.push(`${String(d.workspaces)} workspace${d.workspaces === 1 ? "" : "s"}`);
|
|
@@ -128,7 +140,7 @@ function formatStop(d) {
|
|
|
128
140
|
return `Stopped (via ${via}${d.pid !== void 0 ? `, PID ${String(d.pid)}` : ""}).`;
|
|
129
141
|
}
|
|
130
142
|
function formatRestart(d) {
|
|
131
|
-
if (d.
|
|
143
|
+
if (d.spawned_fresh === true) return "Restarted (eager spawn).";
|
|
132
144
|
return "Restarted; next CLI command will auto-spawn.";
|
|
133
145
|
}
|
|
134
146
|
function formatKill(d) {
|
|
@@ -139,8 +151,8 @@ function formatKill(d) {
|
|
|
139
151
|
function readPid() {
|
|
140
152
|
const p = pidFilePath();
|
|
141
153
|
if (!existsSync(p)) return null;
|
|
142
|
-
const
|
|
143
|
-
const pid = Number(
|
|
154
|
+
const text2 = readFileSync(p, "utf-8").trim();
|
|
155
|
+
const pid = Number(text2);
|
|
144
156
|
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
145
157
|
}
|
|
146
158
|
function processAlive(pid) {
|
|
@@ -227,13 +239,13 @@ function registerDaemonCommands(program) {
|
|
|
227
239
|
}
|
|
228
240
|
if (opts.eager === true) {
|
|
229
241
|
await buildClient().ensureRunning({ allowSpawn: true });
|
|
230
|
-
emit({ restarted: true,
|
|
242
|
+
emit({ restarted: true, spawned_fresh: true }, formatRestart);
|
|
231
243
|
return;
|
|
232
244
|
}
|
|
233
245
|
emit(
|
|
234
246
|
{
|
|
235
247
|
restarted: true,
|
|
236
|
-
|
|
248
|
+
spawned_fresh: false,
|
|
237
249
|
note: "next workspace create will auto-spawn"
|
|
238
250
|
},
|
|
239
251
|
formatRestart
|
|
@@ -259,30 +271,242 @@ function registerDaemonCommands(program) {
|
|
|
259
271
|
});
|
|
260
272
|
}
|
|
261
273
|
|
|
262
|
-
// src/
|
|
263
|
-
import {
|
|
264
|
-
|
|
265
|
-
|
|
274
|
+
// src/vfs.ts
|
|
275
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
276
|
+
import { buffer } from "stream/consumers";
|
|
277
|
+
import {
|
|
278
|
+
BYTES,
|
|
279
|
+
FLAG,
|
|
280
|
+
INTEGER,
|
|
281
|
+
OWNER,
|
|
282
|
+
SIZE,
|
|
283
|
+
VFS_CALLS
|
|
284
|
+
} from "@struktoai/mirage-server/vfs_calls";
|
|
285
|
+
async function post(target, route, body) {
|
|
286
|
+
const c = makeClient(loadDaemonSettings());
|
|
287
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
288
|
+
const query = new URLSearchParams();
|
|
289
|
+
if (target.session !== void 0) query.set("session_id", target.session);
|
|
290
|
+
if (target.explain === true) query.set("explain", "true");
|
|
291
|
+
const suffix = query.size === 0 ? "" : `?${query.toString()}`;
|
|
292
|
+
const path = `/v1/workspaces/${encodeURIComponent(target.workspace)}/${route}${suffix}`;
|
|
293
|
+
return c.request("POST", path, { body: JSON.stringify(body) });
|
|
294
|
+
}
|
|
295
|
+
async function answer(r) {
|
|
296
|
+
if (r.status >= 400) {
|
|
297
|
+
const text2 = await r.clone().text();
|
|
298
|
+
let body = {};
|
|
299
|
+
try {
|
|
300
|
+
body = JSON.parse(text2);
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
if (body.errno !== void 0) {
|
|
304
|
+
process.stderr.write(`${String(body.detail)} (${body.errno})
|
|
305
|
+
`);
|
|
306
|
+
if (body.refusal) process.stderr.write(`policy denied: ${body.refusal.reason}
|
|
307
|
+
`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return handleResponse(r);
|
|
312
|
+
}
|
|
313
|
+
function explained(said) {
|
|
314
|
+
let verdict = String(said.outcome);
|
|
315
|
+
if (said.reason !== "") verdict += `: ${String(said.reason)}`;
|
|
316
|
+
let line = `${String(said.call)} ${said.paths.join(" ")} [${verdict}]`;
|
|
317
|
+
if (said.error !== "") line += ` ${String(said.error)}`;
|
|
318
|
+
return line;
|
|
319
|
+
}
|
|
320
|
+
var text = (key) => (result) => Buffer2.from(String(result[key]), "base64").toString("utf-8");
|
|
321
|
+
var HUMAN = {
|
|
322
|
+
read: text("data_base64"),
|
|
323
|
+
getxattr: text("value_base64"),
|
|
324
|
+
readdir: (result) => result.entries.join("\n"),
|
|
325
|
+
list_files: (result) => result.files.join("\n"),
|
|
326
|
+
listxattr: (result) => result.names.join("\n"),
|
|
327
|
+
readlink: (result) => String(result.target)
|
|
328
|
+
};
|
|
329
|
+
var flagName = (name) => name.replace(/_base64$/, "");
|
|
330
|
+
async function value(call, name, given) {
|
|
331
|
+
if (call.params[name] === BYTES) {
|
|
332
|
+
const data = typeof given === "string" ? Buffer2.from(given) : await buffer(process.stdin);
|
|
333
|
+
return data.toString("base64");
|
|
334
|
+
}
|
|
335
|
+
if (call.params[name] === OWNER && typeof given === "string" && /^\d+$/.test(given))
|
|
336
|
+
return Number(given);
|
|
337
|
+
return given;
|
|
338
|
+
}
|
|
339
|
+
function camel(name) {
|
|
340
|
+
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
341
|
+
}
|
|
342
|
+
function command(vfs, call) {
|
|
343
|
+
const cmd = vfs.command(call.name.replace(/_/g, "-")).description(call.description);
|
|
344
|
+
const positional = Object.keys(call.params).filter(
|
|
345
|
+
(name) => call.required.includes(name) && call.params[name] !== BYTES
|
|
346
|
+
);
|
|
347
|
+
for (const name of positional) cmd.argument(`<${name}>`);
|
|
348
|
+
for (const name of Object.keys(call.params)) {
|
|
349
|
+
if (positional.includes(name)) continue;
|
|
350
|
+
if (call.params[name] === BYTES) {
|
|
351
|
+
const flag = flagName(name);
|
|
352
|
+
cmd.option(`--${flag} <text>`, `The ${flag} as text; stdin when absent`);
|
|
353
|
+
} else if (call.params[name] === FLAG) {
|
|
354
|
+
cmd.option(`--${name}`);
|
|
355
|
+
} else if (call.params[name] === INTEGER || call.params[name] === SIZE) {
|
|
356
|
+
cmd.option(`--${name} <n>`, `The ${name}`, Number);
|
|
357
|
+
} else {
|
|
358
|
+
cmd.option(`--${name} <value>`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
cmd.requiredOption("-w, --workspace <id>", "Workspace id").option("-s, --session <id>", "Session id").option("--explain", "Say what the call would do; run nothing").action(async (...given) => {
|
|
362
|
+
const opts = given[positional.length];
|
|
363
|
+
const body = {};
|
|
364
|
+
for (const [i, name] of positional.entries()) {
|
|
365
|
+
body[name] = call.params[name] === INTEGER ? Number(given[i]) : given[i];
|
|
366
|
+
}
|
|
367
|
+
for (const name of Object.keys(call.params)) {
|
|
368
|
+
if (positional.includes(name)) continue;
|
|
369
|
+
const raw = opts[camel(call.params[name] === BYTES ? flagName(name) : name)];
|
|
370
|
+
if (call.params[name] === BYTES || raw !== void 0)
|
|
371
|
+
body[name] = await value(call, name, raw);
|
|
372
|
+
}
|
|
373
|
+
const result = await answer(await post(opts, `vfs/${call.name}`, body));
|
|
374
|
+
emit(result, opts.explain === true ? explained : HUMAN[call.name]);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
function registerVfsCommands(program) {
|
|
378
|
+
const vfs = program.command("vfs").description("The session's VFS calls, one command per call, as POSIX names them.");
|
|
379
|
+
for (const call of VFS_CALLS) command(vfs, call);
|
|
380
|
+
program.command("glob").description("Every path a pattern matches, as the session sees them.").argument("<pattern>", "Pathname pattern such as /**/*.py").requiredOption("-w, --workspace <id>", "Workspace id").option("-s, --session <id>", "Session id").action(async (pattern, opts) => {
|
|
381
|
+
const result = await answer(await post(opts, "glob", { pattern }));
|
|
382
|
+
emit(result, (r) => r.paths.join("\n"));
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/shell.ts
|
|
387
|
+
function formatExplanation(data) {
|
|
388
|
+
let verdict = `${data.outcome}, exit ${String(data.exit_code)}`;
|
|
389
|
+
if (data.reason !== "") verdict += `: ${data.reason}`;
|
|
390
|
+
const out = [`${data.line} [${verdict}]`];
|
|
391
|
+
for (const child of data.node.children) explainedLines(child, 1, out);
|
|
392
|
+
return out.join("\n");
|
|
393
|
+
}
|
|
394
|
+
function explainedLines(node, depth, out) {
|
|
395
|
+
const pad = " ".repeat(depth);
|
|
396
|
+
if (node.outcome === void 0) {
|
|
397
|
+
out.push(`${pad}${node.type}: ${node.text}`);
|
|
398
|
+
} else {
|
|
399
|
+
let line = `${pad}${node.text} [${node.outcome}`;
|
|
400
|
+
if (node.exit_code !== void 0 && node.exit_code !== 0)
|
|
401
|
+
line += `, exit ${String(node.exit_code)}`;
|
|
402
|
+
line += node.reason !== void 0 && node.reason !== "" ? `: ${node.reason}]` : "]";
|
|
403
|
+
if (node.source !== void 0 && node.source !== "") line += ` ${node.source}`;
|
|
404
|
+
if (node.runtime !== void 0 && node.runtime !== "") line += ` on ${node.runtime}`;
|
|
405
|
+
out.push(line);
|
|
406
|
+
}
|
|
407
|
+
for (const child of node.children) explainedLines(child, depth + 1, out);
|
|
408
|
+
}
|
|
409
|
+
function registerShellCommand(program) {
|
|
410
|
+
program.command("shell").description("Run a shell line in a workspace.").requiredOption("-w, --workspace <id>", "Workspace id").requiredOption("-c, --command <command>", "Shell line to run").option("-s, --session <id>", "Session id").option("--cwd <path>", "Working directory for this line (a workspace path)").option("--runtime <name>", "Workspace runtime entry to place this line's captured stages on").option("--bg", "Background; return job_id immediately").option("--json", "Collect output and print the final result as JSON").option("--explain", "Say what the line would do, as a tree; run nothing").action(
|
|
266
411
|
async (opts) => {
|
|
267
|
-
const body = { command: opts.command
|
|
268
|
-
if (opts.session !== void 0) body.sessionId = opts.session;
|
|
412
|
+
const body = { command: opts.command };
|
|
269
413
|
if (opts.cwd !== void 0) body.cwd = opts.cwd;
|
|
270
414
|
if (opts.runtime !== void 0) body.runtime = opts.runtime;
|
|
271
|
-
if (opts.
|
|
272
|
-
|
|
415
|
+
if (opts.explain === true) {
|
|
416
|
+
const said = await answer(await post(opts, "shell", body));
|
|
417
|
+
emit(said, opts.json === true ? void 0 : formatExplanation);
|
|
418
|
+
return;
|
|
273
419
|
}
|
|
274
|
-
const
|
|
420
|
+
const session = opts.session === void 0 ? "" : `session_id=${encodeURIComponent(opts.session)}`;
|
|
421
|
+
const path = `/v1/workspaces/${encodeURIComponent(opts.workspace)}/shell`;
|
|
275
422
|
const c = makeClient(loadDaemonSettings());
|
|
276
423
|
await c.ensureRunning({ allowSpawn: false });
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
424
|
+
const foreground = session === "" ? path : `${path}?${session}`;
|
|
425
|
+
const background = session === "" ? `${path}?background=true` : `${path}?${session}&background=true`;
|
|
426
|
+
const piped = !process.stdin.isTTY;
|
|
427
|
+
if (opts.bg !== true) {
|
|
428
|
+
const { streamShell } = await import("../stream-LJTY7SCX.js");
|
|
429
|
+
try {
|
|
430
|
+
process.exitCode = await streamShell(
|
|
431
|
+
c,
|
|
432
|
+
`${foreground}${session === "" ? "?" : "&"}stream=true`,
|
|
433
|
+
body,
|
|
434
|
+
piped,
|
|
435
|
+
opts.json === true
|
|
436
|
+
);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
fail(error instanceof Error ? error.message : String(error), 2);
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const response = piped ? await c.requestUpload("POST", background, body, { name: "stdin", data: process.stdin }) : await c.request("POST", background, { body: JSON.stringify(body) });
|
|
443
|
+
emit(await handleResponse(response));
|
|
282
444
|
}
|
|
283
445
|
);
|
|
284
446
|
}
|
|
285
447
|
|
|
448
|
+
// src/tools.ts
|
|
449
|
+
import { buffer as buffer2 } from "stream/consumers";
|
|
450
|
+
async function callTool(target, name, args) {
|
|
451
|
+
const c = makeClient(loadDaemonSettings());
|
|
452
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
453
|
+
const query = target.session === void 0 ? "" : `?session_id=${encodeURIComponent(target.session)}`;
|
|
454
|
+
const path = `/v1/workspaces/${encodeURIComponent(target.workspace)}/tools/${name}${query}`;
|
|
455
|
+
const response = await handleResponse(
|
|
456
|
+
await c.request("POST", path, { body: JSON.stringify(args) })
|
|
457
|
+
);
|
|
458
|
+
emit(response, (r) => r.text);
|
|
459
|
+
if (response.is_error) process.exitCode = 1;
|
|
460
|
+
}
|
|
461
|
+
function toolCommand(program, name, description) {
|
|
462
|
+
return program.command(name).description(description).requiredOption("-w, --workspace <id>", "Workspace id").option("-s, --session <id>", "Session id");
|
|
463
|
+
}
|
|
464
|
+
function registerToolCommands(parent) {
|
|
465
|
+
const program = parent.command("tools").description("The session's agent tools, as MCP serves them.");
|
|
466
|
+
toolCommand(program, "shell", "Run a command line and read its output as the agent does.").argument("<command>", "The command line to run").action(async (command2, opts) => {
|
|
467
|
+
await callTool(opts, "shell", { command: command2 });
|
|
468
|
+
});
|
|
469
|
+
toolCommand(program, "read", "Read a file with line numbers.").argument("<path>", "File to read").option("--offset <n>", "Line to start at (0-based)", Number).option("--limit <n>", "Most lines to return", Number).action(async (path, opts) => {
|
|
470
|
+
const args = { path };
|
|
471
|
+
if (opts.offset !== void 0) args.offset = opts.offset;
|
|
472
|
+
if (opts.limit !== void 0) args.limit = opts.limit;
|
|
473
|
+
await callTool(opts, "read", args);
|
|
474
|
+
});
|
|
475
|
+
toolCommand(program, "write", "Write a file; an existing one must be read in full first.").argument("<path>", "File to write").option("--content <text>", "Text to write; stdin when absent").action(async (path, opts) => {
|
|
476
|
+
const content = opts.content ?? (await buffer2(process.stdin)).toString("utf-8");
|
|
477
|
+
await callTool(opts, "write", { path, content });
|
|
478
|
+
});
|
|
479
|
+
toolCommand(program, "edit", "Replace a string in an existing file.").argument("<path>", "File to edit").argument("<old>", "The exact text to replace").argument("<new>", "The text to put in its place").option("--replace-all", "Replace every occurrence").action(
|
|
480
|
+
async (path, oldString, newString, opts) => {
|
|
481
|
+
await callTool(opts, "edit", {
|
|
482
|
+
path,
|
|
483
|
+
old_string: oldString,
|
|
484
|
+
new_string: newString,
|
|
485
|
+
replace_all: opts.replaceAll === true
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
);
|
|
489
|
+
toolCommand(program, "ls", "List a directory.").argument("<path>", "Directory to list").action(async (path, opts) => {
|
|
490
|
+
await callTool(opts, "ls", { path });
|
|
491
|
+
});
|
|
492
|
+
toolCommand(program, "grep", "Search files recursively, as grep -rn does.").argument("<pattern>", "Regular expression to search for").argument("<path>", "File or directory to search under").option("-i, --ignore-case", "Match case-insensitively").option("-F, --fixed-strings", "Read the pattern as a literal string").option("--include <glob>", "Search only files whose name matches the glob").option("-C, --context <n>", "Lines of context around each match", Number).option("-l, --files-with-matches", "Print only the names of matching files").option("-c, --count", "Print only a count of matching lines per file").option("-m, --max-count <n>", "Stop each file after this many matches", Number).action(
|
|
493
|
+
async (pattern, path, opts) => {
|
|
494
|
+
const args = { pattern, path };
|
|
495
|
+
if (opts.ignoreCase === true) args.ignore_case = true;
|
|
496
|
+
if (opts.fixedStrings === true) args.fixed_strings = true;
|
|
497
|
+
if (opts.include !== void 0) args.include = opts.include;
|
|
498
|
+
if (opts.context !== void 0) args.context = opts.context;
|
|
499
|
+
if (opts.filesWithMatches === true) args.files_with_matches = true;
|
|
500
|
+
if (opts.count === true) args.count = true;
|
|
501
|
+
if (opts.maxCount !== void 0) args.max_count = opts.maxCount;
|
|
502
|
+
await callTool(opts, "grep", args);
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
toolCommand(program, "glob", "Find files whose path matches a pattern.").argument("<pattern>", "Pathname pattern such as **/*.py").argument("[path]", "Directory a relative pattern is matched under", "/").action(async (pattern, path, opts) => {
|
|
506
|
+
await callTool(opts, "glob", { pattern, path });
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
286
510
|
// src/job.ts
|
|
287
511
|
function buildClient2() {
|
|
288
512
|
return makeClient(loadDaemonSettings());
|
|
@@ -292,77 +516,437 @@ function registerJobCommands(program) {
|
|
|
292
516
|
job.command("list").option("-w, --workspace <id>").action(async (opts) => {
|
|
293
517
|
const c = buildClient2();
|
|
294
518
|
await c.ensureRunning({ allowSpawn: false });
|
|
295
|
-
const path = "/v1/jobs" + (opts.workspace !== void 0 ? `?
|
|
519
|
+
const path = "/v1/jobs" + (opts.workspace !== void 0 ? `?workspace_id=${encodeURIComponent(opts.workspace)}` : "");
|
|
296
520
|
emit(await handleResponse(await c.request("GET", path)));
|
|
297
521
|
});
|
|
298
522
|
job.command("get").argument("<id>").action(async (id) => {
|
|
299
523
|
const c = buildClient2();
|
|
300
524
|
await c.ensureRunning({ allowSpawn: false });
|
|
301
|
-
const response = await handleResponse(
|
|
525
|
+
const response = await handleResponse(
|
|
526
|
+
await c.request("GET", `/v1/jobs/${encodeURIComponent(id)}`)
|
|
527
|
+
);
|
|
302
528
|
emit(response);
|
|
303
|
-
process.
|
|
529
|
+
process.exitCode = exitCodeFromResponse(response);
|
|
304
530
|
});
|
|
305
531
|
job.command("wait").argument("<id>").option("--timeout <s>").action(async (id, opts) => {
|
|
306
532
|
const c = buildClient2();
|
|
307
533
|
await c.ensureRunning({ allowSpawn: false });
|
|
308
534
|
const body = {};
|
|
309
|
-
if (opts.timeout !== void 0) body.
|
|
535
|
+
if (opts.timeout !== void 0) body.timeout_s = Number(opts.timeout);
|
|
310
536
|
const response = await handleResponse(
|
|
311
|
-
await c.request("POST", `/v1/jobs/${id}/wait`, {
|
|
537
|
+
await c.request("POST", `/v1/jobs/${encodeURIComponent(id)}/wait`, {
|
|
538
|
+
body: JSON.stringify(body)
|
|
539
|
+
})
|
|
312
540
|
);
|
|
313
541
|
emit(response);
|
|
314
|
-
process.
|
|
542
|
+
process.exitCode = exitCodeFromResponse(response);
|
|
315
543
|
});
|
|
316
544
|
job.command("cancel").argument("<id>").action(async (id) => {
|
|
317
545
|
const c = buildClient2();
|
|
318
546
|
await c.ensureRunning({ allowSpawn: false });
|
|
319
|
-
emit(await handleResponse(await c.request("DELETE", `/v1/jobs/${id}`)));
|
|
547
|
+
emit(await handleResponse(await c.request("DELETE", `/v1/jobs/${encodeURIComponent(id)}`)));
|
|
320
548
|
});
|
|
321
549
|
}
|
|
322
550
|
|
|
323
|
-
// src/
|
|
324
|
-
import {
|
|
325
|
-
import {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
551
|
+
// src/login.ts
|
|
552
|
+
import { spawn } from "child_process";
|
|
553
|
+
import { createHash, randomBytes } from "crypto";
|
|
554
|
+
import { createServer } from "http";
|
|
555
|
+
import { rstripSlash } from "@struktoai/mirage-core/utils/slash";
|
|
556
|
+
import {
|
|
557
|
+
AUTHORIZATION_SERVER_PATH,
|
|
558
|
+
PROTECTED_RESOURCE_PATH
|
|
559
|
+
} from "@struktoai/mirage-server/auth/config";
|
|
560
|
+
var CALLBACK_PATH = "/callback";
|
|
561
|
+
var CALLBACK_TIMEOUT_SECONDS = 300;
|
|
562
|
+
var SCOPE = "profile email offline_access";
|
|
563
|
+
function noLogin(url) {
|
|
564
|
+
return `${url} publishes no login: a local daemon needs none, and a server with a shared token takes \`mirage login --token\``;
|
|
565
|
+
}
|
|
566
|
+
function metadataUrl(issuer) {
|
|
567
|
+
const parts = new URL(issuer);
|
|
568
|
+
return `${parts.protocol}//${parts.host}${AUTHORIZATION_SERVER_PATH}${rstripSlash(parts.pathname)}`;
|
|
569
|
+
}
|
|
570
|
+
async function getJson(url) {
|
|
571
|
+
let reply;
|
|
572
|
+
try {
|
|
573
|
+
reply = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
574
|
+
} catch (error) {
|
|
575
|
+
throw new LoginError(`could not reach ${url}: ${String(error)}`);
|
|
576
|
+
}
|
|
577
|
+
if (reply.status !== 200) throw new LoginError(`${url} answered ${String(reply.status)}`);
|
|
578
|
+
return jsonBody(reply);
|
|
579
|
+
}
|
|
580
|
+
function challenge(verifier) {
|
|
581
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
582
|
+
}
|
|
583
|
+
function openBrowser(url) {
|
|
584
|
+
const custom = process.env.BROWSER ?? "";
|
|
585
|
+
const [command2, args] = custom !== "" ? [custom, [url]] : process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
586
|
+
const child = spawn(command2, args, { stdio: "ignore", detached: true });
|
|
587
|
+
child.on("error", (error) => {
|
|
588
|
+
console.error(`could not open the browser: ${error.message}`);
|
|
331
589
|
});
|
|
590
|
+
child.unref();
|
|
332
591
|
}
|
|
333
|
-
async function
|
|
334
|
-
|
|
592
|
+
async function listen(state) {
|
|
593
|
+
let settle = () => void 0;
|
|
594
|
+
const answer2 = new Promise((resolve2) => {
|
|
595
|
+
settle = resolve2;
|
|
596
|
+
});
|
|
597
|
+
const server = createServer((req, res) => {
|
|
598
|
+
const parts = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
599
|
+
const query = Object.fromEntries(parts.searchParams);
|
|
600
|
+
if (parts.pathname !== CALLBACK_PATH || query.state !== state) {
|
|
601
|
+
res.writeHead(404).end();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const text2 = query.code !== void 0 ? "Logged in to Mirage. You can close this tab." : `Login failed: ${query.error ?? "no code"}.`;
|
|
605
|
+
const escaped = text2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
606
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
607
|
+
res.end(`<!doctype html><p>${escaped}</p>`);
|
|
608
|
+
settle(query);
|
|
609
|
+
});
|
|
610
|
+
await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
|
|
611
|
+
const { port } = server.address();
|
|
612
|
+
return {
|
|
613
|
+
redirect: `http://127.0.0.1:${String(port)}${CALLBACK_PATH}`,
|
|
614
|
+
answer: answer2,
|
|
615
|
+
close: () => {
|
|
616
|
+
server.closeAllConnections();
|
|
617
|
+
server.close();
|
|
618
|
+
}
|
|
619
|
+
};
|
|
335
620
|
}
|
|
336
|
-
async function
|
|
337
|
-
|
|
338
|
-
const workspace = await buildMcpWorkspace(configPath);
|
|
621
|
+
async function browserLogin(url) {
|
|
622
|
+
let found;
|
|
339
623
|
try {
|
|
340
|
-
await
|
|
341
|
-
|
|
624
|
+
found = await fetch(url + PROTECTED_RESOURCE_PATH, { signal: AbortSignal.timeout(3e4) });
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (isLocalUrl(url)) return null;
|
|
627
|
+
throw new LoginError(`could not reach ${url}: ${String(error)}`);
|
|
628
|
+
}
|
|
629
|
+
if (found.status === 404) return null;
|
|
630
|
+
const resource = await jsonBody(found);
|
|
631
|
+
const servers = resource.authorization_servers;
|
|
632
|
+
const clientId = resource.client_id;
|
|
633
|
+
if (!Array.isArray(servers) || servers.length === 0 || typeof clientId !== "string") {
|
|
634
|
+
throw new LoginError(`${url} published no issuer to log in through`);
|
|
635
|
+
}
|
|
636
|
+
const issuer = String(servers[0]);
|
|
637
|
+
const meta = await getJson(metadataUrl(issuer));
|
|
638
|
+
if (meta.issuer !== issuer) {
|
|
639
|
+
throw new LoginError(`${issuer} published another issuer's endpoints`);
|
|
640
|
+
}
|
|
641
|
+
const verifier = randomBytes(48).toString("base64url");
|
|
642
|
+
const state = randomBytes(16).toString("base64url");
|
|
643
|
+
const callback = await listen(state);
|
|
644
|
+
let answer2;
|
|
645
|
+
try {
|
|
646
|
+
const authorize = new URL(String(meta.authorization_endpoint));
|
|
647
|
+
const params = {
|
|
648
|
+
response_type: "code",
|
|
649
|
+
client_id: clientId,
|
|
650
|
+
redirect_uri: callback.redirect,
|
|
651
|
+
scope: SCOPE,
|
|
652
|
+
state,
|
|
653
|
+
code_challenge: challenge(verifier),
|
|
654
|
+
code_challenge_method: "S256"
|
|
655
|
+
};
|
|
656
|
+
for (const [key, value2] of Object.entries(params)) authorize.searchParams.set(key, value2);
|
|
657
|
+
process.stderr.write(
|
|
658
|
+
`Log in to ${url} in your browser. If it does not open, go to:
|
|
659
|
+
|
|
660
|
+
${authorize.href}
|
|
661
|
+
|
|
662
|
+
`
|
|
663
|
+
);
|
|
664
|
+
openBrowser(authorize.href);
|
|
665
|
+
let timer;
|
|
666
|
+
const late = new Promise((_, reject) => {
|
|
667
|
+
timer = setTimeout(() => {
|
|
668
|
+
reject(new LoginError("the browser did not come back in time"));
|
|
669
|
+
}, CALLBACK_TIMEOUT_SECONDS * 1e3);
|
|
670
|
+
});
|
|
671
|
+
try {
|
|
672
|
+
answer2 = await Promise.race([callback.answer, late]);
|
|
673
|
+
} finally {
|
|
674
|
+
clearTimeout(timer);
|
|
675
|
+
}
|
|
676
|
+
} finally {
|
|
677
|
+
callback.close();
|
|
678
|
+
}
|
|
679
|
+
const code = answer2.code;
|
|
680
|
+
if (code === void 0) {
|
|
681
|
+
throw new LoginError(`the issuer refused the login: ${answer2.error ?? "no code"}`);
|
|
682
|
+
}
|
|
683
|
+
const tokenEndpoint = String(meta.token_endpoint);
|
|
684
|
+
const signedIn = Date.now() / 1e3;
|
|
685
|
+
let reply;
|
|
686
|
+
try {
|
|
687
|
+
reply = await fetch(tokenEndpoint, {
|
|
688
|
+
method: "POST",
|
|
689
|
+
body: new URLSearchParams({
|
|
690
|
+
grant_type: "authorization_code",
|
|
691
|
+
code,
|
|
692
|
+
redirect_uri: callback.redirect,
|
|
693
|
+
client_id: clientId,
|
|
694
|
+
code_verifier: verifier
|
|
695
|
+
}),
|
|
696
|
+
signal: AbortSignal.timeout(3e4)
|
|
342
697
|
});
|
|
343
698
|
} catch (error) {
|
|
344
|
-
|
|
345
|
-
|
|
699
|
+
throw new LoginError(`could not reach the issuer: ${String(error)}`);
|
|
700
|
+
}
|
|
701
|
+
const body = await jsonBody(reply);
|
|
702
|
+
if (reply.status !== 200 || typeof body.access_token !== "string") {
|
|
703
|
+
const reason = typeof body.error === "string" ? body.error : String(reply.status);
|
|
704
|
+
throw new LoginError(`the issuer refused the code: ${reason}`);
|
|
346
705
|
}
|
|
706
|
+
const refresh = body.refresh_token;
|
|
707
|
+
return {
|
|
708
|
+
url,
|
|
709
|
+
access_token: body.access_token,
|
|
710
|
+
logged_in_at: signedIn,
|
|
711
|
+
refresh_token: typeof refresh === "string" && refresh !== "" ? refresh : null,
|
|
712
|
+
expires_at: endsAt(body, signedIn),
|
|
713
|
+
client_id: clientId,
|
|
714
|
+
token_endpoint: tokenEndpoint
|
|
715
|
+
};
|
|
347
716
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
717
|
+
async function tokenLogin(url, token) {
|
|
718
|
+
let reply;
|
|
719
|
+
try {
|
|
720
|
+
reply = await fetch(`${url}/v1/workspaces`, {
|
|
721
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
722
|
+
signal: AbortSignal.timeout(3e4)
|
|
723
|
+
});
|
|
724
|
+
} catch (error) {
|
|
725
|
+
throw new LoginError(`could not reach ${url}: ${String(error)}`);
|
|
726
|
+
}
|
|
727
|
+
if (reply.status === 401) throw new LoginError(`${url} refused the token`);
|
|
728
|
+
if (!reply.ok) throw new LoginError(`${url} answered ${String(reply.status)}`);
|
|
729
|
+
const exp = tokenClaims(token).exp;
|
|
730
|
+
return {
|
|
731
|
+
url,
|
|
732
|
+
access_token: token,
|
|
733
|
+
logged_in_at: Date.now() / 1e3,
|
|
734
|
+
refresh_token: null,
|
|
735
|
+
expires_at: typeof exp === "number" ? exp : null,
|
|
736
|
+
client_id: null,
|
|
737
|
+
token_endpoint: null
|
|
738
|
+
};
|
|
350
739
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
740
|
+
function registerLoginCommands(program) {
|
|
741
|
+
program.command("login").description("Log in to the server the CLI points at.").option(
|
|
742
|
+
"--token <token>",
|
|
743
|
+
"Keep this token instead of using the browser; - reads it from stdin"
|
|
744
|
+
).action(async (opts) => {
|
|
745
|
+
let token = opts.token;
|
|
746
|
+
if (token === "-") {
|
|
747
|
+
const chunks = [];
|
|
748
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
749
|
+
token = Buffer.concat(chunks).toString("utf-8").trim();
|
|
750
|
+
if (token === "") fail("no token on stdin");
|
|
751
|
+
}
|
|
752
|
+
const url = rstripSlash(loadDaemonSettings().url);
|
|
753
|
+
const login = token !== void 0 && token !== "" ? await tokenLogin(url, token) : await browserLogin(url);
|
|
754
|
+
if (login === null) {
|
|
755
|
+
process.stdout.write(noLogin(url) + "\n");
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
writeLogin(login);
|
|
759
|
+
const account = tokenClaims(login.access_token).sub;
|
|
760
|
+
const who = typeof account === "string" ? ` as ${account}.` : ".";
|
|
761
|
+
process.stdout.write(`Logged in to ${url}${who}
|
|
762
|
+
`);
|
|
763
|
+
});
|
|
764
|
+
program.command("logout").description("Forget the stored login.").action(() => {
|
|
765
|
+
const login = removeLogin();
|
|
766
|
+
process.stdout.write(login !== null ? `Logged out of ${login.url}.
|
|
767
|
+
` : "Not logged in.\n");
|
|
768
|
+
});
|
|
769
|
+
program.command("whoami").description("Print who the login is for, and when to log in again.").action(() => {
|
|
770
|
+
const url = rstripSlash(loadDaemonSettings().url);
|
|
771
|
+
const login = readLogin();
|
|
772
|
+
if (login?.url !== url) fail(`not logged in to ${url}`);
|
|
773
|
+
const account = tokenClaims(login.access_token).sub;
|
|
359
774
|
emit(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
775
|
+
{
|
|
776
|
+
account: typeof account === "string" ? account : null,
|
|
777
|
+
url,
|
|
778
|
+
renew_by: new Date(renewBy(login) * 1e3).toISOString().slice(0, 10)
|
|
779
|
+
},
|
|
780
|
+
(row) => Object.entries(row).map(([k, v]) => `${k}: ${String(v)}`).join("\n")
|
|
781
|
+
);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/mcp.ts
|
|
786
|
+
import { resolveWorkspaceConfig } from "@struktoai/mirage-server/workspace_config";
|
|
787
|
+
var MCP_ENV_NAMES = ["MIRAGE_MCP_CONFIG", "MIRAGE_CONFIG"];
|
|
788
|
+
async function hasSession(client, workspacePath, sessionId) {
|
|
789
|
+
const r = await client.request("GET", `${workspacePath}/sessions`);
|
|
790
|
+
if (r.status >= 400) {
|
|
791
|
+
const { detail } = await r.json();
|
|
792
|
+
throw new Error(`daemon error ${String(r.status)}: ${String(detail)}`);
|
|
793
|
+
}
|
|
794
|
+
const rows = await r.json();
|
|
795
|
+
return Array.isArray(rows) && rows.some((row) => row.session_id === sessionId);
|
|
796
|
+
}
|
|
797
|
+
async function runMcp(config, options) {
|
|
798
|
+
if (options.workspace !== void 0 && config !== void 0) {
|
|
799
|
+
fail("pass a config or --workspace, not both", 2);
|
|
800
|
+
}
|
|
801
|
+
let path;
|
|
802
|
+
if (options.workspace === void 0) {
|
|
803
|
+
try {
|
|
804
|
+
path = resolveWorkspaceConfig(config, { envNames: MCP_ENV_NAMES });
|
|
805
|
+
} catch (error) {
|
|
806
|
+
fail(error instanceof Error ? error.message : String(error), 2);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const { relayStdio } = await import("@struktoai/mirage-server/mcp");
|
|
810
|
+
await relayWorkspace(
|
|
811
|
+
path,
|
|
812
|
+
options.workspace,
|
|
813
|
+
options.session,
|
|
814
|
+
"mcp",
|
|
815
|
+
relayStdio,
|
|
816
|
+
options.allCalls === true
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
async function deleteWorkspace(client, workspaceId) {
|
|
820
|
+
const path = `/v1/workspaces/${encodeURIComponent(workspaceId)}`;
|
|
821
|
+
const { url, idleGraceSeconds } = client.settings;
|
|
822
|
+
const attempt = (bearer) => makeClient({ url, idleGraceSeconds, authToken: bearer }).request("DELETE", path);
|
|
823
|
+
let done = await attempt(client.held);
|
|
824
|
+
if (done.status === 401 && client.settings.login !== void 0) {
|
|
825
|
+
let fresh;
|
|
826
|
+
try {
|
|
827
|
+
fresh = await client.token();
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (!(error instanceof LoginError)) throw error;
|
|
830
|
+
process.stderr.write(`could not delete workspace ${workspaceId}: ${error.message}
|
|
831
|
+
`);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
done = await attempt(fresh);
|
|
835
|
+
}
|
|
836
|
+
if (!done.ok) {
|
|
837
|
+
process.stderr.write(
|
|
838
|
+
`could not delete workspace ${workspaceId}: daemon error ${String(done.status)}
|
|
839
|
+
`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async function relayWorkspace(path, workspace, session, endpoint, relay, allCalls = false) {
|
|
844
|
+
const client = makeClient(loadDaemonSettings());
|
|
845
|
+
try {
|
|
846
|
+
await client.ensureRunning({ allowSpawn: true });
|
|
847
|
+
} catch (error) {
|
|
848
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
849
|
+
}
|
|
850
|
+
let workspaceId;
|
|
851
|
+
let minted = false;
|
|
852
|
+
if (path !== void 0) {
|
|
853
|
+
const { checkWorkspaceConfigFile } = await import("@struktoai/mirage-node/config");
|
|
854
|
+
const loaded = checkWorkspaceConfigFile(path);
|
|
855
|
+
const body = JSON.stringify({ config: loaded });
|
|
856
|
+
const created = await handleResponse(await client.request("POST", "/v1/workspaces", { body }));
|
|
857
|
+
workspaceId = created.id;
|
|
858
|
+
minted = typeof loaded.workspace_id !== "string" || loaded.workspace_id === "";
|
|
859
|
+
} else {
|
|
860
|
+
workspaceId = workspace ?? "";
|
|
861
|
+
await handleResponse(
|
|
862
|
+
await client.request("GET", `/v1/workspaces/${encodeURIComponent(workspaceId)}`)
|
|
365
863
|
);
|
|
864
|
+
}
|
|
865
|
+
const workspacePath = `/v1/workspaces/${encodeURIComponent(workspaceId)}`;
|
|
866
|
+
const query = new URLSearchParams();
|
|
867
|
+
if (session !== void 0) query.set("session_id", session);
|
|
868
|
+
if (allCalls) query.set("calls", "all");
|
|
869
|
+
const suffix = query.size === 0 ? "" : `?${query.toString()}`;
|
|
870
|
+
const url = `${client.settings.url}${workspacePath}/${endpoint}${suffix}`;
|
|
871
|
+
let refusal;
|
|
872
|
+
try {
|
|
873
|
+
if (session !== void 0) {
|
|
874
|
+
refusal = await hasSession(client, workspacePath, session).then(
|
|
875
|
+
(found) => found ? void 0 : `session not found: ${session}`,
|
|
876
|
+
(error) => error instanceof Error ? error.message : String(error)
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (refusal === void 0) await relay(url, () => client.token());
|
|
880
|
+
} finally {
|
|
881
|
+
if (minted) await deleteWorkspace(client, workspaceId);
|
|
882
|
+
}
|
|
883
|
+
if (refusal !== void 0) fail(refusal, 2);
|
|
884
|
+
}
|
|
885
|
+
function registerMcpCommand(program) {
|
|
886
|
+
program.command("mcp").argument("[config]", "Mirage workspace YAML config").option("-w, --workspace <id>", "Serve this daemon workspace instead of loading a config").option("-s, --session <id>", "Session the tools act as; the workspace's default when absent").option("--all-calls", "Also serve each VFS call as a tool, and explain").description("Serve a Mirage workspace's MCP tools over stdio.").action(runMcp);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/rpc.ts
|
|
890
|
+
import { resolveWorkspaceConfig as resolveWorkspaceConfig2 } from "@struktoai/mirage-server/workspace_config";
|
|
891
|
+
var RPC_ENV_NAMES = ["MIRAGE_RPC_CONFIG", "MIRAGE_CONFIG"];
|
|
892
|
+
function registerRpcCommand(program) {
|
|
893
|
+
program.command("rpc").argument("[config]", "Mirage workspace YAML config").option("-w, --workspace <id>", "Serve this daemon workspace instead of loading a config").option("-s, --session <id>", "Session the methods act as; the workspace's default when absent").description("Serve a workspace session's API over JSON-RPC on stdio.").action(
|
|
894
|
+
async (config, options) => {
|
|
895
|
+
if (options.workspace !== void 0 && config !== void 0) {
|
|
896
|
+
fail("pass a config or --workspace, not both", 2);
|
|
897
|
+
}
|
|
898
|
+
let path;
|
|
899
|
+
if (options.workspace === void 0) {
|
|
900
|
+
try {
|
|
901
|
+
path = resolveWorkspaceConfig2(config, { envNames: RPC_ENV_NAMES });
|
|
902
|
+
} catch (error) {
|
|
903
|
+
fail(error instanceof Error ? error.message : String(error), 2);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const { relayStdio } = await import("@struktoai/mirage-server/rpc");
|
|
907
|
+
await relayWorkspace(path, options.workspace, options.session, "rpc", relayStdio);
|
|
908
|
+
}
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/ssh.ts
|
|
913
|
+
function registerSshProxyCommand(program) {
|
|
914
|
+
program.command("ssh-proxy").argument("<workspace>", "The workspace to log in to").description("Carry SSH to a workspace over the server's HTTPS port, on stdio.").action(async (workspace) => {
|
|
915
|
+
const client = makeClient(loadDaemonSettings());
|
|
916
|
+
try {
|
|
917
|
+
await client.ensureRunning({ allowSpawn: true });
|
|
918
|
+
} catch (error) {
|
|
919
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
920
|
+
}
|
|
921
|
+
const base = client.settings.url;
|
|
922
|
+
const token = await client.token();
|
|
923
|
+
const url = `ws${base.slice("http".length)}/v1/workspaces/${encodeURIComponent(workspace)}/ssh`;
|
|
924
|
+
const headers = token === "" ? {} : { Authorization: `Bearer ${token}` };
|
|
925
|
+
const { TunnelRefused, relaySsh } = await import("@struktoai/mirage-server/ssh/relay");
|
|
926
|
+
try {
|
|
927
|
+
await relaySsh(url, headers);
|
|
928
|
+
} catch (error) {
|
|
929
|
+
if (error instanceof TunnelRefused) fail(`ssh-proxy: ${error.message}`);
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
function registerCodexExecCommand(program) {
|
|
935
|
+
program.command("codex-exec").argument("<workspace>", "The workspace to serve").option("--listen <address>", "HOST:PORT to accept codex exec-server forward on", "127.0.0.1:0").description("Serve a workspace to `codex exec-server forward` on a local port.").action(async (workspace, opts) => {
|
|
936
|
+
const at = opts.listen.lastIndexOf(":");
|
|
937
|
+
const host = opts.listen.slice(0, at);
|
|
938
|
+
const port = opts.listen.slice(at + 1);
|
|
939
|
+
if (at <= 0 || !/^\d+$/.test(port)) fail("codex-exec: --listen takes HOST:PORT");
|
|
940
|
+
const client = makeClient(loadDaemonSettings());
|
|
941
|
+
try {
|
|
942
|
+
await client.ensureRunning({ allowSpawn: true });
|
|
943
|
+
} catch (error) {
|
|
944
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
945
|
+
}
|
|
946
|
+
const base = client.settings.url;
|
|
947
|
+
const url = `ws${base.slice("http".length)}/v1/workspaces/${encodeURIComponent(workspace)}/codex-exec`;
|
|
948
|
+
const { relayCodex } = await import("@struktoai/mirage-server/ssh/relay");
|
|
949
|
+
await relayCodex(host.replace(/^\[|\]$/g, ""), Number(port), url, () => client.token());
|
|
366
950
|
});
|
|
367
951
|
}
|
|
368
952
|
|
|
@@ -389,7 +973,7 @@ function registerSessionCommands(program) {
|
|
|
389
973
|
sess.command("create").argument("<wsId>").option("--id <sessionId>").option(
|
|
390
974
|
"-m, --mount <prefix>",
|
|
391
975
|
"narrow a mount's mode for this session: '/data:read' (alias '/data:r'), '/scratch:rw', '/bin:rwx', or a bare '/data' to keep the mount's own mode; repeatable. This narrows only: a mount you do not name keeps its own mode, and keeping a session away from one is a hide in its profile",
|
|
392
|
-
(
|
|
976
|
+
(value2, prev) => prev.concat([value2]),
|
|
393
977
|
[]
|
|
394
978
|
).option(
|
|
395
979
|
"-p, --profile <name>",
|
|
@@ -398,14 +982,14 @@ function registerSessionCommands(program) {
|
|
|
398
982
|
const c = buildClient3();
|
|
399
983
|
await c.ensureRunning({ allowSpawn: false });
|
|
400
984
|
const body = {};
|
|
401
|
-
if (opts.id !== void 0) body.
|
|
985
|
+
if (opts.id !== void 0) body.session_id = opts.id;
|
|
402
986
|
if (opts.mount !== void 0 && opts.mount.length > 0) {
|
|
403
987
|
body.mounts = parseMountModes(opts.mount);
|
|
404
988
|
}
|
|
405
989
|
if (opts.profile !== void 0) body.profile = opts.profile;
|
|
406
990
|
emit(
|
|
407
991
|
await handleResponse(
|
|
408
|
-
await c.request("POST", `/v1/workspaces/${wsId}/sessions`, {
|
|
992
|
+
await c.request("POST", `/v1/workspaces/${encodeURIComponent(wsId)}/sessions`, {
|
|
409
993
|
body: JSON.stringify(body)
|
|
410
994
|
})
|
|
411
995
|
)
|
|
@@ -414,44 +998,81 @@ function registerSessionCommands(program) {
|
|
|
414
998
|
sess.command("list").argument("<wsId>").action(async (wsId) => {
|
|
415
999
|
const c = buildClient3();
|
|
416
1000
|
await c.ensureRunning({ allowSpawn: false });
|
|
417
|
-
emit(
|
|
1001
|
+
emit(
|
|
1002
|
+
await handleResponse(
|
|
1003
|
+
await c.request("GET", `/v1/workspaces/${encodeURIComponent(wsId)}/sessions`)
|
|
1004
|
+
)
|
|
1005
|
+
);
|
|
418
1006
|
});
|
|
419
1007
|
sess.command("delete").argument("<wsId>").argument("<sessionId>").action(async (wsId, sessionId) => {
|
|
420
1008
|
const c = buildClient3();
|
|
421
1009
|
await c.ensureRunning({ allowSpawn: false });
|
|
422
1010
|
emit(
|
|
423
1011
|
await handleResponse(
|
|
424
|
-
await c.request(
|
|
1012
|
+
await c.request(
|
|
1013
|
+
"DELETE",
|
|
1014
|
+
`/v1/workspaces/${encodeURIComponent(wsId)}/sessions/${encodeURIComponent(sessionId)}`
|
|
1015
|
+
)
|
|
425
1016
|
)
|
|
426
1017
|
);
|
|
427
1018
|
});
|
|
1019
|
+
sess.command("cancel").description("Cancel the session's running and queued commands.").argument("<wsId>").argument("<sessionId>").action(async (wsId, sessionId) => {
|
|
1020
|
+
const c = buildClient3();
|
|
1021
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
1022
|
+
emit(
|
|
1023
|
+
await handleResponse(
|
|
1024
|
+
await c.request(
|
|
1025
|
+
"POST",
|
|
1026
|
+
`/v1/workspaces/${encodeURIComponent(wsId)}/sessions/${encodeURIComponent(sessionId)}/cancel`
|
|
1027
|
+
)
|
|
1028
|
+
)
|
|
1029
|
+
);
|
|
1030
|
+
});
|
|
1031
|
+
sess.command("kill").description("Kill the session's background jobs; the session stays open.").argument("<wsId>").argument("<sessionId>").action(async (wsId, sessionId) => {
|
|
1032
|
+
const c = buildClient3();
|
|
1033
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
1034
|
+
emit(
|
|
1035
|
+
await handleResponse(
|
|
1036
|
+
await c.request(
|
|
1037
|
+
"POST",
|
|
1038
|
+
`/v1/workspaces/${encodeURIComponent(wsId)}/sessions/${encodeURIComponent(sessionId)}/kill`
|
|
1039
|
+
)
|
|
1040
|
+
)
|
|
1041
|
+
);
|
|
1042
|
+
});
|
|
1043
|
+
sess.command("update").description("Replace the session's profile; its cwd, env and history stay.").argument("<wsId>").argument("<sessionId>").option("-p, --profile <name>", "Replace the live session's profile.").option("--default-profile", "Use the workspace default profile.").action(
|
|
1044
|
+
async (wsId, sessionId, opts) => {
|
|
1045
|
+
if (opts.profile === void 0 === (opts.defaultProfile !== true))
|
|
1046
|
+
fail("choose --profile or --default-profile", 2);
|
|
1047
|
+
const c = buildClient3();
|
|
1048
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
1049
|
+
emit(
|
|
1050
|
+
await handleResponse(
|
|
1051
|
+
await c.request(
|
|
1052
|
+
"PATCH",
|
|
1053
|
+
`/v1/workspaces/${encodeURIComponent(wsId)}/sessions/${encodeURIComponent(sessionId)}`,
|
|
1054
|
+
{ body: JSON.stringify({ profile: opts.profile ?? null }) }
|
|
1055
|
+
)
|
|
1056
|
+
)
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
);
|
|
428
1060
|
}
|
|
429
1061
|
|
|
430
1062
|
// src/workspace.ts
|
|
431
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1063
|
+
import { createReadStream, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
432
1064
|
import { dirname, resolve } from "path";
|
|
433
|
-
import {
|
|
434
|
-
absolutizeScripts,
|
|
435
|
-
checkWorkspaceConfigFile,
|
|
436
|
-
interpolateEnv
|
|
437
|
-
} from "@struktoai/mirage-server";
|
|
438
|
-
import { parse as yamlParse } from "yaml";
|
|
439
1065
|
function buildClient4() {
|
|
440
1066
|
return makeClient(loadDaemonSettings());
|
|
441
1067
|
}
|
|
442
|
-
function
|
|
443
|
-
const out = {};
|
|
444
|
-
for (const [k, v] of Object.entries(process.env)) {
|
|
445
|
-
if (typeof v === "string") out[k] = v;
|
|
446
|
-
}
|
|
447
|
-
return out;
|
|
448
|
-
}
|
|
449
|
-
function loadConfigArgument(path) {
|
|
1068
|
+
async function loadConfigArgument(path) {
|
|
450
1069
|
if (!existsSync2(path)) fail(`config file not found: ${path}`, 2);
|
|
451
|
-
const
|
|
1070
|
+
const { absolutizeScripts, interpolateEnv, readProcessEnv } = await import("@struktoai/mirage-node/config");
|
|
1071
|
+
const { parse: yamlParse } = await import("yaml");
|
|
1072
|
+
const text2 = readFileSync2(path, "utf-8");
|
|
452
1073
|
let config;
|
|
453
1074
|
try {
|
|
454
|
-
config = interpolateEnv(yamlParse(
|
|
1075
|
+
config = interpolateEnv(yamlParse(text2), readProcessEnv());
|
|
455
1076
|
} catch (err) {
|
|
456
1077
|
fail(`invalid config YAML/JSON at ${path}: ${String(err)}`, 2);
|
|
457
1078
|
}
|
|
@@ -465,9 +1086,9 @@ function formatWorkspaceList(items) {
|
|
|
465
1086
|
const rows = items.map((w) => [
|
|
466
1087
|
w.id,
|
|
467
1088
|
w.mode,
|
|
468
|
-
String(w.
|
|
469
|
-
String(w.
|
|
470
|
-
formatAge(w.
|
|
1089
|
+
String(w.mount_count),
|
|
1090
|
+
String(w.session_count),
|
|
1091
|
+
formatAge(w.created_at)
|
|
471
1092
|
]);
|
|
472
1093
|
return formatTable(["ID", "MODE", "MOUNTS", "SESSIONS", "AGE"], rows);
|
|
473
1094
|
}
|
|
@@ -475,17 +1096,17 @@ function formatWorkspaceDetail(d) {
|
|
|
475
1096
|
const lines = [
|
|
476
1097
|
`ID: ${d.id}`,
|
|
477
1098
|
`Mode: ${d.mode}`,
|
|
478
|
-
`Created: ${formatAge(d.
|
|
1099
|
+
`Created: ${formatAge(d.created_at)} ago`
|
|
479
1100
|
];
|
|
480
1101
|
if (d.mounts !== void 0 && d.mounts.length > 0) {
|
|
481
|
-
const rows = d.mounts.map((m) => [m.prefix, m.
|
|
1102
|
+
const rows = d.mounts.map((m) => [m.prefix, m.vfs, m.mode]);
|
|
482
1103
|
lines.push("", "Mounts:");
|
|
483
|
-
for (const ln of formatTable(["PREFIX", "
|
|
1104
|
+
for (const ln of formatTable(["PREFIX", "VFS", "MODE"], rows).split("\n")) {
|
|
484
1105
|
lines.push(" " + ln);
|
|
485
1106
|
}
|
|
486
1107
|
}
|
|
487
1108
|
if (d.sessions !== void 0 && d.sessions.length > 0) {
|
|
488
|
-
const rows = d.sessions.map((s) => [s.
|
|
1109
|
+
const rows = d.sessions.map((s) => [s.session_id, s.cwd]);
|
|
489
1110
|
lines.push("", "Sessions:");
|
|
490
1111
|
for (const ln of formatTable(["SESSION", "CWD"], rows).split("\n")) {
|
|
491
1112
|
lines.push(" " + ln);
|
|
@@ -493,9 +1114,9 @@ function formatWorkspaceDetail(d) {
|
|
|
493
1114
|
}
|
|
494
1115
|
if (d.internals != null) {
|
|
495
1116
|
lines.push("", "Internals:");
|
|
496
|
-
for (const k of ["
|
|
497
|
-
const
|
|
498
|
-
lines.push(` ${k.padEnd(16)} ${
|
|
1117
|
+
for (const k of ["cache_bytes", "cache_entries", "history_length", "in_flight_jobs"]) {
|
|
1118
|
+
const value2 = d.internals[k];
|
|
1119
|
+
lines.push(` ${k.padEnd(16)} ${value2 === null ? "n/a (not tracked)" : String(value2)}`);
|
|
499
1120
|
}
|
|
500
1121
|
}
|
|
501
1122
|
return lines.join("\n");
|
|
@@ -506,31 +1127,38 @@ function formatAsks(items) {
|
|
|
506
1127
|
["ID", "SESSION", "COMMAND", "STATUS", "REASON"],
|
|
507
1128
|
items.map((a) => [
|
|
508
1129
|
a.id,
|
|
509
|
-
a.
|
|
1130
|
+
a.session_id,
|
|
510
1131
|
[a.command, ...a.argv].join(" "),
|
|
511
1132
|
a.outcome ?? "pending",
|
|
512
1133
|
a.reason
|
|
513
1134
|
])
|
|
514
1135
|
);
|
|
515
1136
|
}
|
|
516
|
-
function formatVersionLog(items) {
|
|
517
|
-
if (items.length === 0) return "No versions.";
|
|
518
|
-
return formatTable(
|
|
519
|
-
["VERSION", "MESSAGE"],
|
|
520
|
-
items.map((v) => [v.id.slice(0, 12), v.message])
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
function formatDiff(changes) {
|
|
524
|
-
const lines = [];
|
|
525
|
-
for (const kind of ["added", "modified", "deleted"]) {
|
|
526
|
-
for (const path of changes[kind]) lines.push(`${kind.padEnd(9)} ${path}`);
|
|
527
|
-
}
|
|
528
|
-
return lines.length > 0 ? lines.join("\n") : "No changes.";
|
|
529
|
-
}
|
|
530
1137
|
function registerWorkspaceCommands(program) {
|
|
531
1138
|
const ws = program.command("workspace").description("Manage workspaces.");
|
|
1139
|
+
for (const kind of ["vfs", "skill"]) {
|
|
1140
|
+
ws.command(`${kind}-md`).argument("<wsId>").description(`Generate ${kind} Markdown, or expose it inside the workspace with --path.`).option("--path <path>", "Expose a live file at this workspace path.").option("--session <id>", "Limit exposure or generation to this session.").option("--profile <name>", "Preview a profile without creating a file.").action(async (wsId, opts) => {
|
|
1141
|
+
if (opts.profile !== void 0 && (opts.path !== void 0 || opts.session !== void 0))
|
|
1142
|
+
fail("--profile requires no --path or --session", 2);
|
|
1143
|
+
const c = buildClient4();
|
|
1144
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
1145
|
+
let route = `/v1/workspaces/${encodeURIComponent(wsId)}`;
|
|
1146
|
+
if (opts.session !== void 0) route += `/sessions/${encodeURIComponent(opts.session)}`;
|
|
1147
|
+
route += `/${kind}-md`;
|
|
1148
|
+
if (opts.profile !== void 0) route += `?profile=${encodeURIComponent(opts.profile)}`;
|
|
1149
|
+
const response = opts.path === void 0 ? await c.request("GET", route) : await c.request("PUT", route, { body: JSON.stringify({ path: opts.path }) });
|
|
1150
|
+
if (!response.ok) await handleResponse(response);
|
|
1151
|
+
if (opts.path === void 0) process.stdout.write(await response.text());
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
532
1154
|
ws.command("create").description("Create a workspace; daemon auto-spawns if not running.").argument("<config>", "YAML/JSON workspace config").option("--id <id>", "Explicit workspace id").action(async (configPath, opts) => {
|
|
533
|
-
const
|
|
1155
|
+
const { checkWorkspaceConfigFile } = await import("@struktoai/mirage-node/config");
|
|
1156
|
+
let cfg;
|
|
1157
|
+
try {
|
|
1158
|
+
cfg = checkWorkspaceConfigFile(configPath);
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
fail(err instanceof Error ? err.message : String(err), 2);
|
|
1161
|
+
}
|
|
534
1162
|
const body = { config: cfg };
|
|
535
1163
|
if (opts.id !== void 0) body.id = opts.id;
|
|
536
1164
|
const c = buildClient4();
|
|
@@ -549,7 +1177,7 @@ function registerWorkspaceCommands(program) {
|
|
|
549
1177
|
ws.command("get").description("Show full details for one workspace.").argument("<id>").option("--verbose", "Include cache/dirty/history internals").action(async (id, opts) => {
|
|
550
1178
|
const c = buildClient4();
|
|
551
1179
|
await c.ensureRunning({ allowSpawn: false });
|
|
552
|
-
const path = `/v1/workspaces/${id}` + (opts.verbose === true ? "?verbose=true" : "");
|
|
1180
|
+
const path = `/v1/workspaces/${encodeURIComponent(id)}` + (opts.verbose === true ? "?verbose=true" : "");
|
|
553
1181
|
emit(
|
|
554
1182
|
await handleResponse(await c.request("GET", path)),
|
|
555
1183
|
formatWorkspaceDetail
|
|
@@ -559,66 +1187,61 @@ function registerWorkspaceCommands(program) {
|
|
|
559
1187
|
const c = buildClient4();
|
|
560
1188
|
await c.ensureRunning({ allowSpawn: false });
|
|
561
1189
|
emit(
|
|
562
|
-
await handleResponse(
|
|
1190
|
+
await handleResponse(
|
|
1191
|
+
await c.request("DELETE", `/v1/workspaces/${encodeURIComponent(id)}`)
|
|
1192
|
+
),
|
|
563
1193
|
(d) => `Deleted workspace ${d.id}.`
|
|
564
1194
|
);
|
|
565
1195
|
});
|
|
566
|
-
ws.command("
|
|
567
|
-
const body = { sourceId: srcId };
|
|
568
|
-
if (opts.id !== void 0) body.id = opts.id;
|
|
569
|
-
if (opts.at !== void 0) body.at = opts.at;
|
|
1196
|
+
ws.command("close").description("Stop a workspace and keep its state for the same id.").argument("<id>").action(async (id) => {
|
|
570
1197
|
const c = buildClient4();
|
|
571
1198
|
await c.ensureRunning({ allowSpawn: false });
|
|
572
|
-
|
|
573
|
-
|
|
1199
|
+
emit(
|
|
1200
|
+
await handleResponse(
|
|
1201
|
+
await c.request("POST", `/v1/workspaces/${encodeURIComponent(id)}/close`)
|
|
1202
|
+
),
|
|
1203
|
+
(d) => `Closed workspace ${d.id}.`
|
|
1204
|
+
);
|
|
574
1205
|
});
|
|
575
|
-
ws.command("
|
|
1206
|
+
ws.command("cancel").description("Cancel the running and queued commands of every session.").argument("<id>").action(async (id) => {
|
|
576
1207
|
const c = buildClient4();
|
|
577
1208
|
await c.ensureRunning({ allowSpawn: false });
|
|
578
|
-
const r = await c.request("POST", `/v1/workspaces/${id}/commit`, {
|
|
579
|
-
body: JSON.stringify({ message: opts.message, branch: opts.branch })
|
|
580
|
-
});
|
|
581
1209
|
emit(
|
|
582
|
-
await handleResponse(
|
|
583
|
-
|
|
1210
|
+
await handleResponse(
|
|
1211
|
+
await c.request("POST", `/v1/workspaces/${encodeURIComponent(id)}/cancel`)
|
|
1212
|
+
)
|
|
584
1213
|
);
|
|
585
1214
|
});
|
|
586
|
-
ws.command("
|
|
1215
|
+
ws.command("kill").description("Kill the background jobs of every session.").argument("<id>").action(async (id) => {
|
|
587
1216
|
const c = buildClient4();
|
|
588
1217
|
await c.ensureRunning({ allowSpawn: false });
|
|
589
|
-
const r = await c.request("POST", `/v1/workspaces/${id}/branch`, {
|
|
590
|
-
body: JSON.stringify({ name, fromBranch: opts.from })
|
|
591
|
-
});
|
|
592
1218
|
emit(
|
|
593
|
-
await handleResponse(
|
|
594
|
-
|
|
1219
|
+
await handleResponse(
|
|
1220
|
+
await c.request("POST", `/v1/workspaces/${encodeURIComponent(id)}/kill`)
|
|
1221
|
+
)
|
|
595
1222
|
);
|
|
596
1223
|
});
|
|
597
|
-
ws.command("
|
|
1224
|
+
ws.command("clone").description("Clone a workspace's live state.").argument("<srcId>").option("--id <id>", "Explicit id for the clone").action(async (srcId, opts) => {
|
|
1225
|
+
const body = {};
|
|
1226
|
+
if (opts.id !== void 0) body.id = opts.id;
|
|
598
1227
|
const c = buildClient4();
|
|
599
1228
|
await c.ensureRunning({ allowSpawn: false });
|
|
600
|
-
const r = await c.request("
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const params = new URLSearchParams({ branch: opts.branch });
|
|
606
|
-
if (a !== void 0) params.set("a", a);
|
|
607
|
-
if (b !== void 0) params.set("b", b);
|
|
608
|
-
const c = buildClient4();
|
|
609
|
-
await c.ensureRunning({ allowSpawn: false });
|
|
610
|
-
const r = await c.request("GET", `/v1/workspaces/${id}/diff?${params.toString()}`);
|
|
611
|
-
emit(await handleResponse(r), formatDiff);
|
|
612
|
-
}
|
|
613
|
-
);
|
|
1229
|
+
const r = await c.request("POST", `/v1/workspaces/${encodeURIComponent(srcId)}/clone`, {
|
|
1230
|
+
body: JSON.stringify(body)
|
|
1231
|
+
});
|
|
1232
|
+
emit(await handleResponse(r), formatWorkspaceDetail);
|
|
1233
|
+
});
|
|
614
1234
|
ws.command("list-asks").description("List pending asks (every decision with --all).").argument("<id>").option("--session <sessionId>", "Only this session's asks").option("--all", "Include settled decisions, not just pending asks").action(async (id, opts) => {
|
|
615
1235
|
const params = new URLSearchParams();
|
|
616
|
-
if (opts.session !== void 0) params.set("
|
|
1236
|
+
if (opts.session !== void 0) params.set("session_id", opts.session);
|
|
617
1237
|
if (opts.all === true) params.set("all", "true");
|
|
618
1238
|
const c = buildClient4();
|
|
619
1239
|
await c.ensureRunning({ allowSpawn: false });
|
|
620
1240
|
const qs = params.toString();
|
|
621
|
-
const r = await c.request(
|
|
1241
|
+
const r = await c.request(
|
|
1242
|
+
"GET",
|
|
1243
|
+
`/v1/workspaces/${encodeURIComponent(id)}/asks${qs === "" ? "" : `?${qs}`}`
|
|
1244
|
+
);
|
|
622
1245
|
emit(await handleResponse(r), formatAsks);
|
|
623
1246
|
});
|
|
624
1247
|
ws.command("allow").description("Allow a pending ask; the retry of the asked line passes.").argument("<id>").argument("<askId>", "Ask id, as quoted in the refusal").option(
|
|
@@ -628,48 +1251,87 @@ function registerWorkspaceCommands(program) {
|
|
|
628
1251
|
).option("--note <note>", "What to record alongside the answer", "").action(async (id, askId, opts) => {
|
|
629
1252
|
const c = buildClient4();
|
|
630
1253
|
await c.ensureRunning({ allowSpawn: false });
|
|
631
|
-
const r = await c.request(
|
|
632
|
-
|
|
633
|
-
|
|
1254
|
+
const r = await c.request(
|
|
1255
|
+
"POST",
|
|
1256
|
+
`/v1/workspaces/${encodeURIComponent(id)}/asks/${encodeURIComponent(askId)}`,
|
|
1257
|
+
{
|
|
1258
|
+
body: JSON.stringify({ answer: "allow", scope: opts.scope, note: opts.note })
|
|
1259
|
+
}
|
|
1260
|
+
);
|
|
634
1261
|
emit(await handleResponse(r), (d) => `Allowed ${d.id} (${d.scope}).`);
|
|
635
1262
|
});
|
|
636
1263
|
ws.command("deny").description("Deny a pending ask; the retry is refused in the deny voice, once.").argument("<id>").argument("<askId>", "Ask id, as quoted in the refusal").option("--note <note>", "What to record alongside the answer", "").action(async (id, askId, opts) => {
|
|
637
1264
|
const c = buildClient4();
|
|
638
1265
|
await c.ensureRunning({ allowSpawn: false });
|
|
639
|
-
const r = await c.request(
|
|
640
|
-
|
|
641
|
-
|
|
1266
|
+
const r = await c.request(
|
|
1267
|
+
"POST",
|
|
1268
|
+
`/v1/workspaces/${encodeURIComponent(id)}/asks/${encodeURIComponent(askId)}`,
|
|
1269
|
+
{
|
|
1270
|
+
body: JSON.stringify({ answer: "deny", note: opts.note })
|
|
1271
|
+
}
|
|
1272
|
+
);
|
|
642
1273
|
emit(await handleResponse(r), (d) => `Denied ${d.id}.`);
|
|
643
1274
|
});
|
|
644
|
-
ws.command("checkout").description("Restore a workspace in place to one of its versions.").argument("<id>").argument("<ref>", "Version id or branch to restore").action(async (id, ref) => {
|
|
645
|
-
const c = buildClient4();
|
|
646
|
-
await c.ensureRunning({ allowSpawn: false });
|
|
647
|
-
const r = await c.request("POST", `/v1/workspaces/${id}/checkout`, {
|
|
648
|
-
body: JSON.stringify({ ref })
|
|
649
|
-
});
|
|
650
|
-
emit(await handleResponse(r), formatWorkspaceDetail);
|
|
651
|
-
});
|
|
652
1275
|
ws.command("snapshot").description(
|
|
653
|
-
"Snapshot a workspace
|
|
654
|
-
).argument("<id>").argument("
|
|
1276
|
+
"Snapshot a workspace. The server sends the tar back and it is written to <output> here, whether the server runs on this machine or another; with --key it goes to the server's snapshot store."
|
|
1277
|
+
).argument("<id>").argument("[output]", "File to write the .tar to, on this machine").option("--key <key>", "Put it in the server's snapshot store instead").action(async (id, output, opts) => {
|
|
1278
|
+
if (output !== void 0 && opts.key !== void 0) {
|
|
1279
|
+
fail("snapshot takes an output file or --key", 2);
|
|
1280
|
+
}
|
|
655
1281
|
const c = buildClient4();
|
|
656
1282
|
await c.ensureRunning({ allowSpawn: false });
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1283
|
+
const path = `/v1/workspaces/${encodeURIComponent(id)}/snapshot`;
|
|
1284
|
+
const human = (d, target2) => `Snapshot ${d.id} -> ${target2} (${d.size.toLocaleString()} bytes).`;
|
|
1285
|
+
if (opts.key !== void 0) {
|
|
1286
|
+
const key = opts.key;
|
|
1287
|
+
const r2 = await c.request("POST", path, {
|
|
1288
|
+
body: JSON.stringify({ key }),
|
|
1289
|
+
timeoutMs: null
|
|
1290
|
+
});
|
|
1291
|
+
emit(
|
|
1292
|
+
await handleResponse(r2),
|
|
1293
|
+
(d) => human(d, key)
|
|
1294
|
+
);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (output === void 0) fail("snapshot takes an output file or --key", 2);
|
|
1298
|
+
const target = output;
|
|
1299
|
+
const r = await c.request("GET", path, { timeoutMs: null });
|
|
1300
|
+
if (!r.ok) await handleResponse(r);
|
|
1301
|
+
const tar = new Uint8Array(await r.arrayBuffer());
|
|
1302
|
+
writeFileSync(target, tar);
|
|
1303
|
+
emit({ id, path: target, size: tar.byteLength }, (d) => human(d, target));
|
|
672
1304
|
});
|
|
1305
|
+
ws.command("load").description(
|
|
1306
|
+
"Load a workspace from a snapshot. <file> is read here and uploaded, whether the server runs on this machine or another; with --key the tar comes from the server's snapshot store."
|
|
1307
|
+
).argument("[file]", "The .tar to upload; omit with --key").argument("[config]", "Workspace YAML/JSON config").option("--key <key>", "Load from the server's snapshot store").option("--id <id>", "Explicit workspace id").action(
|
|
1308
|
+
async (first, second, opts) => {
|
|
1309
|
+
const given = [first, second].filter((p) => p !== void 0);
|
|
1310
|
+
if (opts.key !== void 0 && given.length > 1)
|
|
1311
|
+
fail("load takes a FILE or --key, not both", 2);
|
|
1312
|
+
if (opts.key === void 0 && given.length === 0) {
|
|
1313
|
+
fail("load takes a FILE (or --key) and an optional CONFIG", 2);
|
|
1314
|
+
}
|
|
1315
|
+
const tarPath = opts.key === void 0 ? given.shift() : void 0;
|
|
1316
|
+
const configPath = given[0];
|
|
1317
|
+
for (const p of [tarPath, configPath]) {
|
|
1318
|
+
if (p !== void 0 && !existsSync2(p)) fail(`file not found: ${p}`, 2);
|
|
1319
|
+
}
|
|
1320
|
+
const body = {};
|
|
1321
|
+
if (opts.id !== void 0) body.id = opts.id;
|
|
1322
|
+
if (configPath !== void 0) body.override = await loadConfigArgument(configPath);
|
|
1323
|
+
const c = buildClient4();
|
|
1324
|
+
await c.ensureRunning({ allowSpawn: true });
|
|
1325
|
+
const r = tarPath === void 0 ? await c.request("POST", "/v1/workspaces/load", {
|
|
1326
|
+
body: JSON.stringify({ ...body, key: opts.key }),
|
|
1327
|
+
timeoutMs: null
|
|
1328
|
+
}) : await c.requestUpload("POST", "/v1/workspaces/load", body, {
|
|
1329
|
+
name: "snapshot",
|
|
1330
|
+
data: createReadStream(tarPath)
|
|
1331
|
+
});
|
|
1332
|
+
emit(await handleResponse(r), formatWorkspaceDetail);
|
|
1333
|
+
}
|
|
1334
|
+
);
|
|
673
1335
|
}
|
|
674
1336
|
|
|
675
1337
|
// src/main.ts
|
|
@@ -677,7 +1339,7 @@ function resolveVersion() {
|
|
|
677
1339
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
678
1340
|
for (let i = 0; i < 6; i++) {
|
|
679
1341
|
try {
|
|
680
|
-
const pkg = JSON.parse(
|
|
1342
|
+
const pkg = JSON.parse(readFileSync3(join(dir, "package.json"), "utf8"));
|
|
681
1343
|
if (pkg.name === "@struktoai/mirage-cli" && pkg.version) return pkg.version;
|
|
682
1344
|
} catch {
|
|
683
1345
|
}
|
|
@@ -687,13 +1349,18 @@ function resolveVersion() {
|
|
|
687
1349
|
}
|
|
688
1350
|
function buildProgram() {
|
|
689
1351
|
const program = new Command();
|
|
690
|
-
program.name("mirage").description("Mirage daemon CLI: manage workspaces and
|
|
1352
|
+
program.name("mirage").description("Mirage daemon CLI: manage workspaces and run shell lines.").version(resolveVersion());
|
|
691
1353
|
registerWorkspaceCommands(program);
|
|
692
1354
|
registerSessionCommands(program);
|
|
693
1355
|
registerJobCommands(program);
|
|
694
|
-
|
|
1356
|
+
registerShellCommand(program);
|
|
1357
|
+
registerVfsCommands(program);
|
|
1358
|
+
registerToolCommands(program);
|
|
695
1359
|
registerMcpCommand(program);
|
|
696
|
-
|
|
1360
|
+
registerRpcCommand(program);
|
|
1361
|
+
registerSshProxyCommand(program);
|
|
1362
|
+
registerCodexExecCommand(program);
|
|
1363
|
+
registerLoginCommands(program);
|
|
697
1364
|
registerDaemonCommands(program);
|
|
698
1365
|
registerConfigCommands(program);
|
|
699
1366
|
return program;
|
|
@@ -702,7 +1369,8 @@ function buildProgram() {
|
|
|
702
1369
|
// src/bin/mirage.ts
|
|
703
1370
|
var ALIASES = {
|
|
704
1371
|
"--workspace_id": "--workspace",
|
|
705
|
-
"--session_id": "--session"
|
|
1372
|
+
"--session_id": "--session",
|
|
1373
|
+
"--background": "--bg"
|
|
706
1374
|
};
|
|
707
1375
|
function rewriteArgv(argv) {
|
|
708
1376
|
return argv.map((arg) => {
|
|
@@ -718,6 +1386,10 @@ buildProgram().parseAsync(rewriteArgv(process.argv)).catch((err) => {
|
|
|
718
1386
|
console.error(err.message);
|
|
719
1387
|
process.exit(2);
|
|
720
1388
|
}
|
|
1389
|
+
if (err instanceof DaemonUnreachable || err instanceof LoginError) {
|
|
1390
|
+
console.error(err.message);
|
|
1391
|
+
process.exit(1);
|
|
1392
|
+
}
|
|
721
1393
|
console.error(err);
|
|
722
1394
|
process.exit(1);
|
|
723
1395
|
});
|