@struktoai/mirage-cli 0.0.7-alpha.2 → 0.0.7

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.
@@ -1,35 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- emit,
4
- exitCodeFromResponse,
5
- fail,
6
- formatAge,
7
- formatTable,
3
+ DaemonUnreachable,
4
+ LoginError,
5
+ endsAt,
8
6
  getConfig,
9
- handleResponse,
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
- unsetConfig
16
- } from "../chunk-BNUV7A4V.js";
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 readFileSync4 } from "fs";
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, value) {
31
- if (key === "auth_token" && value !== "") return "***";
32
- return value;
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, [value, origin]] of Object.entries(resolved)) {
50
- payload[key] = { value: mask(key, value), origin };
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 value;
91
+ let value2;
80
92
  try {
81
- value = getConfig(key);
93
+ value2 = getConfig(key);
82
94
  } catch (e) {
83
95
  fail(e.message, 2);
84
96
  return;
85
97
  }
86
- if (value === void 0) {
98
+ if (value2 === void 0) {
87
99
  fail(`${key} is not set`, 1);
88
100
  return;
89
101
  }
90
- emit({ [key]: value }, () => value);
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, value) => {
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, value);
106
+ setConfig(key, value2);
95
107
  } catch (e) {
96
108
  fail(e.message, 2);
97
109
  return;
98
110
  }
99
- emit({ [key]: value, written: true }, () => `${key} = ${value}`);
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.uptimeS !== void 0) {
119
- parts.push(`uptime ${formatAge(Date.now() / 1e3 - d.uptimeS)}`);
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.spawnedFresh === true) return "Restarted (eager spawn).";
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 text = readFileSync(p, "utf-8").trim();
143
- const pid = Number(text);
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, spawnedFresh: true }, formatRestart);
242
+ emit({ restarted: true, spawned_fresh: true }, formatRestart);
231
243
  return;
232
244
  }
233
245
  emit(
234
246
  {
235
247
  restarted: true,
236
- spawnedFresh: false,
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/execute.ts
263
- import { readFileSync as readFileSync2 } from "fs";
264
- function registerExecuteCommand(program) {
265
- program.command("execute").description("Execute a command.").requiredOption("-w, --workspace <id>", "Workspace id").requiredOption("-c, --command <command>", "Shell command to execute").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").action(
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, provision: false };
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.bg !== true && !process.stdin.isTTY) {
272
- body.stdinBase64 = readFileSync2(0).toString("base64");
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 path = `/v1/workspaces/${opts.workspace}/execute` + (opts.bg === true ? "?background=true" : "");
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 response = await handleResponse(
278
- await c.request("POST", path, { body: JSON.stringify(body) })
279
- );
280
- emit(response);
281
- process.exit(exitCodeFromResponse(response));
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 ? `?workspaceId=${opts.workspace}` : "");
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(await c.request("GET", `/v1/jobs/${id}`));
525
+ const response = await handleResponse(
526
+ await c.request("GET", `/v1/jobs/${encodeURIComponent(id)}`)
527
+ );
302
528
  emit(response);
303
- process.exit(exitCodeFromResponse(response));
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.timeoutS = Number(opts.timeout);
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`, { body: JSON.stringify(body) })
537
+ await c.request("POST", `/v1/jobs/${encodeURIComponent(id)}/wait`, {
538
+ body: JSON.stringify(body)
539
+ })
312
540
  );
313
541
  emit(response);
314
- process.exit(exitCodeFromResponse(response));
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/mcp.ts
324
- import { serveMirageMcp } from "@struktoai/mirage-agents/mcp";
325
- import { buildWorkspaceFromConfig, resolveWorkspaceConfig } from "@struktoai/mirage-server";
326
- function resolveMcpConfig(config, options = {}) {
327
- return resolveWorkspaceConfig(config, {
328
- ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
329
- ...options.env !== void 0 ? { env: options.env } : {},
330
- envNames: ["MIRAGE_MCP_CONFIG", "MIRAGE_CONFIG"]
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 buildMcpWorkspace(configPath) {
334
- return buildWorkspaceFromConfig(configPath);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 runMcpServer(config, options) {
337
- const configPath = resolveMcpConfig(config);
338
- const workspace = await buildMcpWorkspace(configPath);
621
+ async function browserLogin(url) {
622
+ let found;
339
623
  try {
340
- await serveMirageMcp(workspace, {
341
- staleWriteProtection: options.staleWriteProtection
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
- await workspace.close();
345
- throw error;
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 registerMcpCommand(program) {
349
- program.command("mcp").argument("[config]", "Mirage workspace YAML config").option("--no-stale-write-protection", "allow edits after a file changed since it was read").description("Serve a Mirage workspace as MCP tools over stdio.").action(runMcpServer);
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
- // src/provision.ts
353
- function registerProvisionCommand(program) {
354
- program.command("provision").description("Estimate cost / preview a command without running it.").requiredOption("-w, --workspace <id>", "Workspace id").requiredOption("-c, --command <command>", "Shell command to estimate").option("-s, --session <id>", "Session id").action(async (opts) => {
355
- const body = { command: opts.command, provision: true };
356
- if (opts.session !== void 0) body.sessionId = opts.session;
357
- const c = makeClient(loadDaemonSettings());
358
- await c.ensureRunning({ allowSpawn: false });
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
- await handleResponse(
361
- await c.request("POST", `/v1/workspaces/${opts.workspace}/execute`, {
362
- body: JSON.stringify(body)
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
- (value, prev) => prev.concat([value]),
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.sessionId = opts.id;
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(await handleResponse(await c.request("GET", `/v1/workspaces/${wsId}/sessions`)));
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("DELETE", `/v1/workspaces/${wsId}/sessions/${sessionId}`)
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 readFileSync3 } from "fs";
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 envRecord() {
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 text = readFileSync3(path, "utf-8");
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(text), envRecord());
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.mountCount),
469
- String(w.sessionCount),
470
- formatAge(w.createdAt)
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.createdAt)} ago`
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.resource, m.mode]);
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", "RESOURCE", "MODE"], rows).split("\n")) {
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.sessionId, s.cwd]);
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 ["cacheBytes", "cacheEntries", "historyLength", "inFlightJobs"]) {
497
- const value = d.internals[k];
498
- lines.push(` ${k.padEnd(16)} ${value === null ? "n/a (not tracked)" : String(value)}`);
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.sessionId,
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 cfg = checkWorkspaceConfigFile(configPath);
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(await c.request("DELETE", `/v1/workspaces/${id}`)),
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("clone").description("Clone a workspace, optionally from one of its past versions.").argument("<srcId>").option("--id <id>", "Explicit id for the clone").option("--at <ref>", "Clone from a past version (id or branch) not the live state").action(async (srcId, opts) => {
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
- const r = await c.request("POST", "/v1/workspaces/clone", { body: JSON.stringify(body) });
573
- emit(await handleResponse(r), formatWorkspaceDetail);
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("commit").description("Commit the workspace's current state as a version.").argument("<id>").option("-m, --message <msg>", "Version message", "").option("-b, --branch <branch>", "Branch to commit on", "main").action(async (id, opts) => {
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(r),
583
- (d) => `Committed ${d.version.slice(0, 12)} on ${d.branch}.`
1210
+ await handleResponse(
1211
+ await c.request("POST", `/v1/workspaces/${encodeURIComponent(id)}/cancel`)
1212
+ )
584
1213
  );
585
1214
  });
586
- ws.command("branch").description("Create a branch at another branch's current version.").argument("<id>").argument("<name>").option("--from <branch>", "Branch to fork from", "main").action(async (id, name, opts) => {
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(r),
594
- (d) => `Created branch ${d.branch} at ${d.version.slice(0, 12)}.`
1219
+ await handleResponse(
1220
+ await c.request("POST", `/v1/workspaces/${encodeURIComponent(id)}/kill`)
1221
+ )
595
1222
  );
596
1223
  });
597
- ws.command("log").description("List a workspace's versions (newest first).").argument("<id>").option("-b, --branch <branch>", "Branch", "main").action(async (id, opts) => {
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("GET", `/v1/workspaces/${id}/versions?branch=${opts.branch}`);
601
- emit(await handleResponse(r), formatVersionLog);
602
- });
603
- ws.command("diff").description("Show changed files (git-style): live vs HEAD, live vs <a>, or <a> vs <b>.").argument("<id>").argument("[a]", "Base ref; omit to use live state").argument("[b]", "Compare ref; omit to use live state").option("-b, --branch <branch>", "Branch", "main").action(
604
- async (id, a, b, opts) => {
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("sessionId", opts.session);
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("GET", `/v1/workspaces/${id}/asks${qs === "" ? "" : `?${qs}`}`);
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("POST", `/v1/workspaces/${id}/asks/${askId}`, {
632
- body: JSON.stringify({ answer: "allow", scope: opts.scope, note: opts.note })
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("POST", `/v1/workspaces/${id}/asks/${askId}`, {
640
- body: JSON.stringify({ answer: "deny", note: opts.note })
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 to a tar file. The path is resolved to an absolute path and the daemon writes the tar."
654
- ).argument("<id>").argument("<output>", "Path to write the .tar to").action(async (id, output) => {
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 r = await c.request("POST", `/v1/workspaces/${id}/snapshot`, {
658
- body: JSON.stringify({ path: resolve(output) })
659
- });
660
- const d = await handleResponse(r);
661
- emit(d, (x) => `Snapshot ${x.id} -> ${x.path} (${x.size.toLocaleString()} bytes).`);
662
- });
663
- ws.command("load").description("Load a workspace from a tar file.").argument("<tar>", "Path to a .tar produced by `mirage workspace snapshot`").argument("[config]", "Workspace YAML/JSON config").option("--id <id>", "Explicit workspace id").action(async (tarPath, configPath, opts) => {
664
- if (!existsSync2(tarPath)) fail(`tar file not found: ${tarPath}`, 2);
665
- const body = { path: resolve(tarPath) };
666
- if (opts.id !== void 0) body.id = opts.id;
667
- if (configPath !== void 0) body.override = loadConfigArgument(configPath);
668
- const c = buildClient4();
669
- await c.ensureRunning({ allowSpawn: true });
670
- const r = await c.request("POST", "/v1/workspaces/load", { body: JSON.stringify(body) });
671
- emit(await handleResponse(r), formatWorkspaceDetail);
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(readFileSync4(join(dir, "package.json"), "utf8"));
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 execute commands.").version(resolveVersion());
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
- registerExecuteCommand(program);
1356
+ registerShellCommand(program);
1357
+ registerVfsCommands(program);
1358
+ registerToolCommands(program);
695
1359
  registerMcpCommand(program);
696
- registerProvisionCommand(program);
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
  });