@sealant/mend 0.31.0 → 0.32.1

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.
Files changed (77) hide show
  1. package/dist/main.js +1682 -878
  2. package/man/mend-accounts.1 +1 -1
  3. package/man/mend-adopt.1 +1 -1
  4. package/man/mend-attach.1 +1 -1
  5. package/man/mend-codex.1 +1 -1
  6. package/man/mend-completions.1 +1 -1
  7. package/man/mend-connect.1 +1 -1
  8. package/man/mend-continue.1 +1 -1
  9. package/man/mend-doctor.1 +28 -3
  10. package/man/mend-dotfiles-repo.1 +1 -1
  11. package/man/mend-dotfiles-sync.1 +1 -1
  12. package/man/mend-dotfiles.1 +1 -1
  13. package/man/mend-env-cluster.1 +1 -1
  14. package/man/mend-env-load.1 +1 -1
  15. package/man/mend-env-show.1 +1 -1
  16. package/man/mend-folder-create.1 +1 -1
  17. package/man/mend-folder-list.1 +1 -1
  18. package/man/mend-folder-push.1 +1 -1
  19. package/man/mend-folder-rm.1 +1 -1
  20. package/man/mend-git-author.1 +1 -1
  21. package/man/mend-help.1 +1 -1
  22. package/man/mend-invite.1 +1 -1
  23. package/man/mend-keys-init.1 +1 -1
  24. package/man/mend-keys-mode.1 +1 -1
  25. package/man/mend-keys-share.1 +1 -1
  26. package/man/mend-keys-show.1 +1 -1
  27. package/man/mend-land.1 +1 -1
  28. package/man/mend-login.1 +1 -1
  29. package/man/mend-logout.1 +1 -1
  30. package/man/mend-man.1 +1 -1
  31. package/man/mend-members.1 +1 -1
  32. package/man/mend-operator-exposure.1 +1 -1
  33. package/man/mend-operator-gate.1 +1 -1
  34. package/man/mend-operator-grant-owner.1 +1 -1
  35. package/man/mend-operator-org-create.1 +1 -1
  36. package/man/mend-operator-org-invite-owner.1 +1 -1
  37. package/man/mend-operator-org-list.1 +1 -1
  38. package/man/mend-operator-org-rename.1 +1 -1
  39. package/man/mend-operator-reset-link.1 +1 -1
  40. package/man/mend-pair.1 +1 -1
  41. package/man/mend-projects.1 +1 -1
  42. package/man/mend-pull.1 +1 -1
  43. package/man/mend-refresh.1 +1 -1
  44. package/man/mend-rejoin.1 +1 -1
  45. package/man/mend-resume.1 +1 -1
  46. package/man/mend-run.1 +1 -1
  47. package/man/mend-server-logs.1 +1 -1
  48. package/man/mend-server-restart.1 +1 -1
  49. package/man/mend-server-setup.1 +1 -1
  50. package/man/mend-server-start.1 +1 -1
  51. package/man/mend-server-status.1 +1 -1
  52. package/man/mend-server-stop.1 +1 -1
  53. package/man/mend-server-upgrade.1 +1 -1
  54. package/man/mend-server.1 +1 -1
  55. package/man/mend-service-add.1 +1 -1
  56. package/man/mend-service-connect.1 +1 -1
  57. package/man/mend-service-init.1 +1 -1
  58. package/man/mend-service-list.1 +1 -1
  59. package/man/mend-service-logs.1 +1 -1
  60. package/man/mend-service-restart.1 +1 -1
  61. package/man/mend-service-run.1 +1 -1
  62. package/man/mend-service-stop.1 +1 -1
  63. package/man/mend-session-share.1 +1 -1
  64. package/man/mend-sessions.1 +1 -1
  65. package/man/mend-shell.1 +1 -1
  66. package/man/mend-skills-push.1 +1 -1
  67. package/man/mend-skills.1 +1 -1
  68. package/man/mend-snake.1 +1 -1
  69. package/man/mend-ssh-setup.1 +1 -1
  70. package/man/mend-ssh.1 +1 -1
  71. package/man/mend-stop.1 +1 -1
  72. package/man/mend-ui.1 +1 -1
  73. package/man/mend-uninstall.1 +1 -1
  74. package/man/mend-version.1 +1 -1
  75. package/man/mend-worktrees.1 +1 -1
  76. package/man/mend.1 +2 -2
  77. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -31,11 +31,11 @@ import {
31
31
 
32
32
  // src/main.ts
33
33
  import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
34
- import * as fs12 from "node:fs";
34
+ import * as fs14 from "node:fs";
35
35
  import { createRequire } from "node:module";
36
36
  import * as net4 from "node:net";
37
- import * as os8 from "node:os";
38
- import * as path13 from "node:path";
37
+ import * as os9 from "node:os";
38
+ import * as path15 from "node:path";
39
39
 
40
40
  // src/agent-share.ts
41
41
  import * as net from "node:net";
@@ -43,7 +43,7 @@ import { setTimeout as delay } from "node:timers/promises";
43
43
  var retryDelayMs = (attempt2) => Math.min(3e4, 1e3 * 2 ** Math.min(Math.max(attempt2 - 1, 0), 5));
44
44
  var SSH_AGENTC_REQUEST_IDENTITIES = 11;
45
45
  var SSH_AGENTC_SIGN_REQUEST = 13;
46
- var askLocalAgent = (agentSock, payload) => new Promise((resolve6, reject) => {
46
+ var askLocalAgent = (agentSock, payload) => new Promise((resolve7, reject) => {
47
47
  const connection = net.connect(agentSock, () => {
48
48
  connection.write(payload);
49
49
  });
@@ -57,7 +57,7 @@ var askLocalAgent = (agentSock, payload) => new Promise((resolve6, reject) => {
57
57
  if (pending.length >= 4 && pending.length >= 4 + pending.readUInt32BE(0)) {
58
58
  clearTimeout(timer);
59
59
  connection.end();
60
- resolve6(pending.subarray(0, 4 + pending.readUInt32BE(0)));
60
+ resolve7(pending.subarray(0, 4 + pending.readUInt32BE(0)));
61
61
  }
62
62
  });
63
63
  connection.on("error", (error) => {
@@ -87,12 +87,12 @@ var shareAgent = async (options) => {
87
87
  const ws = new WebSocket(target);
88
88
  const onAbort = () => ws.close();
89
89
  signal.addEventListener("abort", onAbort, { once: true });
90
- const closed = new Promise((resolve6) => {
91
- ws.addEventListener("close", () => resolve6(), { once: true });
90
+ const closed = new Promise((resolve7) => {
91
+ ws.addEventListener("close", () => resolve7(), { once: true });
92
92
  });
93
- const opened = await new Promise((resolve6) => {
94
- ws.addEventListener("open", () => resolve6(true), { once: true });
95
- ws.addEventListener("error", () => resolve6(false), { once: true });
93
+ const opened = await new Promise((resolve7) => {
94
+ ws.addEventListener("open", () => resolve7(true), { once: true });
95
+ ws.addEventListener("error", () => resolve7(false), { once: true });
96
96
  });
97
97
  if (opened) {
98
98
  attempt2 = 0;
@@ -110,19 +110,19 @@ var shareAgent = async (options) => {
110
110
  }
111
111
  const id = frame.id;
112
112
  const payload = Buffer.from(frame.payload, "base64");
113
- const type = payload[4];
113
+ const type2 = payload[4];
114
114
  const context = typeof frame.context === "string" ? frame.context : "mend";
115
115
  chain = chain.then(async () => {
116
116
  const started = Date.now();
117
- if (type === SSH_AGENTC_SIGN_REQUEST) onEvent({ kind: "sign-requested", context });
118
- else if (type === SSH_AGENTC_REQUEST_IDENTITIES) {
117
+ if (type2 === SSH_AGENTC_SIGN_REQUEST) onEvent({ kind: "sign-requested", context });
118
+ else if (type2 === SSH_AGENTC_REQUEST_IDENTITIES) {
119
119
  onEvent({ kind: "identities-requested", context });
120
120
  }
121
121
  try {
122
122
  const response = await askLocalAgent(agentSock, payload);
123
123
  if (ws.readyState !== WebSocket.OPEN) return null;
124
124
  ws.send(JSON.stringify({ t: "res", id, payload: response.toString("base64") }));
125
- if (type === SSH_AGENTC_SIGN_REQUEST) {
125
+ if (type2 === SSH_AGENTC_SIGN_REQUEST) {
126
126
  onEvent({ kind: "signed", context, seconds: (Date.now() - started) / 1e3 });
127
127
  }
128
128
  } catch (error) {
@@ -130,7 +130,7 @@ var shareAgent = async (options) => {
130
130
  if (ws.readyState === WebSocket.OPEN) {
131
131
  ws.send(JSON.stringify({ t: "err", id, message }));
132
132
  }
133
- if (type === SSH_AGENTC_SIGN_REQUEST) onEvent({ kind: "not-signed", context, message });
133
+ if (type2 === SSH_AGENTC_SIGN_REQUEST) onEvent({ kind: "not-signed", context, message });
134
134
  }
135
135
  return null;
136
136
  });
@@ -243,8 +243,8 @@ var readGrant = (configDir, platform = process.platform) => {
243
243
  import { execFile } from "node:child_process";
244
244
  var IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
245
245
  var pickImageType = (offered) => {
246
- const types = offered.split(/\s+/).map((type) => type.trim().toLowerCase());
247
- for (const type of IMAGE_TYPES) if (types.includes(type)) return type;
246
+ const types = offered.split(/\s+/).map((type2) => type2.trim().toLowerCase());
247
+ for (const type2 of IMAGE_TYPES) if (types.includes(type2)) return type2;
248
248
  return null;
249
249
  };
250
250
  var parsePngfHex = (output) => {
@@ -253,13 +253,13 @@ var parsePngfHex = (output) => {
253
253
  return Buffer.from(match[1], "hex");
254
254
  };
255
255
  var MAX_BYTES = 32 * 1024 * 1024;
256
- var run = (command, args) => new Promise((resolve6) => {
256
+ var run = (command, args) => new Promise((resolve7) => {
257
257
  execFile(
258
258
  command,
259
259
  [...args],
260
260
  { encoding: "buffer", maxBuffer: MAX_BYTES, timeout: 3e3 },
261
261
  (error, stdout) => {
262
- resolve6(error === null ? stdout : null);
262
+ resolve7(error === null ? stdout : null);
263
263
  }
264
264
  );
265
265
  });
@@ -296,465 +296,736 @@ var readClipboardImage = async (platform = process.platform, env = process.env)
296
296
  return null;
297
297
  };
298
298
 
299
- // src/doctor.ts
299
+ // src/doctor-bundle-collectors.ts
300
+ import * as fs3 from "node:fs";
301
+ import * as os2 from "node:os";
302
+ import * as path3 from "node:path";
303
+
304
+ // src/server-runtime.ts
305
+ import { spawn } from "node:child_process";
300
306
  import * as fs2 from "node:fs";
301
307
  import * as path2 from "node:path";
302
- var TIMEOUT_MS = 3e3;
303
- var MARKS = { ok: "\u2713", todo: "\u25CB", failed: "\u2717" };
304
- var LABEL_WIDTH = 11;
305
- var day = (at) => at.toISOString().slice(0, 10);
306
- var formatCheck = (check, paint6 = (_state, mark) => mark) => `${paint6(check.state, MARKS[check.state])} ${check.label.padEnd(LABEL_WIDTH)} ${check.detail}${check.fix === null ? "" : ` \u2192 ${check.fix}`}`;
307
- var getJson = async (config, route) => {
308
- try {
309
- const response = await fetch(`${config.url}/api${route}`, {
310
- headers: config.token === null ? {} : { authorization: `Bearer ${config.token}` },
311
- signal: AbortSignal.timeout(TIMEOUT_MS)
308
+ var serverProcessDeadlines = {
309
+ ordinary: 3e4,
310
+ pull: 10 * 6e4,
311
+ startup: 3 * 6e4,
312
+ stop: 9e4,
313
+ dump: 15 * 6e4,
314
+ composeWaitSeconds: 120
315
+ };
316
+ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
317
+ const timeoutMs = options?.timeoutMs ?? serverProcessDeadlines.ordinary;
318
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
319
+ return Promise.resolve({
320
+ status: null,
321
+ stdout: "",
322
+ stderr: "",
323
+ error: "Invalid process timeout"
312
324
  });
313
- if (!response.ok) return { value: null, status: response.status };
314
- return { value: await response.json(), status: response.status };
315
- } catch {
316
- return { value: null, status: null };
317
325
  }
326
+ if (options.stdoutFile !== void 0 && options.stdout === "inherit") {
327
+ return Promise.resolve({
328
+ status: null,
329
+ stdout: "",
330
+ stderr: "",
331
+ error: "Invalid process output options"
332
+ });
333
+ }
334
+ const env = {};
335
+ for (const key of [
336
+ "PATH",
337
+ "HOME",
338
+ "USER",
339
+ "LOGNAME",
340
+ "XDG_CONFIG_HOME",
341
+ "XDG_RUNTIME_DIR",
342
+ "DOCKER_CONFIG",
343
+ "SSH_AUTH_SOCK",
344
+ "TMPDIR",
345
+ "TMP",
346
+ "TEMP"
347
+ ]) {
348
+ if (parentEnvironment[key] !== void 0) env[key] = parentEnvironment[key];
349
+ }
350
+ return new Promise((resolve7) => {
351
+ let fd;
352
+ try {
353
+ if (options.stdoutFile !== void 0) fd = fs2.openSync(options.stdoutFile, "wx", 384);
354
+ const child = spawn(command, [...args], {
355
+ env,
356
+ detached: process.platform !== "win32",
357
+ stdio: ["ignore", fd ?? (options.stdout === "inherit" ? "inherit" : "pipe"), "pipe"]
358
+ });
359
+ let stdout = "";
360
+ let stderr = "";
361
+ let stdoutBytes = 0;
362
+ let stderrBytes = 0;
363
+ let failure2;
364
+ const terminate = () => {
365
+ if (process.platform === "win32" || child.pid === void 0) {
366
+ child.kill("SIGKILL");
367
+ return;
368
+ }
369
+ try {
370
+ process.kill(-child.pid, "SIGKILL");
371
+ } catch (cause) {
372
+ if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) {
373
+ failure2 = "Could not terminate the process group; retain the installation lock until its processes stop.";
374
+ child.kill("SIGKILL");
375
+ }
376
+ }
377
+ };
378
+ child.stdout?.on("data", (chunk) => {
379
+ stdoutBytes += chunk.length;
380
+ if (stdoutBytes <= 4 * 1024 * 1024) stdout += chunk.toString();
381
+ else {
382
+ failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
383
+ terminate();
384
+ }
385
+ });
386
+ child.stderr?.on("data", (chunk) => {
387
+ stderrBytes += chunk.length;
388
+ if (stderrBytes <= 64 * 1024) stderr += chunk.toString();
389
+ else {
390
+ failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
391
+ terminate();
392
+ }
393
+ });
394
+ const timer = setTimeout(() => {
395
+ failure2 ??= `Process timed out after ${timeoutMs}ms`;
396
+ terminate();
397
+ }, timeoutMs);
398
+ child.once("error", (error) => {
399
+ failure2 ??= error.message;
400
+ });
401
+ child.once("close", (status) => {
402
+ globalThis.clearTimeout(timer);
403
+ try {
404
+ if (fd !== void 0) fs2.fsyncSync(fd);
405
+ } catch {
406
+ failure2 ??= "Could not durably write process output.";
407
+ } finally {
408
+ try {
409
+ if (fd !== void 0) fs2.closeSync(fd);
410
+ } catch {
411
+ failure2 ??= "Could not close process output file.";
412
+ }
413
+ }
414
+ resolve7({
415
+ status: failure2 === void 0 ? status : null,
416
+ stdout,
417
+ stderr,
418
+ ...failure2 === void 0 ? {} : { error: failure2 }
419
+ });
420
+ });
421
+ } catch (cause) {
422
+ try {
423
+ if (fd !== void 0) fs2.closeSync(fd);
424
+ } catch {
425
+ }
426
+ resolve7({
427
+ status: null,
428
+ stdout: "",
429
+ stderr: "",
430
+ error: cause instanceof Error ? cause.message : "Could not start process."
431
+ });
432
+ }
433
+ });
318
434
  };
319
- var exposureCheck = (exposure) => {
320
- const beyondMachine = exposure.declared !== "loopback" && exposure.originOnMachine !== true;
321
- const gateOpen = beyondMachine ? exposure.gateOpen : 0;
322
- const facts = [
323
- `declared ${exposure.declared}`,
324
- `${exposure.originScheme} origin`,
325
- ...exposure.arrivedVia === "trusted-proxy" ? ["arrived via a trusted proxy"] : [],
326
- ...gateOpen === 0 ? [] : [`${gateOpen} gate items open`]
327
- ].join(" \xB7 ");
328
- const plainBeyondMachine = beyondMachine && exposure.originScheme === "http";
329
- return {
330
- label: "exposure",
331
- state: plainBeyondMachine ? "todo" : "ok",
332
- detail: facts,
333
- fix: plainBeyondMachine ? "serve it over https and set APP_URL to that origin" : gateOpen === 0 ? null : "mend operator exposure"
435
+ var serverComposeArgs = (installation, command) => [
436
+ "--context",
437
+ installation.dockerContext,
438
+ "compose",
439
+ "--project-name",
440
+ "mend",
441
+ "--project-directory",
442
+ installation.directory,
443
+ "--env-file",
444
+ path2.join(installation.directory, "server.env"),
445
+ "-f",
446
+ path2.join(installation.directory, "compose.yaml"),
447
+ ...command
448
+ ];
449
+
450
+ // src/doctor-bundle-collectors.ts
451
+ var json = (value) => `${JSON.stringify(value, null, 2)}
452
+ `;
453
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
454
+ var field = (value, key) => isRecord(value) ? value[key] : void 0;
455
+ var stringField = (value, key) => {
456
+ const found = field(value, key);
457
+ return typeof found === "string" ? found : null;
458
+ };
459
+ var stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
460
+ var outputOrFailure = (output) => {
461
+ if (output.error !== void 0) return `failed: ${output.error}`;
462
+ if (output.status !== 0)
463
+ return `exit ${output.status ?? "unknown"}
464
+ ${output.stderr.trim() || output.stdout.trim()}`;
465
+ return output.stdout.trimEnd();
466
+ };
467
+ var failedOutput = (output, what) => new Error(
468
+ `${what}: ${output.error ?? `exit ${output.status ?? "unknown"}`}${output.stderr.trim() === "" ? "" : `
469
+ ${output.stderr.trim()}`}`
470
+ );
471
+ var lastLines = (text, count) => {
472
+ const lines = text.split("\n");
473
+ return lines.length <= count ? text : lines.slice(lines.length - count).join("\n");
474
+ };
475
+ var mapLimit = async (items, limit, work) => {
476
+ const results = [];
477
+ let next = 0;
478
+ const worker = async () => {
479
+ for (; ; ) {
480
+ const index = next;
481
+ next += 1;
482
+ if (index >= items.length) return;
483
+ const item = items[index];
484
+ if (item === void 0) return;
485
+ results[index] = await work(item);
486
+ }
334
487
  };
488
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
489
+ return results;
335
490
  };
336
- var LOGIN_COMMANDS = {
337
- claude: "claude setup-token",
338
- codex: "codex login",
339
- github: "gh auth login"
340
- };
341
- var HARNESS_CLIS = [
342
- { command: "claude", provider: "claude" },
343
- { command: "codex", provider: "codex" },
344
- { command: "gh", provider: "github" }
345
- ];
346
- var notChecked = (label) => ({
347
- label,
348
- state: "todo",
349
- detail: "not checked",
350
- fix: null
351
- });
352
- var identityOf = (account) => {
353
- const meta = account.metadata;
354
- for (const key of ["login", "email", "accountEmail", "accountId"]) {
355
- const value = meta[key];
356
- if (typeof value === "string" && value !== "") return value;
491
+ var pathOf = (command, env = process.env) => {
492
+ for (const directory of (env["PATH"] ?? "").split(path3.delimiter)) {
493
+ if (directory === "") continue;
494
+ const candidate = path3.join(directory, command);
495
+ try {
496
+ fs3.accessSync(candidate, fs3.constants.X_OK);
497
+ return candidate;
498
+ } catch {
499
+ }
357
500
  }
358
501
  return null;
359
502
  };
360
- var runChecks = async (config, probes) => {
361
- const checks = [];
362
- const health = await getJson(config, "/health");
363
- checks.push(
364
- health.value === null ? {
365
- label: "server",
366
- state: "failed",
367
- detail: `cannot reach ${config.url}`,
368
- fix: "start the Mend server"
369
- } : {
370
- label: "server",
371
- state: "ok",
372
- detail: `${config.url} \xB7 mend ${health.value.version}`,
373
- fix: null
503
+ var cliCollector = (deps) => ({
504
+ name: "cli",
505
+ collect: async () => [
506
+ {
507
+ path: "cli.json",
508
+ content: json({
509
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
510
+ cliVersion: deps.cliVersion,
511
+ node: process.versions.node,
512
+ platform: process.platform,
513
+ arch: process.arch,
514
+ osType: os2.type(),
515
+ osRelease: os2.release(),
516
+ hostname: os2.hostname(),
517
+ term: deps.env["TERM"] ?? null,
518
+ shell: deps.env["SHELL"] ?? null,
519
+ lang: deps.env["LANG"] ?? null,
520
+ stdinTty: deps.stdinTty,
521
+ stdoutTty: deps.stdoutTty,
522
+ serverUrl: deps.serverUrl,
523
+ configuredUrl: deps.configuredUrl,
524
+ deviceId: deps.deviceId,
525
+ tokenSaved: deps.tokenSaved,
526
+ mendEnvNames: Object.keys(deps.env).filter((name) => name.startsWith("MEND_")).toSorted()
527
+ })
374
528
  }
375
- );
376
- const projects = health.value !== null && config.token !== null ? await getJson(config, "/projects") : null;
377
- if (health.value === null) checks.push(notChecked("signed in"));
378
- else if (config.token === null) {
379
- checks.push({
380
- label: "signed in",
381
- state: "failed",
382
- detail: "no token saved",
383
- fix: "mend login"
384
- });
385
- } else if (projects !== null && projects.value !== null) {
386
- checks.push({ label: "signed in", state: "ok", detail: "token accepted", fix: null });
387
- } else {
388
- const status = projects === null ? null : projects.status;
389
- checks.push(
390
- status === 401 || status === 403 ? { label: "signed in", state: "failed", detail: "token rejected", fix: "mend login" } : {
391
- label: "signed in",
392
- state: "failed",
393
- detail: `GET /projects \u2192 ${status ?? "no answer"}`,
394
- fix: null
395
- }
529
+ ]
530
+ });
531
+ var doctorCollector = (deps) => ({
532
+ name: "doctor",
533
+ collect: async () => [{ path: "doctor.txt", content: await deps.doctor() }]
534
+ });
535
+ var serverHealthCollector = (deps) => ({
536
+ name: "server-health",
537
+ collect: async () => [
538
+ { path: "server-health.json", content: json(await deps.get("/health")) }
539
+ ]
540
+ });
541
+ var NO_SERVER = "no Mend server is installed on this machine (mend server setup installs one)";
542
+ var serverFactsOnce = (deps) => {
543
+ let pending = null;
544
+ return () => {
545
+ pending ??= deps.readServer();
546
+ return pending;
547
+ };
548
+ };
549
+ var serverConfigCollector = (facts) => ({
550
+ name: "server-config",
551
+ collect: async () => {
552
+ const server = await facts();
553
+ if (server === null) throw new Error(NO_SERVER);
554
+ return [
555
+ {
556
+ path: "server-config.json",
557
+ content: json({
558
+ directory: server.directory,
559
+ config: server.config,
560
+ envKeys: server.envKeys
561
+ })
562
+ },
563
+ { path: "server-compose.yaml", content: server.compose }
564
+ ];
565
+ }
566
+ });
567
+ var COMPOSE_TIMEOUT_MS = 3e4;
568
+ var serverLogsCollector = (deps, facts) => ({
569
+ name: "server-logs",
570
+ collect: async () => {
571
+ const server = await facts();
572
+ if (server === null) throw new Error(NO_SERVER);
573
+ const installation = {
574
+ directory: server.directory,
575
+ dockerContext: server.config.dockerContext
576
+ };
577
+ const listed = await deps.run(
578
+ "docker",
579
+ serverComposeArgs(installation, ["ps", "--all", "--services"]),
580
+ COMPOSE_TIMEOUT_MS
396
581
  );
582
+ if (listed.status !== 0 || listed.error !== void 0)
583
+ throw failedOutput(listed, "docker compose ps failed");
584
+ const services = listed.stdout.split(/\s+/).filter((name) => name !== "");
585
+ if (services.length === 0) throw new Error("no Compose containers found (mend server start)");
586
+ const files = [];
587
+ for (const service of services) {
588
+ const logs = await deps.run(
589
+ "docker",
590
+ serverComposeArgs(installation, [
591
+ "logs",
592
+ "--no-color",
593
+ "--tail",
594
+ String(deps.tail),
595
+ service
596
+ ]),
597
+ COMPOSE_TIMEOUT_MS
598
+ );
599
+ const text = logs.error !== void 0 || logs.status !== 0 ? outputOrFailure(logs) : `${logs.stdout.trimEnd()}${logs.stderr.trim() === "" ? "" : `
600
+ --- stderr ---
601
+ ${logs.stderr.trimEnd()}`}`;
602
+ files.push({ path: `server-logs/${service}.log`, content: `${text}
603
+ ` });
604
+ }
605
+ return files;
397
606
  }
398
- const signedIn = projects !== null && projects.value !== null;
399
- const connection = signedIn ? await getJson(config, "/sealant/connection") : null;
400
- if (connection === null) checks.push(notChecked("sealant"));
401
- else if (connection.value === null) {
402
- checks.push(
403
- connection.status === 404 ? notChecked("sealant") : {
404
- label: "sealant",
405
- state: "failed",
406
- detail: `GET /sealant/connection \u2192 ${connection.status ?? "no answer"}`,
407
- fix: null
408
- }
409
- );
410
- } else if (connection.value.status === "connected") {
411
- checks.push({
412
- label: "sealant",
413
- state: "ok",
414
- detail: `connected \xB7 ${connection.value.baseUrl}`,
415
- fix: null
416
- });
417
- } else {
418
- checks.push({
419
- label: "sealant",
420
- state: "failed",
421
- detail: `${connection.value.status} \xB7 ${connection.value.detail ?? connection.value.baseUrl}`,
422
- fix: null
423
- });
607
+ });
608
+ var DOCKER_TIMEOUT_MS = 2e4;
609
+ var CONTAINER_FILTERS = [
610
+ ["--filter", "label=com.docker.compose.project=mend"],
611
+ ["--filter", "name=sealant-"],
612
+ ["--filter", "label=sealant.workspace"]
613
+ ];
614
+ var parseContainerRows = (stdout) => stdout.split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
615
+ let parsed;
616
+ try {
617
+ parsed = JSON.parse(line);
618
+ } catch {
619
+ return [];
424
620
  }
425
- const identity = signedIn ? await getJson(config, "/me/sealant") : null;
426
- const platform = identity === null ? null : identity.value;
427
- for (const { provider } of HARNESS_CLIS) {
428
- if (platform === null) {
429
- checks.push(notChecked(provider));
430
- continue;
621
+ const id = stringField(parsed, "ID");
622
+ if (id === null) return [];
623
+ return [
624
+ {
625
+ id,
626
+ name: stringField(parsed, "Names") ?? id,
627
+ image: stringField(parsed, "Image") ?? "",
628
+ state: stringField(parsed, "State") ?? "",
629
+ status: stringField(parsed, "Status") ?? "",
630
+ labels: stringField(parsed, "Labels") ?? ""
431
631
  }
432
- const account = platform.accounts.find((row) => row.provider === provider && row.name === "default") ?? platform.accounts.find((row) => row.provider === provider);
433
- if (account === void 0) {
434
- checks.push({
435
- label: provider,
436
- state: "todo",
437
- detail: "not connected",
438
- fix: `mend connect ${provider}`
439
- });
440
- continue;
632
+ ];
633
+ });
634
+ var dockerFactsOnce = (deps, facts) => {
635
+ let pending = null;
636
+ const read = async () => {
637
+ let context = [];
638
+ const notes = [];
639
+ try {
640
+ const server = await facts();
641
+ if (server !== null) context = ["--context", server.config.dockerContext];
642
+ } catch (error) {
643
+ notes.push(`server config: ${error instanceof Error ? error.message : String(error)}`);
644
+ }
645
+ const version = await deps.run("docker", [...context, "version"], DOCKER_TIMEOUT_MS);
646
+ if (version.error !== void 0 || version.status !== 0)
647
+ throw failedOutput(version, "docker version failed");
648
+ const info = await deps.run("docker", [...context, "info"], DOCKER_TIMEOUT_MS);
649
+ const contexts = await deps.run("docker", ["context", "ls"], DOCKER_TIMEOUT_MS);
650
+ const byId = /* @__PURE__ */ new Map();
651
+ for (const filter of CONTAINER_FILTERS) {
652
+ const listed = await deps.run(
653
+ "docker",
654
+ [...context, "ps", "--all", "--no-trunc", "--format", "{{json .}}", ...filter],
655
+ DOCKER_TIMEOUT_MS
656
+ );
657
+ if (listed.error !== void 0 || listed.status !== 0) {
658
+ notes.push(`docker ps ${filter.join(" ")}: ${outputOrFailure(listed)}`);
659
+ continue;
660
+ }
661
+ for (const row of parseContainerRows(listed.stdout)) byId.set(row.id, row);
441
662
  }
442
- const who = identityOf(account);
443
- checks.push(
444
- account.status === "active" ? {
445
- label: provider,
446
- state: "ok",
447
- detail: who === null ? "connected" : `connected \xB7 ${who}`,
448
- fix: null
449
- } : {
450
- label: provider,
451
- state: "todo",
452
- detail: account.status,
453
- fix: `mend connect ${provider}`
663
+ return {
664
+ context,
665
+ version: outputOrFailure(version),
666
+ info: outputOrFailure(info),
667
+ contexts: outputOrFailure(contexts),
668
+ containers: [...byId.values()].toSorted((a, b) => a.name.localeCompare(b.name)),
669
+ notes
670
+ };
671
+ };
672
+ return () => {
673
+ pending ??= read();
674
+ return pending;
675
+ };
676
+ };
677
+ var summarizeInspect = (container) => {
678
+ const config = field(container, "Config");
679
+ const hostConfig = field(container, "HostConfig");
680
+ const state = field(container, "State");
681
+ const mounts = field(container, "Mounts");
682
+ return {
683
+ id: stringField(container, "Id"),
684
+ name: stringField(container, "Name"),
685
+ created: stringField(container, "Created"),
686
+ image: stringField(config, "Image"),
687
+ imageId: stringField(container, "Image"),
688
+ state: isRecord(state) ? {
689
+ status: state["Status"],
690
+ exitCode: state["ExitCode"],
691
+ oomKilled: state["OOMKilled"],
692
+ startedAt: state["StartedAt"],
693
+ finishedAt: state["FinishedAt"],
694
+ error: state["Error"]
695
+ } : null,
696
+ privileged: field(hostConfig, "Privileged"),
697
+ runtime: field(hostConfig, "Runtime"),
698
+ networkMode: field(hostConfig, "NetworkMode"),
699
+ restartPolicy: field(hostConfig, "RestartPolicy"),
700
+ portBindings: field(hostConfig, "PortBindings"),
701
+ mounts: Array.isArray(mounts) ? mounts.map((mount) => ({
702
+ type: field(mount, "Type"),
703
+ source: field(mount, "Source"),
704
+ destination: field(mount, "Destination"),
705
+ mode: field(mount, "Mode"),
706
+ rw: field(mount, "RW")
707
+ })) : null,
708
+ envNames: stringList(field(config, "Env")).map((entry) => entry.split("=")[0] ?? entry),
709
+ labels: field(config, "Labels")
710
+ };
711
+ };
712
+ var dockerCollector = (deps, docker2) => ({
713
+ name: "docker",
714
+ collect: async () => {
715
+ const found = await docker2();
716
+ const sections = [
717
+ `# docker version${found.context.length === 0 ? "" : ` (${found.context.join(" ")})`}`,
718
+ found.version,
719
+ "",
720
+ "# docker info",
721
+ found.info,
722
+ "",
723
+ "# docker context ls",
724
+ found.contexts,
725
+ "",
726
+ "# docker ps -a (Compose project mend \xB7 names sealant-* \xB7 label sealant.workspace)",
727
+ found.containers.length === 0 ? "none" : found.containers.map((row) => `${row.id.slice(0, 12)} ${row.name} ${row.image} ${row.status}`).join("\n")
728
+ ];
729
+ if (found.notes.length > 0) sections.push("", "# notes", ...found.notes);
730
+ if (found.containers.length > 0) {
731
+ const inspected = await deps.run(
732
+ "docker",
733
+ [...found.context, "inspect", ...found.containers.map((row) => row.id)],
734
+ DOCKER_TIMEOUT_MS
735
+ );
736
+ sections.push("", "# docker inspect (Env as names only)");
737
+ if (inspected.error !== void 0 || inspected.status !== 0) {
738
+ sections.push(outputOrFailure(inspected));
739
+ } else {
740
+ let parsed = null;
741
+ try {
742
+ parsed = JSON.parse(inspected.stdout);
743
+ } catch {
744
+ sections.push("docker inspect returned invalid JSON");
745
+ }
746
+ if (Array.isArray(parsed)) sections.push(json(parsed.map(summarizeInspect)).trimEnd());
454
747
  }
455
- );
456
- }
457
- const grant = probes.claudeGrant();
458
- if (grant !== null) {
459
- const facts = claudeGrantFacts(grant);
460
- if (facts === null) {
461
- checks.push({
462
- label: "grant",
463
- state: "failed",
464
- detail: "unreadable",
465
- fix: "mend connect claude"
466
- });
467
- } else if (!facts.hasRefreshToken) {
468
- checks.push({
469
- label: "grant",
470
- state: "failed",
471
- detail: "signed out",
472
- fix: "mend connect claude"
473
- });
474
- } else if (facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now()) {
475
- checks.push({
476
- label: "grant",
477
- state: "todo",
478
- detail: `expired ${day(facts.refreshExpiresAt)}`,
479
- fix: "mend connect claude"
480
- });
481
- } else {
482
- checks.push({
483
- label: "grant",
484
- state: "ok",
485
- detail: facts.refreshExpiresAt === null ? "Mend's own" : `Mend's own \xB7 expires ${day(facts.refreshExpiresAt)}`,
486
- fix: null
487
- });
488
748
  }
749
+ return [{ path: "docker.txt", content: `${sections.join("\n")}
750
+ ` }];
489
751
  }
490
- const adopted = projects === null ? null : projects.value;
491
- if (adopted === null) checks.push(notChecked("projects"));
492
- else if (adopted.length === 0) {
493
- checks.push({ label: "projects", state: "todo", detail: "none adopted", fix: "mend adopt" });
494
- } else {
495
- checks.push({ label: "projects", state: "ok", detail: `${adopted.length} adopted`, fix: null });
496
- }
497
- for (const { command, provider } of HARNESS_CLIS) {
498
- const label = `${command} cli`;
499
- if (!probes.onPath(command)) {
500
- checks.push({ label, state: "todo", detail: "not on PATH", fix: null });
501
- continue;
752
+ });
753
+ var isWorkspaceContainer = (row) => row.name.startsWith("sealant-") || row.labels.includes("sealant.workspace=");
754
+ var workspaceLogsCollector = (deps, docker2) => ({
755
+ name: "workspace-logs",
756
+ collect: async () => {
757
+ const found = await docker2();
758
+ const running = found.containers.filter(
759
+ (row) => isWorkspaceContainer(row) && row.state === "running"
760
+ );
761
+ if (running.length === 0) {
762
+ return [
763
+ {
764
+ path: "workspace-logs.txt",
765
+ content: "no running workspace container observed (names sealant-* or label sealant.workspace)\n"
766
+ }
767
+ ];
502
768
  }
503
- const credential = probes.localCredential(provider);
504
- checks.push(
505
- credential === null ? {
506
- label,
507
- state: "todo",
508
- detail: "on PATH \xB7 no credential here",
509
- fix: LOGIN_COMMANDS[provider]
510
- } : { label, state: "ok", detail: "on PATH \xB7 credential present", fix: null }
511
- );
769
+ const files = [];
770
+ for (const row of running) {
771
+ const logs = await deps.run(
772
+ "docker",
773
+ [...found.context, "logs", "--tail", String(deps.tail), row.id],
774
+ DOCKER_TIMEOUT_MS
775
+ );
776
+ const text = logs.error !== void 0 || logs.status !== 0 ? outputOrFailure(logs) : `${logs.stdout.trimEnd()}${logs.stderr.trim() === "" ? "" : `
777
+ --- stderr ---
778
+ ${logs.stderr.trimEnd()}`}`;
779
+ files.push({ path: `workspace-logs/${row.name}.log`, content: `${text}
780
+ ` });
781
+ }
782
+ return files;
512
783
  }
513
- const machine = signedIn ? await getJson(config, "/machine") : null;
514
- const exposure = machine === null || machine.value === null ? void 0 : machine.value.exposure;
515
- checks.push(exposure === void 0 ? notChecked("exposure") : exposureCheck(exposure));
516
- return checks;
517
- };
518
- var onPath = (command) => {
519
- for (const directory of (process.env["PATH"] ?? "").split(path2.delimiter)) {
520
- if (directory === "") continue;
521
- try {
522
- fs2.accessSync(path2.join(directory, command), fs2.constants.X_OK);
523
- return true;
524
- } catch {
784
+ });
785
+ var RECORD_PAGE_LIMIT = 1e3;
786
+ var RECORD_MAX_PAGES = 200;
787
+ var RECORD_TIME_BUDGET_MS = 3e4;
788
+ var SESSION_CONCURRENCY = 4;
789
+ var readRecordTail = async (deps, processId) => {
790
+ let cursor = "0";
791
+ let text = "";
792
+ let chunks = 0;
793
+ let pages = 0;
794
+ let status = null;
795
+ const deadline = Date.now() + RECORD_TIME_BUDGET_MS;
796
+ try {
797
+ for (; ; ) {
798
+ const page = await deps.get(
799
+ `/processes/${processId}/logs?from=${encodeURIComponent(cursor)}&limit=${RECORD_PAGE_LIMIT}`
800
+ );
801
+ pages += 1;
802
+ status = page.status;
803
+ for (const chunk of page.chunks)
804
+ text += Buffer.from(chunk.dataBase64, "base64").toString("utf8");
805
+ chunks += page.chunks.length;
806
+ text = lastLines(text, deps.tail + 1);
807
+ const advanced = page.nextFrom !== cursor;
808
+ cursor = page.nextFrom;
809
+ if (!advanced) return { text, chunks, pages, complete: true, status, failure: null };
810
+ if (pages >= RECORD_MAX_PAGES || Date.now() > deadline)
811
+ return { text, chunks, pages, complete: false, status, failure: null };
525
812
  }
813
+ } catch (error) {
814
+ return {
815
+ text,
816
+ chunks,
817
+ pages,
818
+ complete: false,
819
+ status,
820
+ failure: error instanceof Error ? error.message : String(error)
821
+ };
526
822
  }
527
- return false;
528
823
  };
529
- var paintMark = (state, mark) => {
530
- if (process.stdout.isTTY !== true) return mark;
531
- const code = state === "ok" ? "32" : state === "todo" ? "33" : "31";
532
- return `\x1B[${code}m${mark}\x1B[0m`;
824
+ var processLine = (row) => [
825
+ row.id,
826
+ row.kind,
827
+ row.label ?? row.harness ?? "",
828
+ row.status,
829
+ row.exitCode === null ? "" : `exit ${row.exitCode}`,
830
+ row.argv.length === 0 ? "" : `argv: ${row.argv.join(" ")}`
831
+ ].filter((part) => part !== "").join(" \xB7 ");
832
+ var recordFile = async (deps, detail) => {
833
+ const recorded = detail.processes.filter((row) => row.sealantSessionId !== null);
834
+ if (recorded.length === 0) return null;
835
+ const sections = [
836
+ `# session ${detail.session.id} \xB7 ${recorded.length} recorded process(es) \xB7 last ${deps.tail} lines of each, raw PTY output as UTF-8`
837
+ ];
838
+ for (const row of recorded) {
839
+ const tail = await readRecordTail(deps, row.id);
840
+ const facts = [
841
+ `${tail.chunks} chunks in ${tail.pages} page(s)`,
842
+ tail.status === null ? null : `record ${tail.status}`,
843
+ tail.complete ? null : `the read stopped at the page budget (${RECORD_MAX_PAGES} pages / ${RECORD_TIME_BUDGET_MS / 1e3} s) before the record ended, so these are the last lines READ, not the last lines recorded`,
844
+ tail.failure === null ? null : `read failed: ${tail.failure}`
845
+ ].filter((fact) => fact !== null);
846
+ sections.push("", `== ${processLine(row)}`, `(${facts.join(" \xB7 ")})`, tail.text.trimEnd());
847
+ }
848
+ return `${sections.join("\n")}
849
+ `;
533
850
  };
534
- var doctorCommand = async (config, localCredential2, claudeGrant2) => {
535
- const checks = await runChecks(config, { localCredential: localCredential2, claudeGrant: claudeGrant2, onPath });
536
- for (const check of checks) process.stdout.write(`${formatCheck(check, paintMark)}
537
- `);
538
- if (checks.some((check) => check.state === "failed")) process.exitCode = 1;
851
+ var sessionsCollector = (deps) => ({
852
+ name: "sessions",
853
+ collect: async () => {
854
+ const projects = await deps.get("/projects");
855
+ const details = await mapLimit(
856
+ projects,
857
+ SESSION_CONCURRENCY,
858
+ (project2) => deps.get(`/projects/${project2.id}?deadEnds=include`)
859
+ );
860
+ const rows = details.flatMap(
861
+ (detail) => detail.sessions.map((session) => ({ session, projectName: detail.project.name }))
862
+ ).toSorted((a, b) => b.session.createdAt.localeCompare(a.session.createdAt));
863
+ const collected = await mapLimit(
864
+ rows,
865
+ SESSION_CONCURRENCY,
866
+ async ({ session, projectName }) => {
867
+ const files2 = [];
868
+ let detail;
869
+ try {
870
+ detail = await deps.get(`/sessions/${session.id}`);
871
+ } catch (error) {
872
+ files2.push({
873
+ path: `sessions/${session.id}.error.txt`,
874
+ content: `${error instanceof Error ? error.message : String(error)}
875
+ `
876
+ });
877
+ return {
878
+ files: files2,
879
+ summary: {
880
+ id: session.id,
881
+ project: projectName,
882
+ harness: session.harness,
883
+ label: session.label,
884
+ status: session.status,
885
+ createdAt: session.createdAt,
886
+ updatedAt: session.updatedAt ?? null,
887
+ detail: `sessions/${session.id}.error.txt`
888
+ }
889
+ };
890
+ }
891
+ files2.push({ path: `sessions/${session.id}.json`, content: json(detail) });
892
+ const record = await recordFile(deps, detail);
893
+ if (record !== null)
894
+ files2.push({ path: `sessions/${session.id}-record.txt`, content: record });
895
+ const services = new Set(
896
+ detail.processes.flatMap((row) => row.serviceId === null ? [] : [row.serviceId])
897
+ );
898
+ return {
899
+ files: files2,
900
+ summary: {
901
+ id: session.id,
902
+ project: projectName,
903
+ harness: session.harness,
904
+ label: session.label,
905
+ status: session.status,
906
+ worktree: session.worktree,
907
+ branch: session.branch,
908
+ createdAt: session.createdAt,
909
+ updatedAt: session.updatedAt ?? null,
910
+ processes: detail.processes.map((row) => ({
911
+ id: row.id,
912
+ kind: row.kind,
913
+ label: row.label,
914
+ harness: row.harness,
915
+ status: row.status,
916
+ exitCode: row.exitCode,
917
+ argv: row.argv,
918
+ createdAt: row.createdAt,
919
+ exitedAt: row.exitedAt,
920
+ recorded: row.sealantSessionId !== null
921
+ })),
922
+ services: services.size,
923
+ liveServices: detail.liveServices ?? null,
924
+ detail: `sessions/${session.id}.json`,
925
+ record: record === null ? null : `sessions/${session.id}-record.txt`
926
+ }
927
+ };
928
+ }
929
+ );
930
+ const summaries = collected.map((item) => item.summary);
931
+ const files = collected.flatMap((item) => item.files);
932
+ files.unshift({
933
+ path: "sessions.json",
934
+ content: json({
935
+ projects: projects.length,
936
+ sessions: rows.length,
937
+ recordTailLines: deps.tail,
938
+ rows: summaries
939
+ })
940
+ });
941
+ return files;
942
+ }
943
+ });
944
+ var accountsCollector = (deps) => ({
945
+ name: "accounts",
946
+ collect: async () => [{ path: "accounts.txt", content: await deps.accounts() }]
947
+ });
948
+ var TOOLS = ["claude", "codex", "gh", "git", "docker"];
949
+ var TOOL_TIMEOUT_MS = 15e3;
950
+ var toolsCollector = (deps) => ({
951
+ name: "tools",
952
+ collect: async () => {
953
+ const lines = [];
954
+ for (const tool of TOOLS) {
955
+ const found = deps.pathOf(tool);
956
+ if (found === null) {
957
+ lines.push(`${tool.padEnd(8)} not on PATH`);
958
+ continue;
959
+ }
960
+ const output = await deps.run(tool, ["--version"], TOOL_TIMEOUT_MS);
961
+ const version = output.error !== void 0 || output.status !== 0 ? `--version ${outputOrFailure(output).split("\n")[0] ?? ""}` : output.stdout.trim().split("\n")[0] ?? "";
962
+ lines.push(`${tool.padEnd(8)} ${found} \xB7 ${version}`);
963
+ }
964
+ return [{ path: "tools.txt", content: `${lines.join("\n")}
965
+ ` }];
966
+ }
967
+ });
968
+ var bundleCollectors = (deps) => {
969
+ const server = serverFactsOnce(deps);
970
+ const docker2 = dockerFactsOnce(deps, server);
971
+ return [
972
+ cliCollector(deps),
973
+ doctorCollector(deps),
974
+ serverHealthCollector(deps),
975
+ serverConfigCollector(server),
976
+ dockerCollector(deps, docker2),
977
+ serverLogsCollector(deps, server),
978
+ workspaceLogsCollector(deps, docker2),
979
+ sessionsCollector(deps),
980
+ accountsCollector(deps),
981
+ toolsCollector(deps)
982
+ ];
539
983
  };
540
984
 
541
- // src/dotfiles.ts
542
- import * as fs3 from "node:fs";
543
- import * as path3 from "node:path";
544
- var DOTFILE_CANDIDATES = [
545
- {
546
- group: "shell",
547
- paths: [
548
- ".zshrc",
549
- ".zshenv",
550
- ".zprofile",
551
- ".bashrc",
552
- ".bash_profile",
553
- ".profile",
554
- ".aliases",
555
- ".config/fish/config.fish",
556
- ".config/starship.toml"
557
- ]
558
- },
559
- {
560
- group: "git",
561
- paths: [".gitconfig", ".gitignore_global", ".config/git/config", ".config/git/ignore"]
562
- },
985
+ // src/doctor-bundle.ts
986
+ import * as fs4 from "node:fs";
987
+ import * as path4 from "node:path";
988
+ import { gzipSync } from "node:zlib";
989
+
990
+ // src/help.ts
991
+ var SECTIONS = [
992
+ "start",
993
+ "sessions",
994
+ "services",
995
+ "project setup",
996
+ "organization",
997
+ "this machine"
998
+ ];
999
+ var project = (what = "the project") => ({
1000
+ flag: "--project <p>",
1001
+ text: `${what} by name, when the current directory is not inside it`
1002
+ });
1003
+ var sessionArg = "With no id, the one live session is taken; with several, a picker opens. A prefix of the id is enough.";
1004
+ var tunnelText = "On a server that is not this machine, the session's live Services declared --http or --https are tunneled to this machine's loopback while this terminal is attached, on the Service's own port when it is free and on a free one when it is not. One line per tunnel says where it opens, for example web \u2192 http://localhost:5173. A Service that stops closes its tunnel, and detaching closes them all. The Services keep running.";
1005
+ var noTunnel = {
1006
+ flag: "--no-tunnel",
1007
+ text: "do not tunnel the session's Services to this machine"
1008
+ };
1009
+ var COMMANDS = [
1010
+ // ── start ──────────────────────────────────────────────────────────────
563
1011
  {
564
- group: "editors",
565
- paths: [".vimrc", ".ideavimrc", ".editorconfig", ".config/helix/config.toml"]
566
- },
567
- {
568
- group: "terminal",
569
- paths: [".tmux.conf", ".config/tmux/tmux.conf", ".inputrc", ".dir_colors"]
570
- },
571
- {
572
- group: "tools",
573
- paths: [
574
- ".ripgreprc",
575
- ".config/bat/config",
576
- ".config/lazygit/config.yml",
577
- ".config/mise/config.toml",
578
- ".config/atuin/config.toml",
579
- ".config/zellij/config.kdl"
580
- ]
581
- }
582
- ];
583
- var MAX_FILE_BYTES = 1024 * 1024;
584
- var statFile = (home, relative4) => {
585
- try {
586
- const stat = fs3.statSync(path3.join(home, relative4));
587
- return stat.isFile() ? stat : null;
588
- } catch {
589
- return null;
590
- }
591
- };
592
- var scanDotfileCandidates = (home) => DOTFILE_CANDIDATES.flatMap(
593
- ({ group, paths }) => paths.flatMap((relative4) => {
594
- const stat = statFile(home, relative4);
595
- return stat === null ? [] : [{ path: relative4, group, bytes: stat.size }];
596
- })
597
- );
598
- var homeRelativePath = (home, requested) => {
599
- const relative4 = path3.relative(home, path3.resolve(home, requested));
600
- const outside = relative4 === "" || relative4 === ".." || relative4.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative4);
601
- return outside ? null : relative4.split(path3.sep).join("/");
602
- };
603
- var readSyncFiles = (home, paths_) => {
604
- const files = [];
605
- for (const requested of paths_) {
606
- const relative4 = homeRelativePath(home, requested);
607
- if (relative4 === null) {
608
- return {
609
- error: `${requested} is not under ${home} \u2014 only files in your home directory sync`
610
- };
611
- }
612
- const stat = statFile(home, relative4);
613
- if (stat === null) {
614
- return { error: `${relative4} is not a file under ${home}` };
615
- }
616
- if (stat.size > MAX_FILE_BYTES) {
617
- return { error: `${relative4} is over 1MB \u2014 dotfiles are text; trim the selection` };
618
- }
619
- files.push({
620
- path: relative4,
621
- contentsBase64: fs3.readFileSync(path3.join(home, relative4)).toString("base64"),
622
- // eslint-disable-next-line no-bitwise -- permission bits are the point
623
- mode: (stat.mode & 511).toString(8)
624
- });
625
- }
626
- return { files };
627
- };
628
- var DOTFILES_MANAGERS = ["auto", "copy", "stow", "chezmoi"];
629
- var VALUE_FLAGS = ["--ref", "--subdirectory", "--manager"];
630
- var parseDotfilesRepoArgs = (args) => {
631
- if (args.includes("--clear")) {
632
- return args.length === 1 ? { kind: "clear" } : { kind: "error", error: "--clear takes no URL or other options" };
633
- }
634
- let url = null;
635
- const values = /* @__PURE__ */ new Map();
636
- let bootstrap = true;
637
- for (let index = 0; index < args.length; index += 1) {
638
- const arg = args[index] ?? "";
639
- if (arg === "--no-bootstrap") {
640
- bootstrap = false;
641
- continue;
642
- }
643
- const flag = VALUE_FLAGS.find((candidate) => candidate === arg);
644
- if (flag !== void 0) {
645
- const value = args[index + 1];
646
- if (value === void 0 || value.trim() === "" || value.startsWith("--")) {
647
- return { kind: "error", error: `${flag} needs a value` };
648
- }
649
- values.set(flag, value.trim());
650
- index += 1;
651
- continue;
652
- }
653
- if (arg.startsWith("-")) return { kind: "error", error: `unknown flag ${arg}` };
654
- if (url !== null) return { kind: "error", error: "one repository URL only" };
655
- url = arg.trim();
656
- }
657
- if (url === null || url === "") {
658
- return { kind: "error", error: "name the repository URL, or --clear to remove it" };
659
- }
660
- const requested = values.get("--manager") ?? "auto";
661
- const manager = DOTFILES_MANAGERS.find((candidate) => candidate === requested);
662
- if (manager === void 0) {
663
- return { kind: "error", error: `--manager must be one of ${DOTFILES_MANAGERS.join(", ")}` };
664
- }
665
- return {
666
- kind: "set",
667
- repository: {
668
- url,
669
- ref: values.get("--ref") ?? null,
670
- subdirectory: values.get("--subdirectory") ?? null,
671
- manager,
672
- bootstrap
673
- }
674
- };
675
- };
676
- var dotfilesRepositoryFacts = (repository) => [
677
- repository.ref ?? "default branch",
678
- ...repository.subdirectory === null ? [] : [`${repository.subdirectory}/`],
679
- `manager ${repository.manager}`,
680
- repository.bootstrap ? "install.sh on" : "install.sh off"
681
- ].join(" \xB7 ");
682
-
683
- // src/env.ts
684
- var formatLoadReport = (report, paint6) => {
685
- const width = Math.max(
686
- 0,
687
- ...report.loaded.map((entry) => entry.name.length),
688
- ...report.rejected.map((entry) => entry.name.length)
689
- );
690
- const lines = [];
691
- for (const entry of report.loaded) {
692
- lines.push(
693
- ` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint6.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
694
- );
695
- }
696
- for (const entry of report.rejected) {
697
- lines.push(
698
- ` ${entry.name.padEnd(width)} ${paint6.warn("rejected")} ${paint6.dim(`\xB7 ${entry.reason}`)}`
699
- );
700
- }
701
- return lines;
702
- };
703
-
704
- // src/git-author.ts
705
- var parseGitAuthorArgs = (args) => {
706
- const clear = args.includes("--clear");
707
- const words = args.filter((arg) => !arg.startsWith("--"));
708
- if (clear) return words.length === 0 ? { kind: "clear" } : { kind: "usage" };
709
- if (words.length === 0) return { kind: "show" };
710
- const [name, email] = words;
711
- if (words.length !== 2 || name === void 0 || email === void 0) return { kind: "usage" };
712
- const issue = gitAuthorIssue({ name, email });
713
- if (issue !== null) return { kind: "invalid", message: `git author not saved \xB7 ${issue}` };
714
- const author = normalizeGitAuthor({ name, email });
715
- return { kind: "set", name: author.name, email: author.email };
716
- };
717
- var gitAuthorLine = (author) => `${author.name} <${author.email}> \xB7 ${author.source === "setting" ? "your setting" : "your account's name and email"}`;
718
-
719
- // src/help.ts
720
- var SECTIONS = [
721
- "start",
722
- "sessions",
723
- "services",
724
- "project setup",
725
- "organization",
726
- "this machine"
727
- ];
728
- var project = (what = "the project") => ({
729
- flag: "--project <p>",
730
- text: `${what} by name, when the current directory is not inside it`
731
- });
732
- var sessionArg = "With no id, the one live session is taken; with several, a picker opens. A prefix of the id is enough.";
733
- var tunnelText = "On a server that is not this machine, the session's live Services declared --http or --https are tunneled to this machine's loopback while this terminal is attached, on the Service's own port when it is free and on a free one when it is not. One line per tunnel says where it opens, for example web \u2192 http://localhost:5173. A Service that stops closes its tunnel, and detaching closes them all. The Services keep running.";
734
- var noTunnel = {
735
- flag: "--no-tunnel",
736
- text: "do not tunnel the session's Services to this machine"
737
- };
738
- var COMMANDS = [
739
- // ── start ──────────────────────────────────────────────────────────────
740
- {
741
- name: "login",
742
- section: "start",
743
- summary: "sign this terminal in through the browser",
744
- synopsis: ["[--url <server>]"],
745
- description: [
746
- "Opens <server>/authorize in your browser. You press Authorize there. The CLI saves a device token to ~/.config/mend/cli.json with mode 0600. No password is typed in the terminal, and the token can be revoked from the server at any time."
747
- ],
748
- options: [
749
- {
750
- flag: "--url <server>",
751
- text: "the Mend server. Default: MEND_URL, then http://localhost:3105"
752
- }
753
- ],
754
- examples: [
755
- { command: "mend login --url http://10.0.0.216:3105", text: "sign in to a LAN server" }
756
- ],
757
- see: ["logout", "pair", "doctor"]
1012
+ name: "login",
1013
+ section: "start",
1014
+ summary: "sign this terminal in through the browser",
1015
+ synopsis: ["[--url <server>]"],
1016
+ description: [
1017
+ "Opens <server>/authorize in your browser. You press Authorize there. The CLI saves a device token to ~/.config/mend/cli.json with mode 0600. No password is typed in the terminal, and the token can be revoked from the server at any time."
1018
+ ],
1019
+ options: [
1020
+ {
1021
+ flag: "--url <server>",
1022
+ text: "the Mend server. Default: MEND_URL, then http://localhost:3105"
1023
+ }
1024
+ ],
1025
+ examples: [
1026
+ { command: "mend login --url http://10.0.0.216:3105", text: "sign in to a LAN server" }
1027
+ ],
1028
+ see: ["logout", "pair", "doctor"]
758
1029
  },
759
1030
  {
760
1031
  name: "connect",
@@ -865,12 +1136,29 @@ var COMMANDS = [
865
1136
  {
866
1137
  name: "doctor",
867
1138
  section: "start",
868
- summary: "check this machine's setup",
869
- synopsis: [],
1139
+ summary: "check this machine's setup, or bundle it for a bug report",
1140
+ synopsis: ["", "--bundle [--out <path>] [--tail <n>]"],
870
1141
  description: [
871
- "Read-only. One line per fact: the server, the sign-in, the platform connection, the provider accounts, the git key. Each unfinished line ends with the command that fixes it."
1142
+ "Read-only. One line per fact: the server, the sign-in, the platform connection, the provider accounts, the git key. Each unfinished line ends with the command that fixes it.",
1143
+ "--bundle collects what a maintainer asks for one output at a time into one tar.gz: this CLI and its environment, the doctor lines, the server's health, the local server's configuration (its compose file and the names of its .env keys, never their values), docker version, info, contexts, the Mend and workspace containers with their inspect facts, the local server's container logs, the running workspace containers' logs, every session with its processes, exit codes and argv, the recorded terminal output of each, the connected accounts, and the versions and paths of claude, codex, gh, git and docker on this machine.",
1144
+ "Each part is collected on its own: one that fails leaves a <name>.error.txt in the bundle instead of stopping it. One redactor runs over every file before it is written: bearer and header values, Slack, OpenAI, GitHub and AWS token shapes, JWTs, passwords in URLs, and any password, secret or token value. The archive is written with mode 0600. It still contains logs and configuration; read it before you share it."
1145
+ ],
1146
+ options: [
1147
+ { flag: "--bundle", text: "write the diagnostic archive instead of printing the checklist" },
1148
+ {
1149
+ flag: "--out <path>",
1150
+ text: "where the archive goes. Default: ~/.config/mend/bundles/mend-bundle-<time>.tgz"
1151
+ },
1152
+ {
1153
+ flag: "--tail <n>",
1154
+ text: "lines per container log and per recorded process, 1..2000. Default: 500"
1155
+ }
1156
+ ],
1157
+ examples: [
1158
+ { command: "mend doctor", text: "the checklist" },
1159
+ { command: "mend doctor --bundle", text: "one archive to attach to a bug report" }
872
1160
  ],
873
- see: ["login", "connect", "keys init"]
1161
+ see: ["login", "connect", "keys init", "server logs"]
874
1162
  },
875
1163
  // ── sessions ───────────────────────────────────────────────────────────
876
1164
  {
@@ -2036,16 +2324,611 @@ var renderManIndex = (version) => {
2036
2324
  COMMANDS.filter((d) => !d.hidden).map((d) => `mend-${d.name.replace(/ /g, "-")}(1)`).join(", ")
2037
2325
  )
2038
2326
  );
2039
- return `${out.join("\n")}
2040
- `;
2327
+ return `${out.join("\n")}
2328
+ `;
2329
+ };
2330
+ var manFileName = (doc) => doc === null ? "mend.1" : `mend-${doc.name.replace(/ /g, "-")}.1`;
2331
+
2332
+ // src/doctor-bundle.ts
2333
+ var REDACTED = "[redacted]";
2334
+ var KEY_WORDS = "password|passwd|secret|token|api[_-]?key|private[_-]?key|credential";
2335
+ var blank = () => REDACTED;
2336
+ var RULES = [
2337
+ [
2338
+ /\b(authorization|proxy-authorization|x-api-key|cookie|set-cookie)(\s*[:=]\s*)[^\r\n]+/gi,
2339
+ (_match, header, separator) => `${header}${separator}${REDACTED}`
2340
+ ],
2341
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]+/g, () => `Bearer ${REDACTED}`],
2342
+ [/\b(?:xox[abops]|xapp)-[A-Za-z0-9-]+/g, blank],
2343
+ [/\bsk-[A-Za-z0-9_-]{8,}/g, blank],
2344
+ [/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{6,}/g, blank],
2345
+ [/\bgithub_pat_[A-Za-z0-9_]{6,}/g, blank],
2346
+ [/\beyJ[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]+)*/g, blank],
2347
+ [/\bAKIA[0-9A-Z]{16}\b/g, blank],
2348
+ [
2349
+ /\b([a-z][a-z0-9+.-]*:\/\/[^:/\s@]+):[^@\s/]+@/gi,
2350
+ (_match, prefix) => `${prefix}:${REDACTED}@`
2351
+ ],
2352
+ [
2353
+ new RegExp(
2354
+ `\\b([A-Za-z0-9_.-]*(?:${KEY_WORDS})[A-Za-z0-9_.-]*)("?\\s*[=:]\\s*"?)([^"\\s,;&]+)`,
2355
+ "gi"
2356
+ ),
2357
+ (_match, key, separator, value) => /^(?:true|false|null)$/i.test(value) ? `${key}${separator}${value}` : `${key}${separator}${REDACTED}`
2358
+ ],
2359
+ [/^(\s*(?:export\s+)?[A-Z][A-Z0-9_]*=)(.+)$/gm, (_match, key) => `${key}${REDACTED}`]
2360
+ ];
2361
+ var redact = (text) => {
2362
+ let out = text;
2363
+ for (const [pattern, replacer] of RULES) out = out.replace(pattern, replacer);
2364
+ return out;
2365
+ };
2366
+ var octal = (value, width) => `${value.toString(8).padStart(width - 1, "0")}\0`;
2367
+ var splitName = (name) => {
2368
+ if (Buffer.byteLength(name) <= 100) return { name, prefix: "" };
2369
+ let cut = name.lastIndexOf("/");
2370
+ while (cut > 0 && Buffer.byteLength(name.slice(cut + 1)) > 100)
2371
+ cut = name.lastIndexOf("/", cut - 1);
2372
+ if (cut <= 0 || Buffer.byteLength(name.slice(0, cut)) > 155)
2373
+ throw new Error(`tar entry name too long: ${name}`);
2374
+ return { name: name.slice(cut + 1), prefix: name.slice(0, cut) };
2375
+ };
2376
+ var tarHeader = (entry, mtime) => {
2377
+ const header = Buffer.alloc(512, 0);
2378
+ const { name, prefix } = splitName(entry.path);
2379
+ header.write(name, 0, 100, "utf8");
2380
+ header.write(octal(420, 8), 100, 8, "ascii");
2381
+ header.write(octal(0, 8), 108, 8, "ascii");
2382
+ header.write(octal(0, 8), 116, 8, "ascii");
2383
+ header.write(octal(entry.content.length, 12), 124, 12, "ascii");
2384
+ header.write(octal(mtime, 12), 136, 12, "ascii");
2385
+ header.write(" ", 148, 8, "ascii");
2386
+ header.write("0", 156, 1, "ascii");
2387
+ header.write("ustar\0", 257, 6, "ascii");
2388
+ header.write("00", 263, 2, "ascii");
2389
+ header.write("mend", 265, 32, "ascii");
2390
+ header.write("mend", 297, 32, "ascii");
2391
+ header.write(octal(0, 8), 329, 8, "ascii");
2392
+ header.write(octal(0, 8), 337, 8, "ascii");
2393
+ header.write(prefix, 345, 155, "utf8");
2394
+ let sum = 0;
2395
+ for (const byte of header) sum += byte;
2396
+ header.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "ascii");
2397
+ return header;
2398
+ };
2399
+ var tarGz = (entries, now) => {
2400
+ const mtime = Math.floor(now.getTime() / 1e3);
2401
+ const blocks = [];
2402
+ for (const entry of entries) {
2403
+ blocks.push(tarHeader(entry, mtime), entry.content);
2404
+ const remainder = entry.content.length % 512;
2405
+ if (remainder !== 0) blocks.push(Buffer.alloc(512 - remainder, 0));
2406
+ }
2407
+ blocks.push(Buffer.alloc(1024, 0));
2408
+ return gzipSync(Buffer.concat(blocks));
2409
+ };
2410
+ var describeError = (error) => `${error instanceof Error ? error.message : String(error)}
2411
+ `;
2412
+ var collectBundle = async (collectors) => {
2413
+ const files = [];
2414
+ for (const collector of collectors) {
2415
+ try {
2416
+ for (const file of await collector.collect()) {
2417
+ files.push({ path: file.path, content: redact(file.content) });
2418
+ }
2419
+ } catch (error) {
2420
+ files.push({ path: `${collector.name}.error.txt`, content: redact(describeError(error)) });
2421
+ }
2422
+ }
2423
+ return files;
2424
+ };
2425
+ var bundleFileName = (now) => `mend-bundle-${now.toISOString().replace(/\.\d{3}Z$/, "Z").replace(/:/g, "-")}.tgz`;
2426
+ var writeBundle = (files, outPath, now) => {
2427
+ const directory = path4.dirname(outPath);
2428
+ fs4.mkdirSync(directory, { recursive: true, mode: 448 });
2429
+ const root = path4.basename(outPath).replace(/\.(?:tgz|tar\.gz)$/, "");
2430
+ const entries = files.map((file) => ({
2431
+ path: file.path,
2432
+ content: Buffer.from(file.content, "utf8")
2433
+ }));
2434
+ const bytes = tarGz(
2435
+ entries.map((entry) => ({ path: `${root}/${entry.path}`, content: entry.content })),
2436
+ now
2437
+ );
2438
+ fs4.writeFileSync(outPath, bytes, { mode: 384 });
2439
+ fs4.chmodSync(outPath, 384);
2440
+ return {
2441
+ path: outPath,
2442
+ bytes: bytes.length,
2443
+ entries: entries.map((entry) => ({ path: entry.path, bytes: entry.content.length }))
2444
+ };
2445
+ };
2446
+ var formatBytes = (bytes) => {
2447
+ if (bytes < 1024) return `${bytes} B`;
2448
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
2449
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2450
+ };
2451
+ var NOTICE = "Contains logs and configuration; secrets are redacted, but read it before sharing.";
2452
+ var bundleReport = (written) => {
2453
+ const width = Math.max(...written.entries.map((entry) => entry.path.length), 0);
2454
+ return [
2455
+ `${written.path} \xB7 ${formatBytes(written.bytes)}`,
2456
+ ...written.entries.map((entry) => ` ${entry.path.padEnd(width)} ${formatBytes(entry.bytes)}`),
2457
+ NOTICE
2458
+ ];
2459
+ };
2460
+ var DEFAULT_TAIL = 500;
2461
+ var MAX_TAIL = 2e3;
2462
+ var usage = (message) => ({
2463
+ kind: "usage",
2464
+ message: `${message}
2465
+ ${usageOf("doctor")}`
2466
+ });
2467
+ var parseBundleArgs = (args) => {
2468
+ let out = null;
2469
+ let tail = DEFAULT_TAIL;
2470
+ for (let index = 0; index < args.length; index += 1) {
2471
+ const arg = args[index];
2472
+ if (arg === "--bundle") continue;
2473
+ if (arg === "--out") {
2474
+ const value = args[index + 1];
2475
+ if (value === void 0 || value.startsWith("--")) return usage("--out needs a path");
2476
+ out = value;
2477
+ index += 1;
2478
+ continue;
2479
+ }
2480
+ if (arg === "--tail") {
2481
+ const value = args[index + 1];
2482
+ if (value === void 0 || !/^\d+$/.test(value)) return usage("--tail needs a number");
2483
+ tail = Number(value);
2484
+ if (tail < 1 || tail > MAX_TAIL) return usage(`--tail is 1..${MAX_TAIL}`);
2485
+ index += 1;
2486
+ continue;
2487
+ }
2488
+ return usage(`unknown option "${arg ?? ""}"`);
2489
+ }
2490
+ return { kind: "ok", args: { out, tail } };
2491
+ };
2492
+ var doctorBundleCommand = async (args, deps) => {
2493
+ const parsed = parseBundleArgs(args);
2494
+ if (parsed.kind === "usage") {
2495
+ deps.warn(`mend: ${parsed.message}`);
2496
+ process.exitCode = 1;
2497
+ return;
2498
+ }
2499
+ const now = deps.now();
2500
+ const outPath = path4.resolve(parsed.args.out ?? path4.join(deps.defaultDir, bundleFileName(now)));
2501
+ deps.warn("collecting \xB7 this can take a minute");
2502
+ const files = await collectBundle(deps.collectors(parsed.args.tail));
2503
+ const written = writeBundle(files, outPath, now);
2504
+ for (const line of bundleReport(written)) deps.say(line);
2505
+ };
2506
+
2507
+ // src/doctor.ts
2508
+ import * as fs5 from "node:fs";
2509
+ import * as path5 from "node:path";
2510
+ var TIMEOUT_MS = 3e3;
2511
+ var MARKS = { ok: "\u2713", todo: "\u25CB", failed: "\u2717" };
2512
+ var LABEL_WIDTH = 11;
2513
+ var day = (at) => at.toISOString().slice(0, 10);
2514
+ var formatCheck = (check, paint6 = (_state, mark) => mark) => `${paint6(check.state, MARKS[check.state])} ${check.label.padEnd(LABEL_WIDTH)} ${check.detail}${check.fix === null ? "" : ` \u2192 ${check.fix}`}`;
2515
+ var getJson = async (config, route) => {
2516
+ try {
2517
+ const response = await fetch(`${config.url}/api${route}`, {
2518
+ headers: config.token === null ? {} : { authorization: `Bearer ${config.token}` },
2519
+ signal: AbortSignal.timeout(TIMEOUT_MS)
2520
+ });
2521
+ if (!response.ok) return { value: null, status: response.status };
2522
+ return { value: await response.json(), status: response.status };
2523
+ } catch {
2524
+ return { value: null, status: null };
2525
+ }
2526
+ };
2527
+ var exposureCheck = (exposure) => {
2528
+ const beyondMachine = exposure.declared !== "loopback" && exposure.originOnMachine !== true;
2529
+ const gateOpen = beyondMachine ? exposure.gateOpen : 0;
2530
+ const facts = [
2531
+ `declared ${exposure.declared}`,
2532
+ `${exposure.originScheme} origin`,
2533
+ ...exposure.arrivedVia === "trusted-proxy" ? ["arrived via a trusted proxy"] : [],
2534
+ ...gateOpen === 0 ? [] : [`${gateOpen} gate items open`]
2535
+ ].join(" \xB7 ");
2536
+ const plainBeyondMachine = beyondMachine && exposure.originScheme === "http";
2537
+ return {
2538
+ label: "exposure",
2539
+ state: plainBeyondMachine ? "todo" : "ok",
2540
+ detail: facts,
2541
+ fix: plainBeyondMachine ? "serve it over https and set APP_URL to that origin" : gateOpen === 0 ? null : "mend operator exposure"
2542
+ };
2543
+ };
2544
+ var LOGIN_COMMANDS = {
2545
+ claude: "claude setup-token",
2546
+ codex: "codex login",
2547
+ github: "gh auth login"
2548
+ };
2549
+ var HARNESS_CLIS = [
2550
+ { command: "claude", provider: "claude" },
2551
+ { command: "codex", provider: "codex" },
2552
+ { command: "gh", provider: "github" }
2553
+ ];
2554
+ var notChecked = (label) => ({
2555
+ label,
2556
+ state: "todo",
2557
+ detail: "not checked",
2558
+ fix: null
2559
+ });
2560
+ var identityOf = (account) => {
2561
+ const meta = account.metadata;
2562
+ for (const key of ["login", "email", "accountEmail", "accountId"]) {
2563
+ const value = meta[key];
2564
+ if (typeof value === "string" && value !== "") return value;
2565
+ }
2566
+ return null;
2567
+ };
2568
+ var runChecks = async (config, probes) => {
2569
+ const checks = [];
2570
+ const health = await getJson(config, "/health");
2571
+ checks.push(
2572
+ health.value === null ? {
2573
+ label: "server",
2574
+ state: "failed",
2575
+ detail: `cannot reach ${config.url}`,
2576
+ fix: "start the Mend server"
2577
+ } : {
2578
+ label: "server",
2579
+ state: "ok",
2580
+ detail: `${config.url} \xB7 mend ${health.value.version}`,
2581
+ fix: null
2582
+ }
2583
+ );
2584
+ const projects = health.value !== null && config.token !== null ? await getJson(config, "/projects") : null;
2585
+ if (health.value === null) checks.push(notChecked("signed in"));
2586
+ else if (config.token === null) {
2587
+ checks.push({
2588
+ label: "signed in",
2589
+ state: "failed",
2590
+ detail: "no token saved",
2591
+ fix: "mend login"
2592
+ });
2593
+ } else if (projects !== null && projects.value !== null) {
2594
+ checks.push({ label: "signed in", state: "ok", detail: "token accepted", fix: null });
2595
+ } else {
2596
+ const status = projects === null ? null : projects.status;
2597
+ checks.push(
2598
+ status === 401 || status === 403 ? { label: "signed in", state: "failed", detail: "token rejected", fix: "mend login" } : {
2599
+ label: "signed in",
2600
+ state: "failed",
2601
+ detail: `GET /projects \u2192 ${status ?? "no answer"}`,
2602
+ fix: null
2603
+ }
2604
+ );
2605
+ }
2606
+ const signedIn = projects !== null && projects.value !== null;
2607
+ const connection = signedIn ? await getJson(config, "/sealant/connection") : null;
2608
+ if (connection === null) checks.push(notChecked("sealant"));
2609
+ else if (connection.value === null) {
2610
+ checks.push(
2611
+ connection.status === 404 ? notChecked("sealant") : {
2612
+ label: "sealant",
2613
+ state: "failed",
2614
+ detail: `GET /sealant/connection \u2192 ${connection.status ?? "no answer"}`,
2615
+ fix: null
2616
+ }
2617
+ );
2618
+ } else if (connection.value.status === "connected") {
2619
+ checks.push({
2620
+ label: "sealant",
2621
+ state: "ok",
2622
+ detail: `connected \xB7 ${connection.value.baseUrl}`,
2623
+ fix: null
2624
+ });
2625
+ } else {
2626
+ checks.push({
2627
+ label: "sealant",
2628
+ state: "failed",
2629
+ detail: `${connection.value.status} \xB7 ${connection.value.detail ?? connection.value.baseUrl}`,
2630
+ fix: null
2631
+ });
2632
+ }
2633
+ const identity = signedIn ? await getJson(config, "/me/sealant") : null;
2634
+ const platform = identity === null ? null : identity.value;
2635
+ for (const { provider } of HARNESS_CLIS) {
2636
+ if (platform === null) {
2637
+ checks.push(notChecked(provider));
2638
+ continue;
2639
+ }
2640
+ const account = platform.accounts.find((row) => row.provider === provider && row.name === "default") ?? platform.accounts.find((row) => row.provider === provider);
2641
+ if (account === void 0) {
2642
+ checks.push({
2643
+ label: provider,
2644
+ state: "todo",
2645
+ detail: "not connected",
2646
+ fix: `mend connect ${provider}`
2647
+ });
2648
+ continue;
2649
+ }
2650
+ const who = identityOf(account);
2651
+ checks.push(
2652
+ account.status === "active" ? {
2653
+ label: provider,
2654
+ state: "ok",
2655
+ detail: who === null ? "connected" : `connected \xB7 ${who}`,
2656
+ fix: null
2657
+ } : {
2658
+ label: provider,
2659
+ state: "todo",
2660
+ detail: account.status,
2661
+ fix: `mend connect ${provider}`
2662
+ }
2663
+ );
2664
+ }
2665
+ const grant = probes.claudeGrant();
2666
+ if (grant !== null) {
2667
+ const facts = claudeGrantFacts(grant);
2668
+ if (facts === null) {
2669
+ checks.push({
2670
+ label: "grant",
2671
+ state: "failed",
2672
+ detail: "unreadable",
2673
+ fix: "mend connect claude"
2674
+ });
2675
+ } else if (!facts.hasRefreshToken) {
2676
+ checks.push({
2677
+ label: "grant",
2678
+ state: "failed",
2679
+ detail: "signed out",
2680
+ fix: "mend connect claude"
2681
+ });
2682
+ } else if (facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now()) {
2683
+ checks.push({
2684
+ label: "grant",
2685
+ state: "todo",
2686
+ detail: `expired ${day(facts.refreshExpiresAt)}`,
2687
+ fix: "mend connect claude"
2688
+ });
2689
+ } else {
2690
+ checks.push({
2691
+ label: "grant",
2692
+ state: "ok",
2693
+ detail: facts.refreshExpiresAt === null ? "Mend's own" : `Mend's own \xB7 expires ${day(facts.refreshExpiresAt)}`,
2694
+ fix: null
2695
+ });
2696
+ }
2697
+ }
2698
+ const adopted = projects === null ? null : projects.value;
2699
+ if (adopted === null) checks.push(notChecked("projects"));
2700
+ else if (adopted.length === 0) {
2701
+ checks.push({ label: "projects", state: "todo", detail: "none adopted", fix: "mend adopt" });
2702
+ } else {
2703
+ checks.push({ label: "projects", state: "ok", detail: `${adopted.length} adopted`, fix: null });
2704
+ }
2705
+ for (const { command, provider } of HARNESS_CLIS) {
2706
+ const label = `${command} cli`;
2707
+ if (!probes.onPath(command)) {
2708
+ checks.push({ label, state: "todo", detail: "not on PATH", fix: null });
2709
+ continue;
2710
+ }
2711
+ const credential = probes.localCredential(provider);
2712
+ checks.push(
2713
+ credential === null ? {
2714
+ label,
2715
+ state: "todo",
2716
+ detail: "on PATH \xB7 no credential here",
2717
+ fix: LOGIN_COMMANDS[provider]
2718
+ } : { label, state: "ok", detail: "on PATH \xB7 credential present", fix: null }
2719
+ );
2720
+ }
2721
+ const machine = signedIn ? await getJson(config, "/machine") : null;
2722
+ const exposure = machine === null || machine.value === null ? void 0 : machine.value.exposure;
2723
+ checks.push(exposure === void 0 ? notChecked("exposure") : exposureCheck(exposure));
2724
+ return checks;
2725
+ };
2726
+ var onPath = (command) => {
2727
+ for (const directory of (process.env["PATH"] ?? "").split(path5.delimiter)) {
2728
+ if (directory === "") continue;
2729
+ try {
2730
+ fs5.accessSync(path5.join(directory, command), fs5.constants.X_OK);
2731
+ return true;
2732
+ } catch {
2733
+ }
2734
+ }
2735
+ return false;
2736
+ };
2737
+ var paintMark = (state, mark) => {
2738
+ if (process.stdout.isTTY !== true) return mark;
2739
+ const code = state === "ok" ? "32" : state === "todo" ? "33" : "31";
2740
+ return `\x1B[${code}m${mark}\x1B[0m`;
2741
+ };
2742
+ var doctorCommand = async (config, localCredential2, claudeGrant2) => {
2743
+ const checks = await runChecks(config, { localCredential: localCredential2, claudeGrant: claudeGrant2, onPath });
2744
+ for (const check of checks) process.stdout.write(`${formatCheck(check, paintMark)}
2745
+ `);
2746
+ if (checks.some((check) => check.state === "failed")) process.exitCode = 1;
2747
+ };
2748
+
2749
+ // src/dotfiles.ts
2750
+ import * as fs6 from "node:fs";
2751
+ import * as path6 from "node:path";
2752
+ var DOTFILE_CANDIDATES = [
2753
+ {
2754
+ group: "shell",
2755
+ paths: [
2756
+ ".zshrc",
2757
+ ".zshenv",
2758
+ ".zprofile",
2759
+ ".bashrc",
2760
+ ".bash_profile",
2761
+ ".profile",
2762
+ ".aliases",
2763
+ ".config/fish/config.fish",
2764
+ ".config/starship.toml"
2765
+ ]
2766
+ },
2767
+ {
2768
+ group: "git",
2769
+ paths: [".gitconfig", ".gitignore_global", ".config/git/config", ".config/git/ignore"]
2770
+ },
2771
+ {
2772
+ group: "editors",
2773
+ paths: [".vimrc", ".ideavimrc", ".editorconfig", ".config/helix/config.toml"]
2774
+ },
2775
+ {
2776
+ group: "terminal",
2777
+ paths: [".tmux.conf", ".config/tmux/tmux.conf", ".inputrc", ".dir_colors"]
2778
+ },
2779
+ {
2780
+ group: "tools",
2781
+ paths: [
2782
+ ".ripgreprc",
2783
+ ".config/bat/config",
2784
+ ".config/lazygit/config.yml",
2785
+ ".config/mise/config.toml",
2786
+ ".config/atuin/config.toml",
2787
+ ".config/zellij/config.kdl"
2788
+ ]
2789
+ }
2790
+ ];
2791
+ var MAX_FILE_BYTES = 1024 * 1024;
2792
+ var statFile = (home, relative4) => {
2793
+ try {
2794
+ const stat = fs6.statSync(path6.join(home, relative4));
2795
+ return stat.isFile() ? stat : null;
2796
+ } catch {
2797
+ return null;
2798
+ }
2799
+ };
2800
+ var scanDotfileCandidates = (home) => DOTFILE_CANDIDATES.flatMap(
2801
+ ({ group, paths }) => paths.flatMap((relative4) => {
2802
+ const stat = statFile(home, relative4);
2803
+ return stat === null ? [] : [{ path: relative4, group, bytes: stat.size }];
2804
+ })
2805
+ );
2806
+ var homeRelativePath = (home, requested) => {
2807
+ const relative4 = path6.relative(home, path6.resolve(home, requested));
2808
+ const outside = relative4 === "" || relative4 === ".." || relative4.startsWith(`..${path6.sep}`) || path6.isAbsolute(relative4);
2809
+ return outside ? null : relative4.split(path6.sep).join("/");
2810
+ };
2811
+ var readSyncFiles = (home, paths_) => {
2812
+ const files = [];
2813
+ for (const requested of paths_) {
2814
+ const relative4 = homeRelativePath(home, requested);
2815
+ if (relative4 === null) {
2816
+ return {
2817
+ error: `${requested} is not under ${home} \u2014 only files in your home directory sync`
2818
+ };
2819
+ }
2820
+ const stat = statFile(home, relative4);
2821
+ if (stat === null) {
2822
+ return { error: `${relative4} is not a file under ${home}` };
2823
+ }
2824
+ if (stat.size > MAX_FILE_BYTES) {
2825
+ return { error: `${relative4} is over 1MB \u2014 dotfiles are text; trim the selection` };
2826
+ }
2827
+ files.push({
2828
+ path: relative4,
2829
+ contentsBase64: fs6.readFileSync(path6.join(home, relative4)).toString("base64"),
2830
+ // eslint-disable-next-line no-bitwise -- permission bits are the point
2831
+ mode: (stat.mode & 511).toString(8)
2832
+ });
2833
+ }
2834
+ return { files };
2835
+ };
2836
+ var DOTFILES_MANAGERS = ["auto", "copy", "stow", "chezmoi"];
2837
+ var VALUE_FLAGS = ["--ref", "--subdirectory", "--manager"];
2838
+ var parseDotfilesRepoArgs = (args) => {
2839
+ if (args.includes("--clear")) {
2840
+ return args.length === 1 ? { kind: "clear" } : { kind: "error", error: "--clear takes no URL or other options" };
2841
+ }
2842
+ let url = null;
2843
+ const values = /* @__PURE__ */ new Map();
2844
+ let bootstrap = true;
2845
+ for (let index = 0; index < args.length; index += 1) {
2846
+ const arg = args[index] ?? "";
2847
+ if (arg === "--no-bootstrap") {
2848
+ bootstrap = false;
2849
+ continue;
2850
+ }
2851
+ const flag = VALUE_FLAGS.find((candidate) => candidate === arg);
2852
+ if (flag !== void 0) {
2853
+ const value = args[index + 1];
2854
+ if (value === void 0 || value.trim() === "" || value.startsWith("--")) {
2855
+ return { kind: "error", error: `${flag} needs a value` };
2856
+ }
2857
+ values.set(flag, value.trim());
2858
+ index += 1;
2859
+ continue;
2860
+ }
2861
+ if (arg.startsWith("-")) return { kind: "error", error: `unknown flag ${arg}` };
2862
+ if (url !== null) return { kind: "error", error: "one repository URL only" };
2863
+ url = arg.trim();
2864
+ }
2865
+ if (url === null || url === "") {
2866
+ return { kind: "error", error: "name the repository URL, or --clear to remove it" };
2867
+ }
2868
+ const requested = values.get("--manager") ?? "auto";
2869
+ const manager = DOTFILES_MANAGERS.find((candidate) => candidate === requested);
2870
+ if (manager === void 0) {
2871
+ return { kind: "error", error: `--manager must be one of ${DOTFILES_MANAGERS.join(", ")}` };
2872
+ }
2873
+ return {
2874
+ kind: "set",
2875
+ repository: {
2876
+ url,
2877
+ ref: values.get("--ref") ?? null,
2878
+ subdirectory: values.get("--subdirectory") ?? null,
2879
+ manager,
2880
+ bootstrap
2881
+ }
2882
+ };
2883
+ };
2884
+ var dotfilesRepositoryFacts = (repository) => [
2885
+ repository.ref ?? "default branch",
2886
+ ...repository.subdirectory === null ? [] : [`${repository.subdirectory}/`],
2887
+ `manager ${repository.manager}`,
2888
+ repository.bootstrap ? "install.sh on" : "install.sh off"
2889
+ ].join(" \xB7 ");
2890
+
2891
+ // src/env.ts
2892
+ var formatLoadReport = (report, paint6) => {
2893
+ const width = Math.max(
2894
+ 0,
2895
+ ...report.loaded.map((entry) => entry.name.length),
2896
+ ...report.rejected.map((entry) => entry.name.length)
2897
+ );
2898
+ const lines = [];
2899
+ for (const entry of report.loaded) {
2900
+ lines.push(
2901
+ ` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint6.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
2902
+ );
2903
+ }
2904
+ for (const entry of report.rejected) {
2905
+ lines.push(
2906
+ ` ${entry.name.padEnd(width)} ${paint6.warn("rejected")} ${paint6.dim(`\xB7 ${entry.reason}`)}`
2907
+ );
2908
+ }
2909
+ return lines;
2041
2910
  };
2042
- var manFileName = (doc) => doc === null ? "mend.1" : `mend-${doc.name.replace(/ /g, "-")}.1`;
2911
+
2912
+ // src/git-author.ts
2913
+ var parseGitAuthorArgs = (args) => {
2914
+ const clear = args.includes("--clear");
2915
+ const words = args.filter((arg) => !arg.startsWith("--"));
2916
+ if (clear) return words.length === 0 ? { kind: "clear" } : { kind: "usage" };
2917
+ if (words.length === 0) return { kind: "show" };
2918
+ const [name, email] = words;
2919
+ if (words.length !== 2 || name === void 0 || email === void 0) return { kind: "usage" };
2920
+ const issue = gitAuthorIssue({ name, email });
2921
+ if (issue !== null) return { kind: "invalid", message: `git author not saved \xB7 ${issue}` };
2922
+ const author = normalizeGitAuthor({ name, email });
2923
+ return { kind: "set", name: author.name, email: author.email };
2924
+ };
2925
+ var gitAuthorLine = (author) => `${author.name} <${author.email}> \xB7 ${author.source === "setting" ? "your setting" : "your account's name and email"}`;
2043
2926
 
2044
2927
  // src/landing.ts
2045
2928
  import { spawnSync as spawnSync2 } from "node:child_process";
2046
- import * as fs4 from "node:fs";
2047
- import * as os2 from "node:os";
2048
- import * as path4 from "node:path";
2929
+ import * as fs7 from "node:fs";
2930
+ import * as os3 from "node:os";
2931
+ import * as path7 from "node:path";
2049
2932
  var paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
2050
2933
  var dim = paint("2");
2051
2934
  var green = paint("32");
@@ -2283,9 +3166,9 @@ var gitRemotes = (cwd) => {
2283
3166
  var remoteForOrigin = (remotes, originUrl, cwd) => {
2284
3167
  if (originUrl === null) return null;
2285
3168
  const origin = normalizeRemoteUrl(originUrl);
2286
- const originPath = origin === null ? path4.resolve(originUrl.replace(/^file:\/\//, "")) : null;
3169
+ const originPath = origin === null ? path7.resolve(originUrl.replace(/^file:\/\//, "")) : null;
2287
3170
  return remotes.find(
2288
- (remote) => origin === null ? path4.resolve(cwd, remote.url.replace(/^file:\/\//, "")) === originPath : normalizeRemoteUrl(remote.url) === origin
3171
+ (remote) => origin === null ? path7.resolve(cwd, remote.url.replace(/^file:\/\//, "")) === originPath : normalizeRemoteUrl(remote.url) === origin
2289
3172
  ) ?? null;
2290
3173
  };
2291
3174
  var LOG_LINES = 10;
@@ -2308,10 +3191,10 @@ var fetchBundle = (cwd, bundle) => {
2308
3191
  const before = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
2309
3192
  const previous = before.status === 0 ? before.stdout.trim() : null;
2310
3193
  if (previous !== bundle.tip) {
2311
- const dir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "mend-pull-"));
3194
+ const dir = fs7.mkdtempSync(path7.join(os3.tmpdir(), "mend-pull-"));
2312
3195
  try {
2313
- const file = path4.join(dir, "change.bundle");
2314
- fs4.writeFileSync(file, bundle.bytes, { mode: 384 });
3196
+ const file = path7.join(dir, "change.bundle");
3197
+ fs7.writeFileSync(file, bundle.bytes, { mode: 384 });
2315
3198
  const fetched = git(cwd, [
2316
3199
  "fetch",
2317
3200
  "--no-tags",
@@ -2321,7 +3204,7 @@ var fetchBundle = (cwd, bundle) => {
2321
3204
  ]);
2322
3205
  if (fetched.status !== 0) return refusedWith(gitWords(fetched));
2323
3206
  } finally {
2324
- fs4.rmSync(dir, { recursive: true, force: true });
3207
+ fs7.rmSync(dir, { recursive: true, force: true });
2325
3208
  }
2326
3209
  }
2327
3210
  const after = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).stdout.trim();
@@ -2359,7 +3242,7 @@ var fetchedLines = (fetched) => {
2359
3242
  lines.push(`${dim(" switch to it")} git switch ${fetched.branch}`);
2360
3243
  return lines;
2361
3244
  };
2362
- var formatBytes = (bytes) => {
3245
+ var formatBytes2 = (bytes) => {
2363
3246
  if (bytes < 1024) return `${bytes} bytes`;
2364
3247
  const units = ["KiB", "MiB", "GiB"];
2365
3248
  let value = bytes / 1024;
@@ -2372,13 +3255,13 @@ var formatBytes = (bytes) => {
2372
3255
  };
2373
3256
  var numberField = (value, key) => {
2374
3257
  if (typeof value !== "object" || value === null || !(key in value)) return null;
2375
- const field = Reflect.get(value, key);
2376
- return typeof field === "number" ? field : null;
3258
+ const field2 = Reflect.get(value, key);
3259
+ return typeof field2 === "number" ? field2 : null;
2377
3260
  };
2378
- var stringField = (value, key) => {
3261
+ var stringField2 = (value, key) => {
2379
3262
  if (typeof value !== "object" || value === null || !(key in value)) return null;
2380
- const field = Reflect.get(value, key);
2381
- return typeof field === "string" ? field : null;
3263
+ const field2 = Reflect.get(value, key);
3264
+ return typeof field2 === "string" ? field2 : null;
2382
3265
  };
2383
3266
  var parseJson = (bytes) => {
2384
3267
  try {
@@ -2392,9 +3275,9 @@ var bundleRefusal = (downloaded) => {
2392
3275
  const size = numberField(body, "size");
2393
3276
  const limit = numberField(body, "limit");
2394
3277
  if (downloaded.status === 413 && size !== null && limit !== null) {
2395
- return `bundle not sent \xB7 ${formatBytes(size)} (${size} bytes) \xB7 the server's limit is ${formatBytes(limit)} (MEND_BUDGET_BUNDLE_BYTES) \xB7 nothing was fetched`;
3278
+ return `bundle not sent \xB7 ${formatBytes2(size)} (${size} bytes) \xB7 the server's limit is ${formatBytes2(limit)} (MEND_BUDGET_BUNDLE_BYTES) \xB7 nothing was fetched`;
2396
3279
  }
2397
- return stringField(body, "message") ?? `the bundle request answered ${downloaded.status}`;
3280
+ return stringField2(body, "message") ?? `the bundle request answered ${downloaded.status}`;
2398
3281
  };
2399
3282
  var bundleFactsOf = (downloaded) => {
2400
3283
  const branch = downloaded.header(BUNDLE_HEADERS.branch);
@@ -2439,8 +3322,8 @@ var pullCommand = async (api2, download2, args, cwd = process.cwd()) => {
2439
3322
  };
2440
3323
 
2441
3324
  // src/login.ts
2442
- import { spawn } from "node:child_process";
2443
- import * as os3 from "node:os";
3325
+ import { spawn as spawn2 } from "node:child_process";
3326
+ import * as os4 from "node:os";
2444
3327
 
2445
3328
  // src/pair.ts
2446
3329
  var paint2 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
@@ -2503,23 +3386,23 @@ var qrCommand = async (args) => {
2503
3386
  };
2504
3387
 
2505
3388
  // src/login.ts
2506
- var parseCliAuthStart = (json) => {
2507
- if (typeof json !== "object" || json === null) return null;
2508
- if (!("deviceCode" in json && "code" in json && "verifyPath" in json && "expiresAt" in json && "intervalSeconds" in json)) {
3389
+ var parseCliAuthStart = (json2) => {
3390
+ if (typeof json2 !== "object" || json2 === null) return null;
3391
+ if (!("deviceCode" in json2 && "code" in json2 && "verifyPath" in json2 && "expiresAt" in json2 && "intervalSeconds" in json2)) {
2509
3392
  return null;
2510
3393
  }
2511
- const { deviceCode, code, verifyPath, expiresAt, intervalSeconds } = json;
3394
+ const { deviceCode, code, verifyPath, expiresAt, intervalSeconds } = json2;
2512
3395
  if (typeof deviceCode !== "string" || typeof code !== "string" || typeof verifyPath !== "string" || typeof expiresAt !== "string" || typeof intervalSeconds !== "number") {
2513
3396
  return null;
2514
3397
  }
2515
3398
  return { deviceCode, code, verifyPath, expiresAt, intervalSeconds };
2516
3399
  };
2517
- var parseCliAuthPoll = (json) => {
2518
- if (typeof json !== "object" || json === null || !("status" in json)) return null;
2519
- if (json.status === "pending") return { status: "pending" };
2520
- if (json.status !== "approved") return null;
2521
- if (!("token" in json && "user" in json && "device" in json)) return null;
2522
- const { token, user, device } = json;
3400
+ var parseCliAuthPoll = (json2) => {
3401
+ if (typeof json2 !== "object" || json2 === null || !("status" in json2)) return null;
3402
+ if (json2.status === "pending") return { status: "pending" };
3403
+ if (json2.status !== "approved") return null;
3404
+ if (!("token" in json2 && "user" in json2 && "device" in json2)) return null;
3405
+ const { token, user, device } = json2;
2523
3406
  if (typeof token !== "string") return null;
2524
3407
  if (typeof user !== "object" || user === null) return null;
2525
3408
  if (!("id" in user && "name" in user && "email" in user)) return null;
@@ -2579,7 +3462,7 @@ var openBrowser = (url) => {
2579
3462
  const opener = browserCommand(process.platform);
2580
3463
  if (opener === null || process.stdout.isTTY !== true) return;
2581
3464
  try {
2582
- const child = spawn(opener.command, [...opener.args, url], {
3465
+ const child = spawn2(opener.command, [...opener.args, url], {
2583
3466
  stdio: "ignore",
2584
3467
  detached: true
2585
3468
  });
@@ -2600,21 +3483,21 @@ var post = async (url, body) => {
2600
3483
  return fail3(`cannot reach the Mend server at ${new URL(url).origin} \u2014 is it running?`);
2601
3484
  }
2602
3485
  const text = await response.text();
2603
- let json = null;
3486
+ let json2 = null;
2604
3487
  try {
2605
- json = JSON.parse(text);
3488
+ json2 = JSON.parse(text);
2606
3489
  } catch {
2607
3490
  }
2608
- return { status: response.status, json };
3491
+ return { status: response.status, json: json2 };
2609
3492
  };
2610
- var retryAfterSecondsOf = (json) => {
2611
- if (typeof json === "object" && json !== null && "retryAfterSeconds" in json) {
2612
- const seconds = Number(json.retryAfterSeconds);
3493
+ var retryAfterSecondsOf = (json2) => {
3494
+ if (typeof json2 === "object" && json2 !== null && "retryAfterSeconds" in json2) {
3495
+ const seconds = Number(json2.retryAfterSeconds);
2613
3496
  if (Number.isFinite(seconds) && seconds > 0) return seconds;
2614
3497
  }
2615
3498
  return 5;
2616
3499
  };
2617
- var sleep2 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
3500
+ var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
2618
3501
  var resolveServerUrl = async (args, deps) => {
2619
3502
  const given = takeFlagValue2(args, "--url") ?? deps.configuredUrl;
2620
3503
  if (given !== null) {
@@ -2632,7 +3515,7 @@ var resolveServerUrl = async (args, deps) => {
2632
3515
  };
2633
3516
  var loginCommand = async (args, deps) => {
2634
3517
  const base = await resolveServerUrl(args, deps);
2635
- const started = await post(`${base}/api/cli/auth`, { name: os3.hostname() });
3518
+ const started = await post(`${base}/api/cli/auth`, { name: os4.hostname() });
2636
3519
  if (started.status === 429) {
2637
3520
  return fail3(
2638
3521
  `the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
@@ -2683,8 +3566,8 @@ var loginCommand = async (args, deps) => {
2683
3566
  };
2684
3567
 
2685
3568
  // src/organization.ts
2686
- import * as fs5 from "node:fs";
2687
- import * as path5 from "node:path";
3569
+ import * as fs8 from "node:fs";
3570
+ import * as path8 from "node:path";
2688
3571
  var paint4 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
2689
3572
  var dim4 = paint4("2");
2690
3573
  var green4 = paint4("32");
@@ -2748,7 +3631,7 @@ var scanFolder = (dir) => {
2748
3631
  const files = [];
2749
3632
  const skipped = [];
2750
3633
  const walk = (relative4) => {
2751
- const entries = fs5.readdirSync(path5.join(dir, relative4), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
3634
+ const entries = fs8.readdirSync(path8.join(dir, relative4), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
2752
3635
  for (const entry of entries) {
2753
3636
  const child = relative4 === "" ? entry.name : `${relative4}/${entry.name}`;
2754
3637
  if (entry.isSymbolicLink()) {
@@ -2757,7 +3640,7 @@ var scanFolder = (dir) => {
2757
3640
  if (SKIPPED_DIRECTORIES.has(entry.name)) skipped.push({ path: child, reason: "skipped" });
2758
3641
  else walk(child);
2759
3642
  } else if (entry.isFile()) {
2760
- const size = fs5.statSync(path5.join(dir, child)).size;
3643
+ const size = fs8.statSync(path8.join(dir, child)).size;
2761
3644
  if (size > FOLDER_FILE_BYTES) skipped.push({ path: child, reason: "over 1 MiB" });
2762
3645
  else files.push({ path: child, size });
2763
3646
  }
@@ -2810,7 +3693,7 @@ var folderCommand = async (api2, args) => {
2810
3693
  }
2811
3694
  case "push": {
2812
3695
  if (dir === void 0) return fail4("usage: mend folder push <name> <dir> [--replace]");
2813
- if (!fs5.existsSync(dir) || !fs5.statSync(dir).isDirectory()) {
3696
+ if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) {
2814
3697
  return fail4(`${dir} is not a directory`);
2815
3698
  }
2816
3699
  const folder = await folderNamed(api2, name);
@@ -2825,7 +3708,7 @@ var folderCommand = async (api2, args) => {
2825
3708
  files: batch.map((file) => ({
2826
3709
  path: file.path,
2827
3710
  // Read one batch at a time, so a large tree never sits in memory at once.
2828
- contentsBase64: fs5.readFileSync(path5.join(dir, file.path)).toString("base64")
3711
+ contentsBase64: fs8.readFileSync(path8.join(dir, file.path)).toString("base64")
2829
3712
  })),
2830
3713
  // --replace empties the folder with the first batch; the rest add to it.
2831
3714
  merge: !(replace && index === 0)
@@ -3009,8 +3892,8 @@ var operatorCommand = async (api2, tryApi, baseUrl, args) => {
3009
3892
  import { createHmac, randomBytes } from "node:crypto";
3010
3893
  import { readFile } from "node:fs/promises";
3011
3894
  import * as net2 from "node:net";
3012
- import * as os5 from "node:os";
3013
- import * as path8 from "node:path";
3895
+ import * as os6 from "node:os";
3896
+ import * as path10 from "node:path";
3014
3897
 
3015
3898
  // src/server-docker-volumes.ts
3016
3899
  import { createHash as createHash2 } from "node:crypto";
@@ -3053,14 +3936,14 @@ var docker = async (runtime, context, args) => {
3053
3936
  }
3054
3937
  };
3055
3938
  var listNames = async (runtime, context, kind, project2) => {
3056
- const field = kind === "container" ? "Names" : "Name";
3939
+ const field2 = kind === "container" ? "Names" : "Name";
3057
3940
  const output = await docker(runtime, context, [
3058
3941
  kind,
3059
3942
  "ls",
3060
3943
  ...kind === "container" ? ["--all"] : [],
3061
3944
  ...project2 === void 0 ? [] : ["--filter", `label=com.docker.compose.project=${project2}`],
3062
3945
  "--format",
3063
- `{{json .${field}}}`
3946
+ `{{json .${field2}}}`
3064
3947
  ]);
3065
3948
  if (output._tag === "error") return output;
3066
3949
  const names = [];
@@ -3226,157 +4109,11 @@ var ownership = async (runtime, input, mode) => {
3226
4109
  var claimServerDockerVolumes = (runtime, input) => ownership(runtime, input, "claim");
3227
4110
  var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "verify");
3228
4111
 
3229
- // src/server-runtime.ts
3230
- import { spawn as spawn2 } from "node:child_process";
3231
- import * as fs6 from "node:fs";
3232
- import * as path6 from "node:path";
3233
- var serverProcessDeadlines = {
3234
- ordinary: 3e4,
3235
- pull: 10 * 6e4,
3236
- startup: 3 * 6e4,
3237
- stop: 9e4,
3238
- dump: 15 * 6e4,
3239
- composeWaitSeconds: 120
3240
- };
3241
- var runServerProcess = (command, args, parentEnvironment, options = {}) => {
3242
- const timeoutMs = options?.timeoutMs ?? serverProcessDeadlines.ordinary;
3243
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
3244
- return Promise.resolve({
3245
- status: null,
3246
- stdout: "",
3247
- stderr: "",
3248
- error: "Invalid process timeout"
3249
- });
3250
- }
3251
- if (options.stdoutFile !== void 0 && options.stdout === "inherit") {
3252
- return Promise.resolve({
3253
- status: null,
3254
- stdout: "",
3255
- stderr: "",
3256
- error: "Invalid process output options"
3257
- });
3258
- }
3259
- const env = {};
3260
- for (const key of [
3261
- "PATH",
3262
- "HOME",
3263
- "USER",
3264
- "LOGNAME",
3265
- "XDG_CONFIG_HOME",
3266
- "XDG_RUNTIME_DIR",
3267
- "DOCKER_CONFIG",
3268
- "SSH_AUTH_SOCK",
3269
- "TMPDIR",
3270
- "TMP",
3271
- "TEMP"
3272
- ]) {
3273
- if (parentEnvironment[key] !== void 0) env[key] = parentEnvironment[key];
3274
- }
3275
- return new Promise((resolve6) => {
3276
- let fd;
3277
- try {
3278
- if (options.stdoutFile !== void 0) fd = fs6.openSync(options.stdoutFile, "wx", 384);
3279
- const child = spawn2(command, [...args], {
3280
- env,
3281
- detached: process.platform !== "win32",
3282
- stdio: ["ignore", fd ?? (options.stdout === "inherit" ? "inherit" : "pipe"), "pipe"]
3283
- });
3284
- let stdout = "";
3285
- let stderr = "";
3286
- let stdoutBytes = 0;
3287
- let stderrBytes = 0;
3288
- let failure2;
3289
- const terminate = () => {
3290
- if (process.platform === "win32" || child.pid === void 0) {
3291
- child.kill("SIGKILL");
3292
- return;
3293
- }
3294
- try {
3295
- process.kill(-child.pid, "SIGKILL");
3296
- } catch (cause) {
3297
- if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) {
3298
- failure2 = "Could not terminate the process group; retain the installation lock until its processes stop.";
3299
- child.kill("SIGKILL");
3300
- }
3301
- }
3302
- };
3303
- child.stdout?.on("data", (chunk) => {
3304
- stdoutBytes += chunk.length;
3305
- if (stdoutBytes <= 4 * 1024 * 1024) stdout += chunk.toString();
3306
- else {
3307
- failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
3308
- terminate();
3309
- }
3310
- });
3311
- child.stderr?.on("data", (chunk) => {
3312
- stderrBytes += chunk.length;
3313
- if (stderrBytes <= 64 * 1024) stderr += chunk.toString();
3314
- else {
3315
- failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
3316
- terminate();
3317
- }
3318
- });
3319
- const timer = setTimeout(() => {
3320
- failure2 ??= `Process timed out after ${timeoutMs}ms`;
3321
- terminate();
3322
- }, timeoutMs);
3323
- child.once("error", (error) => {
3324
- failure2 ??= error.message;
3325
- });
3326
- child.once("close", (status) => {
3327
- globalThis.clearTimeout(timer);
3328
- try {
3329
- if (fd !== void 0) fs6.fsyncSync(fd);
3330
- } catch {
3331
- failure2 ??= "Could not durably write process output.";
3332
- } finally {
3333
- try {
3334
- if (fd !== void 0) fs6.closeSync(fd);
3335
- } catch {
3336
- failure2 ??= "Could not close process output file.";
3337
- }
3338
- }
3339
- resolve6({
3340
- status: failure2 === void 0 ? status : null,
3341
- stdout,
3342
- stderr,
3343
- ...failure2 === void 0 ? {} : { error: failure2 }
3344
- });
3345
- });
3346
- } catch (cause) {
3347
- try {
3348
- if (fd !== void 0) fs6.closeSync(fd);
3349
- } catch {
3350
- }
3351
- resolve6({
3352
- status: null,
3353
- stdout: "",
3354
- stderr: "",
3355
- error: cause instanceof Error ? cause.message : "Could not start process."
3356
- });
3357
- }
3358
- });
3359
- };
3360
- var serverComposeArgs = (installation, command) => [
3361
- "--context",
3362
- installation.dockerContext,
3363
- "compose",
3364
- "--project-name",
3365
- "mend",
3366
- "--project-directory",
3367
- installation.directory,
3368
- "--env-file",
3369
- path6.join(installation.directory, "server.env"),
3370
- "-f",
3371
- path6.join(installation.directory, "compose.yaml"),
3372
- ...command
3373
- ];
3374
-
3375
4112
  // src/server-store.ts
3376
4113
  import { randomUUID } from "node:crypto";
3377
- import * as fs7 from "node:fs";
3378
- import * as os4 from "node:os";
3379
- import * as path7 from "node:path";
4114
+ import * as fs9 from "node:fs";
4115
+ import * as os5 from "node:os";
4116
+ import * as path9 from "node:path";
3380
4117
  var ServerStoreError = class extends Error {
3381
4118
  _tag = "ServerStoreError";
3382
4119
  };
@@ -3397,28 +4134,28 @@ var attempt = (operation) => {
3397
4134
  var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
3398
4135
  var readOptional = (file) => {
3399
4136
  try {
3400
- return fs7.readFileSync(file, "utf8");
4137
+ return fs9.readFileSync(file, "utf8");
3401
4138
  } catch (cause) {
3402
4139
  if (hasCode(cause, "ENOENT")) return null;
3403
4140
  throw cause;
3404
4141
  }
3405
4142
  };
3406
4143
  var syncDirectory = (directory) => {
3407
- const fd = fs7.openSync(directory, "r");
4144
+ const fd = fs9.openSync(directory, "r");
3408
4145
  try {
3409
- fs7.fsyncSync(fd);
4146
+ fs9.fsyncSync(fd);
3410
4147
  } finally {
3411
- fs7.closeSync(fd);
4148
+ fs9.closeSync(fd);
3412
4149
  }
3413
4150
  };
3414
4151
  var writeDurable = (file, content, mode = 384) => {
3415
- const fd = fs7.openSync(file, "wx", mode);
4152
+ const fd = fs9.openSync(file, "wx", mode);
3416
4153
  try {
3417
- fs7.writeFileSync(fd, content, "utf8");
3418
- fs7.fchmodSync(fd, mode);
3419
- fs7.fsyncSync(fd);
4154
+ fs9.writeFileSync(fd, content, "utf8");
4155
+ fs9.fchmodSync(fd, mode);
4156
+ fs9.fsyncSync(fd);
3420
4157
  } finally {
3421
- fs7.closeSync(fd);
4158
+ fs9.closeSync(fd);
3422
4159
  }
3423
4160
  };
3424
4161
  var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
@@ -3438,11 +4175,11 @@ var parseLockOwner = (raw) => {
3438
4175
  };
3439
4176
  var describeOwner = (lockDir) => {
3440
4177
  try {
3441
- const raw = JSON.parse(fs7.readFileSync(path7.join(lockDir, "owner.json"), "utf8"));
4178
+ const raw = JSON.parse(fs9.readFileSync(path9.join(lockDir, "owner.json"), "utf8"));
3442
4179
  const owner = parseLockOwner(raw);
3443
4180
  if (owner === null) return "owner metadata is invalid";
3444
4181
  const label = `PID ${owner.pid} on ${owner.hostname}`;
3445
- if (owner.hostname !== os4.hostname()) return label;
4182
+ if (owner.hostname !== os5.hostname()) return label;
3446
4183
  try {
3447
4184
  process.kill(owner.pid, 0);
3448
4185
  return `${label} is still live`;
@@ -3458,28 +4195,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
3458
4195
  );
3459
4196
  var acquireLock = (configDir, create) => {
3460
4197
  if (create) {
3461
- fs7.mkdirSync(configDir, { recursive: true, mode: 448 });
3462
- fs7.chmodSync(configDir, 448);
3463
- } else if (!fs7.existsSync(configDir)) {
4198
+ fs9.mkdirSync(configDir, { recursive: true, mode: 448 });
4199
+ fs9.chmodSync(configDir, 448);
4200
+ } else if (!fs9.existsSync(configDir)) {
3464
4201
  throw new ServerStoreError(
3465
4202
  "No Mend server is configured. Run mend server setup explicitly to install one."
3466
4203
  );
3467
4204
  }
3468
- const lockDir = path7.join(configDir, "server.lock");
4205
+ const lockDir = path9.join(configDir, "server.lock");
3469
4206
  try {
3470
- fs7.mkdirSync(lockDir, { mode: 448 });
4207
+ fs9.mkdirSync(lockDir, { mode: 448 });
3471
4208
  } catch (cause) {
3472
4209
  if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
3473
4210
  throw cause;
3474
4211
  }
3475
- const owned = fs7.statSync(lockDir);
3476
- const ownerFile = path7.join(lockDir, "owner.json");
3477
- const owner = `${JSON.stringify({ pid: process.pid, hostname: os4.hostname(), token: randomUUID() })}
4212
+ const owned = fs9.statSync(lockDir);
4213
+ const ownerFile = path9.join(lockDir, "owner.json");
4214
+ const owner = `${JSON.stringify({ pid: process.pid, hostname: os5.hostname(), token: randomUUID() })}
3478
4215
  `;
3479
4216
  let open = true;
3480
4217
  const assertOwned = () => {
3481
- const current = fs7.statSync(lockDir);
3482
- if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs7.readFileSync(ownerFile, "utf8") !== owner) {
4218
+ const current = fs9.statSync(lockDir);
4219
+ if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs9.readFileSync(ownerFile, "utf8") !== owner) {
3483
4220
  throw new ServerStoreError(
3484
4221
  "Server lock ownership was lost. Stop and check for another server command before retrying."
3485
4222
  );
@@ -3495,8 +4232,8 @@ var acquireLock = (configDir, create) => {
3495
4232
  release: () => {
3496
4233
  const result = attempt(() => {
3497
4234
  assertOwned();
3498
- fs7.unlinkSync(ownerFile);
3499
- fs7.rmdirSync(lockDir);
4235
+ fs9.unlinkSync(ownerFile);
4236
+ fs9.rmdirSync(lockDir);
3500
4237
  });
3501
4238
  open = false;
3502
4239
  return result;
@@ -3505,7 +4242,7 @@ var acquireLock = (configDir, create) => {
3505
4242
  };
3506
4243
  var readIdentity = (paths) => {
3507
4244
  const identity = readOptional(paths.identity);
3508
- if (identity === null && (fs7.existsSync(paths.active) || fs7.existsSync(paths.generations) && fs7.readdirSync(paths.generations).length > 0)) {
4245
+ if (identity === null && (fs9.existsSync(paths.active) || fs9.existsSync(paths.generations) && fs9.readdirSync(paths.generations).length > 0)) {
3509
4246
  throw new ServerStoreError(
3510
4247
  "Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
3511
4248
  );
@@ -3515,7 +4252,7 @@ var readIdentity = (paths) => {
3515
4252
  var readActive = (paths) => {
3516
4253
  let target;
3517
4254
  try {
3518
- target = fs7.readlinkSync(paths.active);
4255
+ target = fs9.readlinkSync(paths.active);
3519
4256
  } catch (cause) {
3520
4257
  if (hasCode(cause, "ENOENT")) return null;
3521
4258
  throw cause;
@@ -3525,13 +4262,13 @@ var readActive = (paths) => {
3525
4262
  "Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
3526
4263
  );
3527
4264
  }
3528
- const directory = path7.join(paths.configDir, target);
4265
+ const directory = path9.join(paths.configDir, target);
3529
4266
  const files = {
3530
- identity: fs7.readFileSync(path7.join(directory, fileNames.identity), "utf8"),
3531
- config: fs7.readFileSync(path7.join(directory, fileNames.config), "utf8"),
3532
- env: fs7.readFileSync(path7.join(directory, fileNames.env), "utf8"),
3533
- compose: fs7.readFileSync(path7.join(directory, fileNames.compose), "utf8"),
3534
- postgresInit: fs7.readFileSync(path7.join(directory, fileNames.postgresInit), "utf8")
4267
+ identity: fs9.readFileSync(path9.join(directory, fileNames.identity), "utf8"),
4268
+ config: fs9.readFileSync(path9.join(directory, fileNames.config), "utf8"),
4269
+ env: fs9.readFileSync(path9.join(directory, fileNames.env), "utf8"),
4270
+ compose: fs9.readFileSync(path9.join(directory, fileNames.compose), "utf8"),
4271
+ postgresInit: fs9.readFileSync(path9.join(directory, fileNames.postgresInit), "utf8")
3535
4272
  };
3536
4273
  if (readIdentity(paths) !== files.identity) {
3537
4274
  throw new ServerStoreError(
@@ -3541,20 +4278,20 @@ var readActive = (paths) => {
3541
4278
  return { directory, files };
3542
4279
  };
3543
4280
  var publishIdentity = (paths, identity) => {
3544
- const temporary = path7.join(paths.configDir, `.identity-${randomUUID()}`);
4281
+ const temporary = path9.join(paths.configDir, `.identity-${randomUUID()}`);
3545
4282
  writeDurable(temporary, identity);
3546
- fs7.linkSync(temporary, paths.identity);
4283
+ fs9.linkSync(temporary, paths.identity);
3547
4284
  syncDirectory(paths.configDir);
3548
- fs7.unlinkSync(temporary);
4285
+ fs9.unlinkSync(temporary);
3549
4286
  };
3550
4287
  var prepareGeneration = (paths, files) => {
3551
- fs7.mkdirSync(paths.generations, { recursive: true, mode: 448 });
4288
+ fs9.mkdirSync(paths.generations, { recursive: true, mode: 448 });
3552
4289
  const generationName = `gen-${randomUUID()}`;
3553
- const directory = path7.join(paths.generations, generationName);
3554
- fs7.mkdirSync(directory, { mode: 448 });
4290
+ const directory = path9.join(paths.generations, generationName);
4291
+ fs9.mkdirSync(directory, { mode: 448 });
3555
4292
  for (const key of fileKeys) {
3556
4293
  writeDurable(
3557
- path7.join(directory, fileNames[key]),
4294
+ path9.join(directory, fileNames[key]),
3558
4295
  files[key],
3559
4296
  key === "postgresInit" ? 493 : 384
3560
4297
  );
@@ -3565,18 +4302,18 @@ var prepareGeneration = (paths, files) => {
3565
4302
  return { directory, files };
3566
4303
  };
3567
4304
  var activateGeneration = (paths, generation) => {
3568
- const relative4 = path7.relative(paths.configDir, generation.directory);
3569
- if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative4) || readIdentity(paths) !== fs7.readFileSync(path7.join(generation.directory, "identity.env"), "utf8")) {
4305
+ const relative4 = path9.relative(paths.configDir, generation.directory);
4306
+ if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative4) || readIdentity(paths) !== fs9.readFileSync(path9.join(generation.directory, "identity.env"), "utf8")) {
3570
4307
  throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
3571
4308
  }
3572
4309
  for (const key of fileKeys) {
3573
- if (fs7.readFileSync(path7.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
4310
+ if (fs9.readFileSync(path9.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
3574
4311
  throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
3575
4312
  }
3576
4313
  }
3577
- const pointer = path7.join(paths.configDir, `.active-${randomUUID()}`);
3578
- fs7.symlinkSync(relative4, pointer);
3579
- fs7.renameSync(pointer, paths.active);
4314
+ const pointer = path9.join(paths.configDir, `.active-${randomUUID()}`);
4315
+ fs9.symlinkSync(relative4, pointer);
4316
+ fs9.renameSync(pointer, paths.active);
3580
4317
  syncDirectory(paths.configDir);
3581
4318
  };
3582
4319
  var prepareFiles = (paths, files) => {
@@ -3585,7 +4322,7 @@ var prepareFiles = (paths, files) => {
3585
4322
  throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
3586
4323
  }
3587
4324
  const active = readActive(paths);
3588
- if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs7.statSync(path7.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
4325
+ if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs9.statSync(path9.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
3589
4326
  return active;
3590
4327
  if (identity === null) publishIdentity(paths, files.identity);
3591
4328
  return prepareGeneration(paths, files);
@@ -3598,9 +4335,9 @@ var commitGeneration = (paths, files) => {
3598
4335
  var createStore = (configDir, lock) => {
3599
4336
  const paths = {
3600
4337
  configDir,
3601
- identity: path7.join(configDir, "identity.env"),
3602
- generations: path7.join(configDir, "generations"),
3603
- active: path7.join(configDir, "active")
4338
+ identity: path9.join(configDir, "identity.env"),
4339
+ generations: path9.join(configDir, "generations"),
4340
+ active: path9.join(configDir, "active")
3604
4341
  };
3605
4342
  const whileOwned = (operation) => attempt(() => {
3606
4343
  lock.assertOwned();
@@ -3613,13 +4350,13 @@ var createStore = (configDir, lock) => {
3613
4350
  prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
3614
4351
  activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
3615
4352
  createBackup: (previous, target) => whileOwned(() => {
3616
- const backups = path7.join(configDir, "backups");
3617
- fs7.mkdirSync(backups, { recursive: true, mode: 448 });
3618
- fs7.chmodSync(backups, 448);
3619
- const directory = path7.join(backups, `upgrade-${randomUUID()}`);
3620
- fs7.mkdirSync(directory, { mode: 448 });
4353
+ const backups = path9.join(configDir, "backups");
4354
+ fs9.mkdirSync(backups, { recursive: true, mode: 448 });
4355
+ fs9.chmodSync(backups, 448);
4356
+ const directory = path9.join(backups, `upgrade-${randomUUID()}`);
4357
+ fs9.mkdirSync(directory, { mode: 448 });
3621
4358
  writeDurable(
3622
- path7.join(directory, "recovery.json"),
4359
+ path9.join(directory, "recovery.json"),
3623
4360
  `${JSON.stringify(
3624
4361
  {
3625
4362
  previousGeneration: previous.directory,
@@ -3635,21 +4372,21 @@ var createStore = (configDir, lock) => {
3635
4372
  syncDirectory(directory);
3636
4373
  syncDirectory(backups);
3637
4374
  syncDirectory(configDir);
3638
- const partialFile = path7.join(directory, "database.sql.partial");
4375
+ const partialFile = path9.join(directory, "database.sql.partial");
3639
4376
  return {
3640
4377
  directory,
3641
4378
  partialFile,
3642
4379
  complete: () => whileOwned(() => {
3643
- if (fs7.statSync(partialFile).size === 0)
4380
+ if (fs9.statSync(partialFile).size === 0)
3644
4381
  throw new ServerStoreError("Database backup is empty; refusing target startup.");
3645
- const fd = fs7.openSync(partialFile, "r");
4382
+ const fd = fs9.openSync(partialFile, "r");
3646
4383
  try {
3647
- fs7.fchmodSync(fd, 384);
3648
- fs7.fsyncSync(fd);
4384
+ fs9.fchmodSync(fd, 384);
4385
+ fs9.fsyncSync(fd);
3649
4386
  } finally {
3650
- fs7.closeSync(fd);
4387
+ fs9.closeSync(fd);
3651
4388
  }
3652
- fs7.renameSync(partialFile, path7.join(directory, "database.sql"));
4389
+ fs9.renameSync(partialFile, path9.join(directory, "database.sql"));
3653
4390
  syncDirectory(directory);
3654
4391
  })
3655
4392
  };
@@ -3658,7 +4395,7 @@ var createStore = (configDir, lock) => {
3658
4395
  };
3659
4396
  var refuseFlatLayout = (configDir) => {
3660
4397
  if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
3661
- (name) => fs7.existsSync(path7.join(configDir, name))
4398
+ (name) => fs9.existsSync(path9.join(configDir, name))
3662
4399
  )) {
3663
4400
  throw new ServerStoreError(
3664
4401
  "Unreleased flat server configuration found. Preserve its credentials and volumes; migrate it to the generation layout before retrying. Setup will not replace this identity."
@@ -3666,7 +4403,7 @@ var refuseFlatLayout = (configDir) => {
3666
4403
  }
3667
4404
  };
3668
4405
  var withServerStore = async (directory, operation, options = {}) => {
3669
- const configDir = path7.resolve(directory);
4406
+ const configDir = path9.resolve(directory);
3670
4407
  let lock;
3671
4408
  let result;
3672
4409
  try {
@@ -3676,16 +4413,16 @@ var withServerStore = async (directory, operation, options = {}) => {
3676
4413
  } catch (cause) {
3677
4414
  result = { _tag: "error", error: storeError(cause) };
3678
4415
  } finally {
3679
- const release = lock?.release();
3680
- if (release?._tag === "error") result = release;
4416
+ const release2 = lock?.release();
4417
+ if (release2?._tag === "error") result = release2;
3681
4418
  }
3682
4419
  return result;
3683
4420
  };
3684
4421
 
3685
4422
  // src/version.ts
3686
- import * as fs8 from "node:fs";
4423
+ import * as fs10 from "node:fs";
3687
4424
  var cliVersion = () => {
3688
- const raw = fs8.readFileSync(new URL("../package.json", import.meta.url), "utf8");
4425
+ const raw = fs10.readFileSync(new URL("../package.json", import.meta.url), "utf8");
3689
4426
  const parsed = JSON.parse(raw);
3690
4427
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
3691
4428
  const version = parsed.version;
@@ -3855,7 +4592,7 @@ var parseHttpOrigin = (input, label) => {
3855
4592
  return parsed.origin;
3856
4593
  };
3857
4594
  var isLoopbackBind = (bind) => bind === "127.0.0.1" || bind === "::1" || bind.startsWith("127.");
3858
- var isLoopbackHost = (hostname6) => hostname6 === "localhost" || hostname6 === "::1" || hostname6 === "[::1]" || hostname6.startsWith("127.");
4595
+ var isLoopbackHost = (hostname7) => hostname7 === "localhost" || hostname7 === "::1" || hostname7 === "[::1]" || hostname7.startsWith("127.");
3859
4596
  var resolveAppUrl = (existing, options, appPort) => {
3860
4597
  const oldLocalUrl = existing === null ? null : `http://localhost:${existing.appPort}`;
3861
4598
  const portChangedOnLocalhost = options.appPort !== void 0 && existing?.appUrl === oldLocalUrl;
@@ -4317,6 +5054,30 @@ var readServerInstallation = (store) => {
4317
5054
  };
4318
5055
  }
4319
5056
  };
5057
+ var envKeyNames = (env) => env.split("\n").flatMap((line) => {
5058
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/.exec(line);
5059
+ return match?.[1] === void 0 ? [] : [match[1]];
5060
+ });
5061
+ var readServerInstallationFacts = async (configDir) => {
5062
+ const result = await withServerStore(
5063
+ configDir,
5064
+ async (store) => {
5065
+ const installation = storeValue(readServerInstallation(store));
5066
+ if (installation === null) return null;
5067
+ const generation = storeValue(store.readActive());
5068
+ if (generation === null) return null;
5069
+ return {
5070
+ directory: installation.directory,
5071
+ config: installation.config,
5072
+ compose: generation.files.compose,
5073
+ envKeys: envKeyNames(generation.files.env)
5074
+ };
5075
+ },
5076
+ { create: false }
5077
+ );
5078
+ if (result._tag === "error") throw result.error;
5079
+ return result.value;
5080
+ };
4320
5081
  var persistSetup = (store, config, secrets, assets) => storeValue(
4321
5082
  store.prepare({
4322
5083
  identity: renderIdentity(secrets),
@@ -4364,7 +5125,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
4364
5125
  return version;
4365
5126
  };
4366
5127
  var parseDockerSocketPath = (socket) => {
4367
- if (!path8.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
5128
+ if (!path10.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
4368
5129
  throw setupError(
4369
5130
  "--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
4370
5131
  );
@@ -4390,8 +5151,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
4390
5151
  if (options.assetsDir !== void 0) {
4391
5152
  try {
4392
5153
  const [compose2, postgresInit2] = await Promise.all([
4393
- readFile(path8.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
4394
- readFile(path8.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
5154
+ readFile(path10.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
5155
+ readFile(path10.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
4395
5156
  ]);
4396
5157
  validateComposeAsset(compose2);
4397
5158
  validatePostgresAsset(postgresInit2);
@@ -4904,10 +5665,10 @@ var manageServer = async (command, args, runtime, store) => {
4904
5665
  };
4905
5666
  var nodeServerRuntime = () => {
4906
5667
  const environment = { ...process.env };
4907
- const home = os5.homedir();
4908
- const configHome = environment["XDG_CONFIG_HOME"] ?? path8.join(home, ".config");
5668
+ const home = os6.homedir();
5669
+ const configHome = environment["XDG_CONFIG_HOME"] ?? path10.join(home, ".config");
4909
5670
  return {
4910
- configDir: path8.join(configHome, "mend"),
5671
+ configDir: path10.join(configHome, "mend"),
4911
5672
  platform: process.platform,
4912
5673
  cliVersion: cliVersion(),
4913
5674
  run: (command, args, options) => runServerProcess(command, args, environment, options),
@@ -4924,7 +5685,7 @@ var nodeServerRuntime = () => {
4924
5685
  }
4925
5686
  },
4926
5687
  randomBytes,
4927
- sleep: (milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)),
5688
+ sleep: (milliseconds) => new Promise((resolve7) => setTimeout(resolve7, milliseconds)),
4928
5689
  writeLine: (line) => process.stdout.write(`${line}
4929
5690
  `)
4930
5691
  };
@@ -5146,9 +5907,9 @@ var touchesSessionServices = (payload, sessionId) => {
5146
5907
  return false;
5147
5908
  }
5148
5909
  if (typeof parsed !== "object" || parsed === null) return false;
5149
- const type = "type" in parsed ? parsed.type : void 0;
5910
+ const type2 = "type" in parsed ? parsed.type : void 0;
5150
5911
  const session = "sessionId" in parsed ? parsed.sessionId : void 0;
5151
- return (type === "session-process" || type === "session") && session === sessionId;
5912
+ return (type2 === "session-process" || type2 === "session") && session === sessionId;
5152
5913
  };
5153
5914
  var pumpConnection = (socket, url) => {
5154
5915
  socket.pause();
@@ -5174,7 +5935,7 @@ var pumpConnection = (socket, url) => {
5174
5935
  () => socket.destroy()
5175
5936
  );
5176
5937
  };
5177
- var listenLocal = (server, port, fallback) => new Promise((resolve6, reject) => {
5938
+ var listenLocal = (server, port, fallback) => new Promise((resolve7, reject) => {
5178
5939
  const attempt2 = (candidate, lastTry) => {
5179
5940
  const onError = (error) => {
5180
5941
  if (!lastTry) {
@@ -5187,7 +5948,7 @@ var listenLocal = (server, port, fallback) => new Promise((resolve6, reject) =>
5187
5948
  server.listen(candidate, "127.0.0.1", () => {
5188
5949
  server.removeListener("error", onError);
5189
5950
  const address = server.address();
5190
- resolve6(typeof address === "object" && address !== null ? address.port : candidate);
5951
+ resolve7(typeof address === "object" && address !== null ? address.port : candidate);
5191
5952
  });
5192
5953
  };
5193
5954
  attempt2(port, !fallback || port === 0);
@@ -5216,7 +5977,7 @@ var serverEvents = (config) => async (handlers, signal) => {
5216
5977
  if (signal.aborted) return;
5217
5978
  }
5218
5979
  connected = true;
5219
- await new Promise((resolve6) => setTimeout(resolve6, 2e3));
5980
+ await new Promise((resolve7) => setTimeout(resolve7, 2e3));
5220
5981
  }
5221
5982
  };
5222
5983
  var createServiceTunnels = (options) => {
@@ -5359,8 +6120,8 @@ var createServiceTunnels = (options) => {
5359
6120
  };
5360
6121
 
5361
6122
  // src/skills.ts
5362
- import * as fs9 from "node:fs";
5363
- import * as path9 from "node:path";
6123
+ import * as fs11 from "node:fs";
6124
+ import * as path11 from "node:path";
5364
6125
  var DEFAULT_SKILLS_DIR = ".agents/skills";
5365
6126
  var MAX_SKILL_FILES = 64;
5366
6127
  var MAX_SKILL_FILE_BYTES = 512 * 1024;
@@ -5383,12 +6144,12 @@ var readFrontmatterDescription = (contents) => {
5383
6144
  var walkFiles = (root) => {
5384
6145
  const found = [];
5385
6146
  const visit = (dir) => {
5386
- for (const entry of fs9.readdirSync(dir, { withFileTypes: true })) {
6147
+ for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
5387
6148
  if (IGNORED_DIRS.has(entry.name)) continue;
5388
- const absolute = path9.join(dir, entry.name);
6149
+ const absolute = path11.join(dir, entry.name);
5389
6150
  let stat;
5390
6151
  try {
5391
- stat = fs9.statSync(absolute);
6152
+ stat = fs11.statSync(absolute);
5392
6153
  } catch {
5393
6154
  continue;
5394
6155
  }
@@ -5402,7 +6163,7 @@ var walkFiles = (root) => {
5402
6163
  var scanSkillLibrary = (root) => {
5403
6164
  let entries;
5404
6165
  try {
5405
- entries = fs9.readdirSync(root, { withFileTypes: true });
6166
+ entries = fs11.readdirSync(root, { withFileTypes: true });
5406
6167
  } catch {
5407
6168
  return { error: `${root} is not a readable directory` };
5408
6169
  }
@@ -5410,23 +6171,23 @@ var scanSkillLibrary = (root) => {
5410
6171
  const notes = [];
5411
6172
  for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
5412
6173
  if (entry.name.startsWith(".")) continue;
5413
- const skillDir = path9.join(root, entry.name);
6174
+ const skillDir = path11.join(root, entry.name);
5414
6175
  let stat;
5415
6176
  try {
5416
- stat = fs9.statSync(skillDir);
6177
+ stat = fs11.statSync(skillDir);
5417
6178
  } catch {
5418
6179
  continue;
5419
6180
  }
5420
6181
  if (!stat.isDirectory()) continue;
5421
- if (!fs9.existsSync(path9.join(skillDir, "SKILL.md"))) {
6182
+ if (!fs11.existsSync(path11.join(skillDir, "SKILL.md"))) {
5422
6183
  notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
5423
6184
  continue;
5424
6185
  }
5425
6186
  const files = [];
5426
6187
  let bytes = 0;
5427
6188
  for (const absolute of walkFiles(skillDir)) {
5428
- const relative4 = path9.relative(skillDir, absolute).split(path9.sep).join("/");
5429
- const buffer = fs9.readFileSync(absolute);
6189
+ const relative4 = path11.relative(skillDir, absolute).split(path11.sep).join("/");
6190
+ const buffer = fs11.readFileSync(absolute);
5430
6191
  if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
5431
6192
  notes.push({ skill: entry.name, message: `${relative4} is over 512KB \u2014 skipped` });
5432
6193
  continue;
@@ -5461,16 +6222,16 @@ var scanSkillLibrary = (root) => {
5461
6222
  };
5462
6223
 
5463
6224
  // src/ssh-setup.ts
5464
- import * as os7 from "node:os";
5465
- import * as path11 from "node:path";
6225
+ import * as os8 from "node:os";
6226
+ import * as path13 from "node:path";
5466
6227
 
5467
6228
  // ../../packages/workspace-ssh/src/index.ts
5468
6229
  import { spawnSync as spawnSync3 } from "node:child_process";
5469
6230
  import { createHash as createHash3 } from "node:crypto";
5470
- import * as fs10 from "node:fs";
6231
+ import * as fs12 from "node:fs";
5471
6232
  import { isIP as isIP2 } from "node:net";
5472
- import * as os6 from "node:os";
5473
- import * as path10 from "node:path";
6233
+ import * as os7 from "node:os";
6234
+ import * as path12 from "node:path";
5474
6235
  var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
5475
6236
  var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
5476
6237
  var WorkspaceSshError = class extends Error {
@@ -5487,16 +6248,16 @@ var WorkspaceSshError = class extends Error {
5487
6248
  };
5488
6249
  var success = (value) => ({ ok: true, value });
5489
6250
  var failure = (error) => ({ ok: false, error });
5490
- var stripIpv6Brackets = (hostname6) => hostname6.startsWith("[") && hostname6.endsWith("]") ? hostname6.slice(1, -1) : hostname6;
6251
+ var stripIpv6Brackets = (hostname7) => hostname7.startsWith("[") && hostname7.endsWith("]") ? hostname7.slice(1, -1) : hostname7;
5491
6252
  var hashIdentity = (identity) => createHash3("sha256").update(identity).digest("hex").slice(0, 24);
5492
- var aliasLabel = (hostname6) => {
5493
- const label = hostname6.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
6253
+ var aliasLabel = (hostname7) => {
6254
+ const label = hostname7.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
5494
6255
  return label === "" ? "server" : label;
5495
6256
  };
5496
6257
  var parseHostOverride = (raw) => {
5497
- const hostname6 = raw.trim();
5498
- if (hostname6 === "") return failure(new WorkspaceSshError("target", "SSH hostname is empty."));
5499
- if (hostname6.includes("://") || /[\s/@]/.test(hostname6)) {
6258
+ const hostname7 = raw.trim();
6259
+ if (hostname7 === "") return failure(new WorkspaceSshError("target", "SSH hostname is empty."));
6260
+ if (hostname7.includes("://") || /[\s/@]/.test(hostname7)) {
5500
6261
  return failure(
5501
6262
  new WorkspaceSshError(
5502
6263
  "target",
@@ -5504,7 +6265,7 @@ var parseHostOverride = (raw) => {
5504
6265
  )
5505
6266
  );
5506
6267
  }
5507
- if (/^[^:]+:\d+$/.test(hostname6)) {
6268
+ if (/^[^:]+:\d+$/.test(hostname7)) {
5508
6269
  return failure(
5509
6270
  new WorkspaceSshError(
5510
6271
  "target",
@@ -5512,11 +6273,11 @@ var parseHostOverride = (raw) => {
5512
6273
  )
5513
6274
  );
5514
6275
  }
5515
- if ((hostname6.startsWith("[") || hostname6.endsWith("]")) && !/^\[[^\]]+\]$/.test(hostname6)) {
6276
+ if ((hostname7.startsWith("[") || hostname7.endsWith("]")) && !/^\[[^\]]+\]$/.test(hostname7)) {
5516
6277
  return failure(new WorkspaceSshError("target", "SSH hostname contains invalid IPv6 brackets."));
5517
6278
  }
5518
- const unbracketed = stripIpv6Brackets(hostname6);
5519
- if ((hostname6.startsWith("[") || unbracketed.includes(":")) && isIP2(unbracketed) !== 6) {
6279
+ const unbracketed = stripIpv6Brackets(hostname7);
6280
+ if ((hostname7.startsWith("[") || unbracketed.includes(":")) && isIP2(unbracketed) !== 6) {
5520
6281
  return failure(
5521
6282
  new WorkspaceSshError("target", "SSH hostname contains an invalid IPv6 address.")
5522
6283
  );
@@ -5557,8 +6318,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
5557
6318
  var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
5558
6319
  var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
5559
6320
  var parseIdentityPath = (value) => {
5560
- const resolved = path10.resolve(
5561
- value.startsWith("~/") ? path10.join(os6.homedir(), value.slice(2)) : value
6321
+ const resolved = path12.resolve(
6322
+ value.startsWith("~/") ? path12.join(os7.homedir(), value.slice(2)) : value
5562
6323
  );
5563
6324
  if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
5564
6325
  return failure(
@@ -5714,7 +6475,7 @@ var agentKeys = () => {
5714
6475
  };
5715
6476
  var readText = (file) => {
5716
6477
  try {
5717
- return success(fs10.readFileSync(file, "utf8"));
6478
+ return success(fs12.readFileSync(file, "utf8"));
5718
6479
  } catch (cause) {
5719
6480
  const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
5720
6481
  return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
@@ -5772,19 +6533,19 @@ var readRequiredKey = (keyPath, source) => {
5772
6533
  var readOptionalKey = (keyPath) => {
5773
6534
  const parsed = parseIdentityPath(keyPath);
5774
6535
  if (!parsed.ok) return parsed;
5775
- if (!fs10.existsSync(parsed.value) && !fs10.existsSync(publicKeyPath(parsed.value)))
6536
+ if (!fs12.existsSync(parsed.value) && !fs12.existsSync(publicKeyPath(parsed.value)))
5776
6537
  return success(null);
5777
6538
  return readRequiredKey(parsed.value, "existing");
5778
6539
  };
5779
6540
  var pinAgentKey = (configHome, publicKey) => {
5780
- const identityFile = path10.resolve(
6541
+ const identityFile = path12.resolve(
5781
6542
  configHome,
5782
6543
  "ssh",
5783
6544
  `agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
5784
6545
  );
5785
6546
  try {
5786
- fs10.mkdirSync(path10.dirname(identityFile), { recursive: true, mode: 448 });
5787
- fs10.writeFileSync(identityFile, `${publicKey}
6547
+ fs12.mkdirSync(path12.dirname(identityFile), { recursive: true, mode: 448 });
6548
+ fs12.writeFileSync(identityFile, `${publicKey}
5788
6549
  `, { mode: 384 });
5789
6550
  return success({ publicKey, identityFile, source: "agent" });
5790
6551
  } catch (cause) {
@@ -5795,15 +6556,15 @@ var pinAgentKey = (configHome, publicKey) => {
5795
6556
  };
5796
6557
  var generateDedicatedKey = (privatePath) => {
5797
6558
  try {
5798
- fs10.mkdirSync(path10.dirname(privatePath), { recursive: true, mode: 448 });
6559
+ fs12.mkdirSync(path12.dirname(privatePath), { recursive: true, mode: 448 });
5799
6560
  } catch (cause) {
5800
6561
  return failure(
5801
- new WorkspaceSshError("key", `Could not create ${path10.dirname(privatePath)}.`, cause)
6562
+ new WorkspaceSshError("key", `Could not create ${path12.dirname(privatePath)}.`, cause)
5802
6563
  );
5803
6564
  }
5804
6565
  const generated = spawnSync3(
5805
6566
  "ssh-keygen",
5806
- ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os6.hostname()}`],
6567
+ ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os7.hostname()}`],
5807
6568
  { encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
5808
6569
  );
5809
6570
  if (generated.status !== 0) {
@@ -5827,7 +6588,7 @@ var generateDedicatedKey = (privatePath) => {
5827
6588
  return fresh;
5828
6589
  };
5829
6590
  var dedicatedKey = (configHome, create) => {
5830
- const privatePath = path10.join(configHome, "ssh", "id_ed25519");
6591
+ const privatePath = path12.join(configHome, "ssh", "id_ed25519");
5831
6592
  const existing = readOptionalKey(privatePath);
5832
6593
  if (existing.ok === false) return failure(existing.error);
5833
6594
  if (existing.value !== null || !create) return existing;
@@ -5875,10 +6636,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
5875
6636
  const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
5876
6637
  if (reconciled.ok === false) return failure(reconciled.error);
5877
6638
  try {
5878
- fs10.mkdirSync(path10.dirname(configFile), { recursive: true, mode: 448 });
5879
- fs10.chmodSync(path10.dirname(configFile), 448);
5880
- fs10.writeFileSync(configFile, reconciled.value, { mode: 384 });
5881
- fs10.chmodSync(configFile, 384);
6639
+ fs12.mkdirSync(path12.dirname(configFile), { recursive: true, mode: 448 });
6640
+ fs12.chmodSync(path12.dirname(configFile), 448);
6641
+ fs12.writeFileSync(configFile, reconciled.value, { mode: 384 });
6642
+ fs12.chmodSync(configFile, 384);
5882
6643
  return success(void 0);
5883
6644
  } catch (cause) {
5884
6645
  return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
@@ -5891,7 +6652,7 @@ var dim5 = ansi("2");
5891
6652
  var green5 = ansi("32");
5892
6653
  var warn = ansi("33");
5893
6654
  var say5 = (line) => console.log(line);
5894
- var sshConfigPath = () => path11.join(os7.homedir(), ".ssh", "config");
6655
+ var sshConfigPath = () => path13.join(os8.homedir(), ".ssh", "config");
5895
6656
  var flagValue = (args, flag) => {
5896
6657
  const index = args.indexOf(flag);
5897
6658
  return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
@@ -5961,7 +6722,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
5961
6722
  if (picked.value === null) return showFailure("No workspace SSH key is available.");
5962
6723
  const registered = await api2("POST", "/workspace-ssh/keys", {
5963
6724
  publicKey: picked.value.publicKey,
5964
- name: os7.hostname()
6725
+ name: os8.hostname()
5965
6726
  });
5966
6727
  const sourceLabel = {
5967
6728
  explicit: "from --key",
@@ -6003,8 +6764,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
6003
6764
  };
6004
6765
 
6005
6766
  // src/uninstall.ts
6006
- import * as fs11 from "node:fs";
6007
- import * as path12 from "node:path";
6767
+ import * as fs13 from "node:fs";
6768
+ import * as path14 from "node:path";
6008
6769
  var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
6009
6770
  var parseUninstallArgs = (args) => {
6010
6771
  let scope = null;
@@ -6026,14 +6787,14 @@ var parseUninstallArgs = (args) => {
6026
6787
  var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
6027
6788
  var countEntries = (directory) => {
6028
6789
  try {
6029
- return fs11.readdirSync(directory).length;
6790
+ return fs13.readdirSync(directory).length;
6030
6791
  } catch {
6031
6792
  return 0;
6032
6793
  }
6033
6794
  };
6034
6795
  var exists = (file) => {
6035
6796
  try {
6036
- fs11.lstatSync(file);
6797
+ fs13.lstatSync(file);
6037
6798
  return true;
6038
6799
  } catch {
6039
6800
  return false;
@@ -6042,7 +6803,7 @@ var exists = (file) => {
6042
6803
  var managedBlockCount = (sshConfigFile) => {
6043
6804
  let config;
6044
6805
  try {
6045
- config = fs11.readFileSync(sshConfigFile, "utf8");
6806
+ config = fs13.readFileSync(sshConfigFile, "utf8");
6046
6807
  } catch {
6047
6808
  return 0;
6048
6809
  }
@@ -6066,27 +6827,27 @@ var describeUninstall = async (runtime, scope) => {
6066
6827
  if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
6067
6828
  server = "none";
6068
6829
  } else if (read.value === null) {
6069
- server = exists(path12.join(configDir, "identity.env")) ? {
6830
+ server = exists(path14.join(configDir, "identity.env")) ? {
6070
6831
  version: "(no active generation)",
6071
6832
  appUrl: "",
6072
6833
  dockerContext: "",
6073
- generations: countEntries(path12.join(configDir, "generations")),
6074
- backups: countEntries(path12.join(configDir, "backups"))
6834
+ generations: countEntries(path14.join(configDir, "generations")),
6835
+ backups: countEntries(path14.join(configDir, "backups"))
6075
6836
  } : "none";
6076
6837
  } else {
6077
6838
  server = {
6078
6839
  version: read.value.config.serverVersion,
6079
6840
  appUrl: read.value.config.appUrl,
6080
6841
  dockerContext: read.value.config.dockerContext,
6081
- generations: countEntries(path12.join(configDir, "generations")),
6082
- backups: countEntries(path12.join(configDir, "backups"))
6842
+ generations: countEntries(path14.join(configDir, "generations")),
6843
+ backups: countEntries(path14.join(configDir, "backups"))
6083
6844
  };
6084
6845
  }
6085
6846
  }
6086
6847
  let home = null;
6087
6848
  if (scope !== "server") {
6088
- const cliConfig = path12.join(runtime.cliHome, "cli.json");
6089
- const sshDirectory = path12.join(runtime.cliHome, "ssh");
6849
+ const cliConfig = path14.join(runtime.cliHome, "cli.json");
6850
+ const sshDirectory = path14.join(runtime.cliHome, "ssh");
6090
6851
  home = {
6091
6852
  cliConfig: exists(cliConfig) ? cliConfig : null,
6092
6853
  sshDirectory: exists(sshDirectory) ? sshDirectory : null,
@@ -6218,7 +6979,7 @@ var removeServer = async (runtime, failures, leftovers) => {
6218
6979
  }
6219
6980
  }
6220
6981
  for (const name of SERVER_FILES) {
6221
- fs11.rmSync(path12.join(configDir, name), { recursive: true, force: true });
6982
+ fs13.rmSync(path14.join(configDir, name), { recursive: true, force: true });
6222
6983
  }
6223
6984
  server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
6224
6985
  },
@@ -6229,20 +6990,20 @@ var removeServer = async (runtime, failures, leftovers) => {
6229
6990
  var removeHome = async (runtime, plan, failures, leftovers) => {
6230
6991
  const { server } = runtime;
6231
6992
  if (plan.cliConfig !== null) {
6232
- fs11.rmSync(plan.cliConfig, { force: true });
6993
+ fs13.rmSync(plan.cliConfig, { force: true });
6233
6994
  server.writeLine(`removed ${plan.cliConfig}`);
6234
6995
  }
6235
6996
  if (plan.sshDirectory !== null) {
6236
- fs11.rmSync(plan.sshDirectory, { recursive: true, force: true });
6997
+ fs13.rmSync(plan.sshDirectory, { recursive: true, force: true });
6237
6998
  server.writeLine(`removed ${plan.sshDirectory}`);
6238
6999
  }
6239
7000
  if (plan.managedSshBlocks > 0) {
6240
7001
  try {
6241
- const before = fs11.readFileSync(runtime.sshConfigFile, "utf8");
7002
+ const before = fs13.readFileSync(runtime.sshConfigFile, "utf8");
6242
7003
  const stripped = stripManagedWorkspaceSshBlocks(before);
6243
7004
  if (!stripped.ok) throw stripped.error;
6244
7005
  if (stripped.value.removed > 0) {
6245
- fs11.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
7006
+ fs13.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
6246
7007
  server.writeLine(
6247
7008
  `removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
6248
7009
  );
@@ -6255,12 +7016,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
6255
7016
  }
6256
7017
  let remaining = [];
6257
7018
  try {
6258
- remaining = fs11.readdirSync(runtime.cliHome);
7019
+ remaining = fs13.readdirSync(runtime.cliHome);
6259
7020
  } catch {
6260
7021
  return;
6261
7022
  }
6262
7023
  if (remaining.length === 0) {
6263
- fs11.rmdirSync(runtime.cliHome);
7024
+ fs13.rmdirSync(runtime.cliHome);
6264
7025
  server.writeLine(`removed ${runtime.cliHome}`);
6265
7026
  } else {
6266
7027
  leftovers.push(
@@ -6324,19 +7085,19 @@ var upgradeUrl = async (input) => {
6324
7085
  // src/main.ts
6325
7086
  var mendCliHome = () => {
6326
7087
  const xdg = process.env["XDG_CONFIG_HOME"];
6327
- const preferred = path13.join(
6328
- xdg === void 0 || xdg === "" ? path13.join(os8.homedir(), ".config") : xdg,
7088
+ const preferred = path15.join(
7089
+ xdg === void 0 || xdg === "" ? path15.join(os9.homedir(), ".config") : xdg,
6329
7090
  "mend"
6330
7091
  );
6331
- const legacy = path13.join(os8.homedir(), ".mend");
6332
- return !fs12.existsSync(preferred) && fs12.existsSync(legacy) ? legacy : preferred;
7092
+ const legacy = path15.join(os9.homedir(), ".mend");
7093
+ return !fs14.existsSync(preferred) && fs14.existsSync(legacy) ? legacy : preferred;
6333
7094
  };
6334
- var CONFIG_PATH = path13.join(mendCliHome(), "cli.json");
7095
+ var CONFIG_PATH = path15.join(mendCliHome(), "cli.json");
6335
7096
  var loadConfig = () => {
6336
7097
  let fileConfig = {};
6337
- if (fs12.existsSync(CONFIG_PATH)) {
7098
+ if (fs14.existsSync(CONFIG_PATH)) {
6338
7099
  try {
6339
- fileConfig = JSON.parse(fs12.readFileSync(CONFIG_PATH, "utf8"));
7100
+ fileConfig = JSON.parse(fs14.readFileSync(CONFIG_PATH, "utf8"));
6340
7101
  } catch {
6341
7102
  fail6(`could not parse ${CONFIG_PATH}`);
6342
7103
  }
@@ -6527,7 +7288,7 @@ var adopt = async (config, args) => {
6527
7288
  }
6528
7289
  const sourceIssue = repositoryCloneUrlIssue(source);
6529
7290
  if (sourceIssue !== null) return fail6(sourceIssue);
6530
- const projectName = name ?? normalizeProjectName(path13.basename(source, ".git"));
7291
+ const projectName = name ?? normalizeProjectName(path15.basename(source, ".git"));
6531
7292
  const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
6532
7293
  if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
6533
7294
  return fail6(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
@@ -6576,7 +7337,7 @@ var findProject = async (config, explicit, adoptCwd = false) => {
6576
7337
  const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
6577
7338
  if (sourceIssue !== null) return fail6(sourceIssue);
6578
7339
  const adopted = await api(config, "POST", "/projects", {
6579
- name: normalizeProjectName(path13.basename(facts.repoRoot)),
7340
+ name: normalizeProjectName(path15.basename(facts.repoRoot)),
6580
7341
  source: facts.originUrl
6581
7342
  });
6582
7343
  say6(`${green6("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim6(adopted.storePath)}`);
@@ -6743,8 +7504,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
6743
7504
  const ws = new WebSocket(url);
6744
7505
  ws.binaryType = "arraybuffer";
6745
7506
  try {
6746
- await new Promise((resolve6, reject) => {
6747
- ws.addEventListener("open", () => resolve6(), { once: true });
7507
+ await new Promise((resolve7, reject) => {
7508
+ ws.addEventListener("open", () => resolve7(), { once: true });
6748
7509
  ws.addEventListener("error", () => reject(new Error("could not connect")), { once: true });
6749
7510
  });
6750
7511
  } catch {
@@ -6752,8 +7513,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
6752
7513
  }
6753
7514
  const stopHerdrHint = hintHerdrAttachment(harness);
6754
7515
  let finishAttachment;
6755
- const finished = new Promise((resolve6) => {
6756
- finishAttachment = resolve6;
7516
+ const finished = new Promise((resolve7) => {
7517
+ finishAttachment = resolve7;
6757
7518
  });
6758
7519
  const onClose = () => finishAttachment?.();
6759
7520
  if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
@@ -7290,11 +8051,11 @@ var autoConnect = async (config, service) => {
7290
8051
  };
7291
8052
  var serviceRun = async (config, args) => {
7292
8053
  const dashdash = args.indexOf("--");
7293
- const usage = usageOf("service run");
8054
+ const usage2 = usageOf("service run");
7294
8055
  if (dashdash === -1) {
7295
8056
  const positionals = args.filter((a) => !a.startsWith("--"));
7296
8057
  const name2 = positionals.at(-1);
7297
- if (name2 === void 0) return fail6(usage);
8058
+ if (name2 === void 0) return fail6(usage2);
7298
8059
  const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
7299
8060
  const session2 = await resolveLiveSession(config, prefix2, "service run");
7300
8061
  const recipes = await api(
@@ -7323,19 +8084,19 @@ var serviceRun = async (config, args) => {
7323
8084
  return;
7324
8085
  }
7325
8086
  const argv = args.slice(dashdash + 1);
7326
- if (argv.length === 0) return fail6(usage);
8087
+ if (argv.length === 0) return fail6(usage2);
7327
8088
  const head = args.slice(0, dashdash);
7328
8089
  const portFlag = head.indexOf("--port");
7329
8090
  const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
7330
- if (!Number.isInteger(port) || port < 1 || port > 65535) return fail6(usage);
8091
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return fail6(usage2);
7331
8092
  const nameFlag = head.indexOf("--name");
7332
8093
  const name = nameFlag !== -1 && head[nameFlag + 1] !== void 0 ? String(head[nameFlag + 1]) : null;
7333
8094
  const protocol = head.includes("--udp") ? "udp" : "tcp";
7334
8095
  const http = head.includes("--http");
7335
8096
  const https = head.includes("--https");
7336
- if (http && https) return fail6(usage);
8097
+ if (http && https) return fail6(usage2);
7337
8098
  const browserScheme = https ? "https" : http ? "http" : null;
7338
- if (protocol === "udp" && browserScheme !== null) return fail6(usage);
8099
+ if (protocol === "udp" && browserScheme !== null) return fail6(usage2);
7339
8100
  const prefix = head.find(
7340
8101
  (a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
7341
8102
  );
@@ -7408,7 +8169,7 @@ var serviceLogs = async (config, args) => {
7408
8169
  cursor = page.nextFrom;
7409
8170
  const ended = page.status === "exited" || page.status === "failed";
7410
8171
  if (ended && !advanced) break;
7411
- if (!advanced) await new Promise((resolve6) => setTimeout(resolve6, 250));
8172
+ if (!advanced) await new Promise((resolve7) => setTimeout(resolve7, 250));
7412
8173
  }
7413
8174
  say6("");
7414
8175
  say6(dim6(`stream ended \xB7 next sequence ${cursor}`));
@@ -7426,13 +8187,13 @@ var serviceRestart = async (config, args) => {
7426
8187
  };
7427
8188
  var serviceInit = async (args) => {
7428
8189
  const root = gitTopLevel(process.cwd()) ?? process.cwd();
7429
- const target = path13.join(root, "mend.toml");
7430
- if (fs12.existsSync(target)) {
8190
+ const target = path15.join(root, "mend.toml");
8191
+ if (fs14.existsSync(target)) {
7431
8192
  return fail6(`${target} already exists \u2014 edit it directly (init never merges)`);
7432
8193
  }
7433
- const rootFiles = fs12.readdirSync(root);
8194
+ const rootFiles = fs14.readdirSync(root);
7434
8195
  const proposals = [];
7435
- const readRoot = (name) => fs12.readFileSync(path13.join(root, name), "utf8");
8196
+ const readRoot = (name) => fs14.readFileSync(path15.join(root, name), "utf8");
7436
8197
  if (rootFiles.includes("package.json")) {
7437
8198
  proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
7438
8199
  }
@@ -7442,13 +8203,13 @@ var serviceInit = async (args) => {
7442
8203
  );
7443
8204
  for (const glob of globs) {
7444
8205
  const parent = glob.replace(/\/?\*+$/, "");
7445
- const dirs = glob.endsWith("*") ? fs12.existsSync(path13.join(root, parent)) ? fs12.readdirSync(path13.join(root, parent)).map((dir) => path13.join(parent, dir)) : [] : [glob];
8206
+ const dirs = glob.endsWith("*") ? fs14.existsSync(path15.join(root, parent)) ? fs14.readdirSync(path15.join(root, parent)).map((dir) => path15.join(parent, dir)) : [] : [glob];
7446
8207
  for (const dir of dirs) {
7447
- const manifest = path13.join(root, dir, "package.json");
7448
- if (!fs12.existsSync(manifest)) continue;
8208
+ const manifest = path15.join(root, dir, "package.json");
8209
+ if (!fs14.existsSync(manifest)) continue;
7449
8210
  for (const proposal of proposeFromWorkspacePackage(
7450
- path13.basename(dir),
7451
- fs12.readFileSync(manifest, "utf8"),
8211
+ path15.basename(dir),
8212
+ fs14.readFileSync(manifest, "utf8"),
7452
8213
  rootFiles
7453
8214
  )) {
7454
8215
  if (!proposals.some((existing) => existing.name === proposal.name)) {
@@ -7492,7 +8253,7 @@ var serviceInit = async (args) => {
7492
8253
  return;
7493
8254
  }
7494
8255
  }
7495
- fs12.writeFileSync(target, toml);
8256
+ fs14.writeFileSync(target, toml);
7496
8257
  say6(`${green6("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
7497
8258
  };
7498
8259
  var tunnelServices = async (config, services, portOverride) => {
@@ -7540,7 +8301,7 @@ var attachTunnels = (config, optOut) => {
7540
8301
  start: async (sessionId) => {
7541
8302
  await Promise.race([
7542
8303
  tunnels.focus(sessionId),
7543
- new Promise((resolve6) => setTimeout(resolve6, 3e3))
8304
+ new Promise((resolve7) => setTimeout(resolve7, 3e3))
7544
8305
  ]);
7545
8306
  },
7546
8307
  attached: (work) => {
@@ -7596,18 +8357,18 @@ var serviceCommand = async (config, args) => {
7596
8357
  }
7597
8358
  };
7598
8359
  var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
7599
- var readIfExists = (file) => fs12.existsSync(file) ? fs12.readFileSync(file, "utf8") : null;
8360
+ var readIfExists = (file) => fs14.existsSync(file) ? fs14.readFileSync(file, "utf8") : null;
7600
8361
  var localCredential = (provider) => {
7601
- const home = os8.homedir();
8362
+ const home = os9.homedir();
7602
8363
  switch (provider) {
7603
8364
  case "codex":
7604
8365
  return readIfExists(
7605
- path13.join(process.env["CODEX_HOME"] ?? path13.join(home, ".codex"), "auth.json")
8366
+ path15.join(process.env["CODEX_HOME"] ?? path15.join(home, ".codex"), "auth.json")
7606
8367
  );
7607
8368
  case "claude":
7608
8369
  return readIfExists(
7609
- path13.join(
7610
- process.env["CLAUDE_CONFIG_DIR"] ?? path13.join(home, ".claude"),
8370
+ path15.join(
8371
+ process.env["CLAUDE_CONFIG_DIR"] ?? path15.join(home, ".claude"),
7611
8372
  ".credentials.json"
7612
8373
  )
7613
8374
  );
@@ -7631,19 +8392,64 @@ var accountLine = (account) => {
7631
8392
  ].filter((fact) => fact !== null);
7632
8393
  return `${account.provider.padEnd(8)} ${facts.join(" \xB7 ")}`;
7633
8394
  };
7634
- var accountsCommand = async (config) => {
7635
- const identity = await api(config, "GET", "/me/sealant");
7636
- process.stdout.write(`platform user ${identity.sealantUserId}
7637
- `);
8395
+ var accountsLines = async (config) => {
8396
+ const identity = await request(config, "GET", "/me/sealant");
7638
8397
  const providers = ["claude", "codex", "github"];
7639
- for (const provider of providers) {
7640
- const account = identity.accounts.find((row) => row.provider === provider && row.name === "default") ?? identity.accounts.find((row) => row.provider === provider);
7641
- process.stdout.write(
7642
- ` ${account === void 0 ? `${provider.padEnd(8)} not connected` : accountLine(account)}
7643
- `
7644
- );
8398
+ return [
8399
+ `platform user ${identity.sealantUserId}`,
8400
+ ...providers.map((provider) => {
8401
+ const account = identity.accounts.find((row) => row.provider === provider && row.name === "default") ?? identity.accounts.find((row) => row.provider === provider);
8402
+ return ` ${account === void 0 ? `${provider.padEnd(8)} not connected` : accountLine(account)}`;
8403
+ })
8404
+ ];
8405
+ };
8406
+ var accountsCommand = async (config) => {
8407
+ let lines;
8408
+ try {
8409
+ lines = await accountsLines(config);
8410
+ } catch (error) {
8411
+ return fail6(error instanceof Error ? error.message : String(error));
7645
8412
  }
8413
+ for (const line of lines) process.stdout.write(`${line}
8414
+ `);
7646
8415
  };
8416
+ var claudeGrantSecret = () => {
8417
+ const read = readGrant(claudeGrantDir(mendCliHome()));
8418
+ return read.kind === "missing" ? null : read.secret;
8419
+ };
8420
+ var doctorBundle = (config, args) => doctorBundleCommand(args, {
8421
+ defaultDir: path15.join(mendCliHome(), "bundles"),
8422
+ now: () => /* @__PURE__ */ new Date(),
8423
+ say: say6,
8424
+ warn: (line) => process.stderr.write(`${line}
8425
+ `),
8426
+ collectors: (tail) => bundleCollectors({
8427
+ cliVersion: cliVersion(),
8428
+ serverUrl: config.url,
8429
+ configuredUrl: config.configuredUrl,
8430
+ deviceId: config.deviceId,
8431
+ tokenSaved: config.token !== null,
8432
+ env: process.env,
8433
+ stdinTty: process.stdin.isTTY === true,
8434
+ stdoutTty: process.stdout.isTTY === true,
8435
+ get: (route) => request(config, "GET", route),
8436
+ doctor: async () => {
8437
+ const checks = await runChecks(config, {
8438
+ localCredential,
8439
+ claudeGrant: claudeGrantSecret,
8440
+ onPath
8441
+ });
8442
+ return `${checks.map((check) => formatCheck(check)).join("\n")}
8443
+ `;
8444
+ },
8445
+ accounts: async () => `${(await accountsLines(config)).join("\n")}
8446
+ `,
8447
+ run: (command, commandArgs, timeoutMs) => runServerProcess(command, commandArgs, process.env, { timeoutMs }),
8448
+ readServer: () => readServerInstallationFacts(nodeServerRuntime().configDir),
8449
+ pathOf,
8450
+ tail
8451
+ })
8452
+ });
7647
8453
  var minuteOrUnknown = (at) => at === null ? "unknown" : at.toISOString().slice(0, 16);
7648
8454
  var staleGrantReason = (secret) => {
7649
8455
  const facts = claudeGrantFacts(secret);
@@ -7676,7 +8482,7 @@ var claudeGrant = async () => {
7676
8482
  return null;
7677
8483
  }
7678
8484
  }
7679
- if (status.configDirectory !== null && path13.resolve(status.configDirectory) !== path13.resolve(dir)) {
8485
+ if (status.configDirectory !== null && path15.resolve(status.configDirectory) !== path15.resolve(dir)) {
7680
8486
  fail6(
7681
8487
  `claude: this Claude read ${status.configDirectory} instead of ${dir}, so a separate grant is not possible \u2014 connect the shared one deliberately with --use-my-login`
7682
8488
  );
@@ -7739,7 +8545,7 @@ var connectCommand = async (config, args) => {
7739
8545
  }
7740
8546
  let secret;
7741
8547
  if (flags.includes("--from-stdin")) {
7742
- secret = fs12.readFileSync(0, "utf8").trim();
8548
+ secret = fs14.readFileSync(0, "utf8").trim();
7743
8549
  if (secret === "") return fail6("nothing on stdin");
7744
8550
  } else if (provider === "claude" && !flags.includes("--use-my-login")) {
7745
8551
  secret = await claudeGrant();
@@ -7785,14 +8591,14 @@ var takeFlagValue4 = (args, flag) => {
7785
8591
  return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
7786
8592
  };
7787
8593
  var saveCliConfig = (next) => {
7788
- fs12.mkdirSync(path13.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
7789
- fs12.writeFileSync(
8594
+ fs14.mkdirSync(path15.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
8595
+ fs14.writeFileSync(
7790
8596
  CONFIG_PATH,
7791
8597
  `${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
7792
8598
  `,
7793
8599
  { mode: 384 }
7794
8600
  );
7795
- fs12.chmodSync(CONFIG_PATH, 384);
8601
+ fs14.chmodSync(CONFIG_PATH, 384);
7796
8602
  };
7797
8603
  var login = async (config, args) => {
7798
8604
  await loginCommand(args, {
@@ -7805,7 +8611,7 @@ var login = async (config, args) => {
7805
8611
  });
7806
8612
  };
7807
8613
  var logout = async (config) => {
7808
- if (!fs12.existsSync(CONFIG_PATH) && config.token === null) {
8614
+ if (!fs14.existsSync(CONFIG_PATH) && config.token === null) {
7809
8615
  say6(dim6("nothing saved \u2014 already signed out"));
7810
8616
  return;
7811
8617
  }
@@ -7858,8 +8664,8 @@ var uninstallCommand = async (config, args) => {
7858
8664
  const runtime = {
7859
8665
  server,
7860
8666
  cliHome: mendCliHome(),
7861
- sshConfigFile: path13.join(os8.homedir(), ".ssh", "config"),
7862
- signedIn: config.token === null || !fs12.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
8667
+ sshConfigFile: path15.join(os9.homedir(), ".ssh", "config"),
8668
+ signedIn: config.token === null || !fs14.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
7863
8669
  revokeDevice: async () => {
7864
8670
  if (config.deviceId === null) return "no device id saved";
7865
8671
  try {
@@ -7923,7 +8729,7 @@ var keysShare = async (config) => {
7923
8729
  return fail6("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
7924
8730
  }
7925
8731
  await shareAgent({
7926
- url: () => socketUrl(config, "keys-bridge", { host: os8.hostname() }),
8732
+ url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
7927
8733
  agentSock,
7928
8734
  signal: new AbortController().signal,
7929
8735
  onEvent: (event) => {
@@ -8053,7 +8859,7 @@ ${usageOf("dotfiles repo")}`);
8053
8859
  var dotfilesSync = async (config, args) => {
8054
8860
  const all = args.includes("--all");
8055
8861
  const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
8056
- const home = os8.homedir();
8862
+ const home = os9.homedir();
8057
8863
  if (!all && requestedPaths.length === 0) {
8058
8864
  const found = scanDotfileCandidates(home);
8059
8865
  if (found.length === 0) {
@@ -8080,13 +8886,13 @@ var dotfilesSync = async (config, args) => {
8080
8886
  if ("error" in read) return fail6(read.error);
8081
8887
  const result = await api(config, "POST", "/dotfiles/snapshot", {
8082
8888
  files: read.files,
8083
- source: os8.hostname(),
8889
+ source: os9.hostname(),
8084
8890
  merge: false
8085
8891
  });
8086
8892
  const snapshot = result.snapshot;
8087
8893
  if (snapshot === null) return fail6("the server accepted the sync but reports no snapshot");
8088
8894
  say6(
8089
- `${green6("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os8.hostname()} \xB7 ${dim6(snapshot.sha.slice(0, 7))} ${dim6("\u2014 applies from the next session launch")}`
8895
+ `${green6("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os9.hostname()} \xB7 ${dim6(snapshot.sha.slice(0, 7))} ${dim6("\u2014 applies from the next session launch")}`
8090
8896
  );
8091
8897
  };
8092
8898
  var envLoad = async (config, args) => {
@@ -8100,10 +8906,10 @@ var envLoad = async (config, args) => {
8100
8906
  const positional = args.filter(
8101
8907
  (arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
8102
8908
  );
8103
- const file = path13.resolve(positional[0] ?? ".env");
8909
+ const file = path15.resolve(positional[0] ?? ".env");
8104
8910
  let contents;
8105
8911
  try {
8106
- contents = fs12.readFileSync(file, "utf8");
8912
+ contents = fs14.readFileSync(file, "utf8");
8107
8913
  } catch {
8108
8914
  return fail6(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
8109
8915
  }
@@ -8119,10 +8925,10 @@ var envLoad = async (config, args) => {
8119
8925
  { contents, allSecret, secretNames }
8120
8926
  );
8121
8927
  if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
8122
- say6(dim6(`${path13.basename(file)} has no variables`));
8928
+ say6(dim6(`${path15.basename(file)} has no variables`));
8123
8929
  return;
8124
8930
  }
8125
- say6(`${green6("\u2713")} loaded ${path13.basename(file)} into ${project2.name}`);
8931
+ say6(`${green6("\u2713")} loaded ${path15.basename(file)} into ${project2.name}`);
8126
8932
  for (const line of formatLoadReport(report, { dim: dim6, warn: amber2 })) say6(line);
8127
8933
  const plaintextUrls = report.loaded.filter(
8128
8934
  (entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
@@ -8196,7 +9002,7 @@ var envCluster = async (config, args) => {
8196
9002
  const explicitProject = takeFlagValue4(args, "--project");
8197
9003
  const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
8198
9004
  const [verb, ...rest] = positional;
8199
- const usage = usageOf("env cluster");
9005
+ const usage2 = usageOf("env cluster");
8200
9006
  const project2 = await findProject(config, explicitProject);
8201
9007
  const route = `/projects/${project2.id}/cluster-bindings`;
8202
9008
  const appliesLine = () => say6(
@@ -8208,7 +9014,7 @@ var envCluster = async (config, args) => {
8208
9014
  const [kind, objectName] = rest;
8209
9015
  if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
8210
9016
  return fail6(`env cluster add takes a kind and an object name
8211
- ${usage}`);
9017
+ ${usage2}`);
8212
9018
  }
8213
9019
  const result = await api(config, "POST", route, {
8214
9020
  kind,
@@ -8223,7 +9029,7 @@ ${usage}`);
8223
9029
  const [kind, objectName] = ref?.split("/", 2) ?? [];
8224
9030
  if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
8225
9031
  return fail6(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
8226
- ${usage}`);
9032
+ ${usage2}`);
8227
9033
  }
8228
9034
  const snapshot = await api(config, "GET", route);
8229
9035
  const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
@@ -8241,7 +9047,7 @@ ${usage}`);
8241
9047
  const [name] = rest;
8242
9048
  if (!clear && name === void 0)
8243
9049
  return fail6(`env cluster sa takes a name or --clear
8244
- ${usage}`);
9050
+ ${usage2}`);
8245
9051
  const result = await api(
8246
9052
  config,
8247
9053
  "PUT",
@@ -8261,7 +9067,7 @@ ${usage}`);
8261
9067
  return appliesLine();
8262
9068
  }
8263
9069
  return fail6(`unknown env cluster command "${verb ?? ""}"
8264
- ${usage}`);
9070
+ ${usage2}`);
8265
9071
  };
8266
9072
  var envCommand = async (config, args) => {
8267
9073
  const [verb, ...rest] = args;
@@ -8313,7 +9119,7 @@ var skillsList = async (config, args) => {
8313
9119
  };
8314
9120
  var skillsPush = async (config, args) => {
8315
9121
  const dirFlag = takeFlagValue4(args, "--dir");
8316
- const root = dirFlag === null ? path13.join(os8.homedir(), DEFAULT_SKILLS_DIR) : path13.resolve(dirFlag);
9122
+ const root = dirFlag === null ? path15.join(os9.homedir(), DEFAULT_SKILLS_DIR) : path15.resolve(dirFlag);
8317
9123
  const scanned = scanSkillLibrary(root);
8318
9124
  if ("error" in scanned) return fail6(scanned.error);
8319
9125
  for (const note of scanned.notes) say6(dim6(` ${note.skill}: ${note.message}`));
@@ -8765,7 +9571,7 @@ var sessionsCommand = async (config, args) => {
8765
9571
  say6(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
8766
9572
  return;
8767
9573
  }
8768
- const json = args.includes("--json");
9574
+ const json2 = args.includes("--json");
8769
9575
  const projectFlag = args.indexOf("--project");
8770
9576
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
8771
9577
  const projects = await api(config, "GET", "/projects");
@@ -8774,7 +9580,7 @@ var sessionsCommand = async (config, args) => {
8774
9580
  return fail6(`no adopted project named "${projectName}"`);
8775
9581
  }
8776
9582
  let rows;
8777
- if (all || projectName !== null || !json) {
9583
+ if (all || projectName !== null || !json2) {
8778
9584
  const details = await Promise.all(
8779
9585
  scope.map(
8780
9586
  (p) => api(config, "GET", `/projects/${p.id}${all ? "?deadEnds=include" : ""}`)
@@ -8800,7 +9606,7 @@ var sessionsCommand = async (config, args) => {
8800
9606
  }));
8801
9607
  }
8802
9608
  if (rows.length === 0) {
8803
- if (json) {
9609
+ if (json2) {
8804
9610
  say6(JSON.stringify({ version: 1, sessions: [] }, null, 2));
8805
9611
  return;
8806
9612
  }
@@ -8815,7 +9621,7 @@ var sessionsCommand = async (config, args) => {
8815
9621
  if (aLive !== bLive) return bLive - aLive;
8816
9622
  return b.session.createdAt.localeCompare(a.session.createdAt);
8817
9623
  });
8818
- if (json) {
9624
+ if (json2) {
8819
9625
  const payload = {
8820
9626
  version: 1,
8821
9627
  sessions: rows.map(({ session, projectName: rowProjectName, annotation }) => ({
@@ -8914,11 +9720,11 @@ var buildWorktreesJson = async (config, projectName) => {
8914
9720
  return { version: 2, worktrees };
8915
9721
  };
8916
9722
  var worktreesCommand = async (config, args) => {
8917
- const json = args.includes("--json");
9723
+ const json2 = args.includes("--json");
8918
9724
  const projectFlag = args.indexOf("--project");
8919
9725
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
8920
9726
  const payload = await buildWorktreesJson(config, projectName);
8921
- if (json) {
9727
+ if (json2) {
8922
9728
  say6(JSON.stringify(payload, null, 2));
8923
9729
  return;
8924
9730
  }
@@ -9009,7 +9815,7 @@ var startShareIfBridge = async (config) => {
9009
9815
  }
9010
9816
  if (access.mode !== "bridge") return null;
9011
9817
  return startAgentShare({
9012
- url: () => socketUrl(config, "keys-bridge", { host: os8.hostname() }),
9818
+ url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
9013
9819
  agentSock
9014
9820
  });
9015
9821
  };
@@ -9049,10 +9855,10 @@ var manCommand = (words) => {
9049
9855
  }
9050
9856
  const version = cliVersion();
9051
9857
  const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
9052
- const file = path13.join(os8.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
9053
- fs12.writeFileSync(file, page);
9858
+ const file = path15.join(os9.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
9859
+ fs14.writeFileSync(file, page);
9054
9860
  const result = spawnSync4("man", ["-l", file], { stdio: "inherit" });
9055
- fs12.rmSync(file, { force: true });
9861
+ fs14.rmSync(file, { force: true });
9056
9862
  if (result.error !== void 0 || result.status !== 0) {
9057
9863
  say6(doc === null ? renderIndex() : renderCommand(doc));
9058
9864
  }
@@ -9125,10 +9931,8 @@ var main = async () => {
9125
9931
  case "qr":
9126
9932
  return qrCommand(rest);
9127
9933
  case "doctor":
9128
- return doctorCommand(config, localCredential, () => {
9129
- const read = readGrant(claudeGrantDir(mendCliHome()));
9130
- return read.kind === "missing" ? null : read.secret;
9131
- });
9934
+ if (rest.includes("--bundle")) return doctorBundle(config, rest);
9935
+ return doctorCommand(config, localCredential, claudeGrantSecret);
9132
9936
  case "env":
9133
9937
  return envCommand(config, rest);
9134
9938
  case "ssh":