@sealant/mend 0.30.0 → 0.32.0

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 (79) hide show
  1. package/dist/{chunk-BMFB2UN7.js → chunk-J3TR4UAG.js} +81 -7
  2. package/dist/{dashboard-OF5UXII2.js → dashboard-3HNFSQZD.js} +60 -7
  3. package/dist/main.js +1821 -871
  4. package/man/mend-accounts.1 +1 -1
  5. package/man/mend-adopt.1 +1 -1
  6. package/man/mend-attach.1 +1 -1
  7. package/man/mend-codex.1 +1 -1
  8. package/man/mend-completions.1 +1 -1
  9. package/man/mend-connect.1 +1 -1
  10. package/man/mend-continue.1 +1 -1
  11. package/man/mend-doctor.1 +28 -3
  12. package/man/mend-dotfiles-repo.1 +1 -1
  13. package/man/mend-dotfiles-sync.1 +1 -1
  14. package/man/mend-dotfiles.1 +1 -1
  15. package/man/mend-env-cluster.1 +1 -1
  16. package/man/mend-env-load.1 +1 -1
  17. package/man/mend-env-show.1 +1 -1
  18. package/man/mend-folder-create.1 +1 -1
  19. package/man/mend-folder-list.1 +1 -1
  20. package/man/mend-folder-push.1 +1 -1
  21. package/man/mend-folder-rm.1 +1 -1
  22. package/man/mend-git-author.1 +24 -0
  23. package/man/mend-help.1 +1 -1
  24. package/man/mend-invite.1 +1 -1
  25. package/man/mend-keys-init.1 +1 -1
  26. package/man/mend-keys-mode.1 +1 -1
  27. package/man/mend-keys-share.1 +1 -1
  28. package/man/mend-keys-show.1 +1 -1
  29. package/man/mend-land.1 +9 -2
  30. package/man/mend-login.1 +1 -1
  31. package/man/mend-logout.1 +1 -1
  32. package/man/mend-man.1 +1 -1
  33. package/man/mend-members.1 +1 -1
  34. package/man/mend-operator-exposure.1 +1 -1
  35. package/man/mend-operator-gate.1 +1 -1
  36. package/man/mend-operator-grant-owner.1 +1 -1
  37. package/man/mend-operator-org-create.1 +1 -1
  38. package/man/mend-operator-org-invite-owner.1 +1 -1
  39. package/man/mend-operator-org-list.1 +1 -1
  40. package/man/mend-operator-org-rename.1 +1 -1
  41. package/man/mend-operator-reset-link.1 +1 -1
  42. package/man/mend-pair.1 +1 -1
  43. package/man/mend-projects.1 +1 -1
  44. package/man/mend-pull.1 +1 -1
  45. package/man/mend-refresh.1 +1 -1
  46. package/man/mend-rejoin.1 +1 -1
  47. package/man/mend-resume.1 +1 -1
  48. package/man/mend-run.1 +1 -1
  49. package/man/mend-server-logs.1 +1 -1
  50. package/man/mend-server-restart.1 +1 -1
  51. package/man/mend-server-setup.1 +1 -1
  52. package/man/mend-server-start.1 +1 -1
  53. package/man/mend-server-status.1 +1 -1
  54. package/man/mend-server-stop.1 +1 -1
  55. package/man/mend-server-upgrade.1 +1 -1
  56. package/man/mend-server.1 +1 -1
  57. package/man/mend-service-add.1 +1 -1
  58. package/man/mend-service-connect.1 +1 -1
  59. package/man/mend-service-init.1 +1 -1
  60. package/man/mend-service-list.1 +1 -1
  61. package/man/mend-service-logs.1 +1 -1
  62. package/man/mend-service-restart.1 +1 -1
  63. package/man/mend-service-run.1 +1 -1
  64. package/man/mend-service-stop.1 +1 -1
  65. package/man/mend-session-share.1 +1 -1
  66. package/man/mend-sessions.1 +1 -1
  67. package/man/mend-shell.1 +1 -1
  68. package/man/mend-skills-push.1 +1 -1
  69. package/man/mend-skills.1 +1 -1
  70. package/man/mend-snake.1 +1 -1
  71. package/man/mend-ssh-setup.1 +1 -1
  72. package/man/mend-ssh.1 +1 -1
  73. package/man/mend-stop.1 +8 -1
  74. package/man/mend-ui.1 +2 -2
  75. package/man/mend-uninstall.1 +1 -1
  76. package/man/mend-version.1 +1 -1
  77. package/man/mend-worktrees.1 +1 -1
  78. package/man/mend.1 +6 -3
  79. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  claudeGrantFacts,
8
8
  createSseParser,
9
9
  cwdFacts,
10
+ gitAuthorIssue,
10
11
  gitCurrentBranch,
11
12
  gitOriginUrl,
12
13
  gitTopLevel,
@@ -16,23 +17,25 @@ import {
16
17
  landingFactsFromWire,
17
18
  matchProjectByCwd,
18
19
  narrowCredential,
20
+ normalizeGitAuthor,
19
21
  normalizeProjectName,
20
22
  normalizeRemoteUrl,
21
23
  parseLaunchArgs,
22
24
  pasteBytes,
23
25
  repositoryCloneUrlIssue,
24
26
  sameGrant,
27
+ servicesHoldOf,
25
28
  sessionDisplayName,
26
29
  trackBracketedPaste
27
- } from "./chunk-BMFB2UN7.js";
30
+ } from "./chunk-J3TR4UAG.js";
28
31
 
29
32
  // src/main.ts
30
33
  import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
31
- import * as fs12 from "node:fs";
34
+ import * as fs14 from "node:fs";
32
35
  import { createRequire } from "node:module";
33
36
  import * as net4 from "node:net";
34
- import * as os8 from "node:os";
35
- import * as path13 from "node:path";
37
+ import * as os9 from "node:os";
38
+ import * as path15 from "node:path";
36
39
 
37
40
  // src/agent-share.ts
38
41
  import * as net from "node:net";
@@ -40,7 +43,7 @@ import { setTimeout as delay } from "node:timers/promises";
40
43
  var retryDelayMs = (attempt2) => Math.min(3e4, 1e3 * 2 ** Math.min(Math.max(attempt2 - 1, 0), 5));
41
44
  var SSH_AGENTC_REQUEST_IDENTITIES = 11;
42
45
  var SSH_AGENTC_SIGN_REQUEST = 13;
43
- var askLocalAgent = (agentSock, payload) => new Promise((resolve6, reject) => {
46
+ var askLocalAgent = (agentSock, payload) => new Promise((resolve7, reject) => {
44
47
  const connection = net.connect(agentSock, () => {
45
48
  connection.write(payload);
46
49
  });
@@ -54,7 +57,7 @@ var askLocalAgent = (agentSock, payload) => new Promise((resolve6, reject) => {
54
57
  if (pending.length >= 4 && pending.length >= 4 + pending.readUInt32BE(0)) {
55
58
  clearTimeout(timer);
56
59
  connection.end();
57
- resolve6(pending.subarray(0, 4 + pending.readUInt32BE(0)));
60
+ resolve7(pending.subarray(0, 4 + pending.readUInt32BE(0)));
58
61
  }
59
62
  });
60
63
  connection.on("error", (error) => {
@@ -84,12 +87,12 @@ var shareAgent = async (options) => {
84
87
  const ws = new WebSocket(target);
85
88
  const onAbort = () => ws.close();
86
89
  signal.addEventListener("abort", onAbort, { once: true });
87
- const closed = new Promise((resolve6) => {
88
- ws.addEventListener("close", () => resolve6(), { once: true });
90
+ const closed = new Promise((resolve7) => {
91
+ ws.addEventListener("close", () => resolve7(), { once: true });
89
92
  });
90
- const opened = await new Promise((resolve6) => {
91
- ws.addEventListener("open", () => resolve6(true), { once: true });
92
- 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 });
93
96
  });
94
97
  if (opened) {
95
98
  attempt2 = 0;
@@ -107,19 +110,19 @@ var shareAgent = async (options) => {
107
110
  }
108
111
  const id = frame.id;
109
112
  const payload = Buffer.from(frame.payload, "base64");
110
- const type = payload[4];
113
+ const type2 = payload[4];
111
114
  const context = typeof frame.context === "string" ? frame.context : "mend";
112
115
  chain = chain.then(async () => {
113
116
  const started = Date.now();
114
- if (type === SSH_AGENTC_SIGN_REQUEST) onEvent({ kind: "sign-requested", context });
115
- 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) {
116
119
  onEvent({ kind: "identities-requested", context });
117
120
  }
118
121
  try {
119
122
  const response = await askLocalAgent(agentSock, payload);
120
123
  if (ws.readyState !== WebSocket.OPEN) return null;
121
124
  ws.send(JSON.stringify({ t: "res", id, payload: response.toString("base64") }));
122
- if (type === SSH_AGENTC_SIGN_REQUEST) {
125
+ if (type2 === SSH_AGENTC_SIGN_REQUEST) {
123
126
  onEvent({ kind: "signed", context, seconds: (Date.now() - started) / 1e3 });
124
127
  }
125
128
  } catch (error) {
@@ -127,7 +130,7 @@ var shareAgent = async (options) => {
127
130
  if (ws.readyState === WebSocket.OPEN) {
128
131
  ws.send(JSON.stringify({ t: "err", id, message }));
129
132
  }
130
- 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 });
131
134
  }
132
135
  return null;
133
136
  });
@@ -240,8 +243,8 @@ var readGrant = (configDir, platform = process.platform) => {
240
243
  import { execFile } from "node:child_process";
241
244
  var IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
242
245
  var pickImageType = (offered) => {
243
- const types = offered.split(/\s+/).map((type) => type.trim().toLowerCase());
244
- 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;
245
248
  return null;
246
249
  };
247
250
  var parsePngfHex = (output) => {
@@ -250,13 +253,13 @@ var parsePngfHex = (output) => {
250
253
  return Buffer.from(match[1], "hex");
251
254
  };
252
255
  var MAX_BYTES = 32 * 1024 * 1024;
253
- var run = (command, args) => new Promise((resolve6) => {
256
+ var run = (command, args) => new Promise((resolve7) => {
254
257
  execFile(
255
258
  command,
256
259
  [...args],
257
260
  { encoding: "buffer", maxBuffer: MAX_BYTES, timeout: 3e3 },
258
261
  (error, stdout) => {
259
- resolve6(error === null ? stdout : null);
262
+ resolve7(error === null ? stdout : null);
260
263
  }
261
264
  );
262
265
  });
@@ -293,450 +296,736 @@ var readClipboardImage = async (platform = process.platform, env = process.env)
293
296
  return null;
294
297
  };
295
298
 
296
- // 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";
297
306
  import * as fs2 from "node:fs";
298
307
  import * as path2 from "node:path";
299
- var TIMEOUT_MS = 3e3;
300
- var MARKS = { ok: "\u2713", todo: "\u25CB", failed: "\u2717" };
301
- var LABEL_WIDTH = 11;
302
- var day = (at) => at.toISOString().slice(0, 10);
303
- 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}`}`;
304
- var getJson = async (config, route) => {
305
- try {
306
- const response = await fetch(`${config.url}/api${route}`, {
307
- headers: config.token === null ? {} : { authorization: `Bearer ${config.token}` },
308
- 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"
309
324
  });
310
- if (!response.ok) return { value: null, status: response.status };
311
- return { value: await response.json(), status: response.status };
312
- } catch {
313
- return { value: null, status: null };
314
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
+ });
315
434
  };
316
- var exposureCheck = (exposure) => {
317
- const beyondMachine = exposure.declared !== "loopback" && exposure.originOnMachine !== true;
318
- const gateOpen = beyondMachine ? exposure.gateOpen : 0;
319
- const facts = [
320
- `declared ${exposure.declared}`,
321
- `${exposure.originScheme} origin`,
322
- ...exposure.arrivedVia === "trusted-proxy" ? ["arrived via a trusted proxy"] : [],
323
- ...gateOpen === 0 ? [] : [`${gateOpen} gate items open`]
324
- ].join(" \xB7 ");
325
- const plainBeyondMachine = beyondMachine && exposure.originScheme === "http";
326
- return {
327
- label: "exposure",
328
- state: plainBeyondMachine ? "todo" : "ok",
329
- detail: facts,
330
- 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
+ }
331
487
  };
488
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
489
+ return results;
332
490
  };
333
- var LOGIN_COMMANDS = {
334
- claude: "claude setup-token",
335
- codex: "codex login",
336
- github: "gh auth login"
337
- };
338
- var HARNESS_CLIS = [
339
- { command: "claude", provider: "claude" },
340
- { command: "codex", provider: "codex" },
341
- { command: "gh", provider: "github" }
342
- ];
343
- var notChecked = (label) => ({
344
- label,
345
- state: "todo",
346
- detail: "not checked",
347
- fix: null
348
- });
349
- var identityOf = (account) => {
350
- const meta = account.metadata;
351
- for (const key of ["login", "email", "accountEmail", "accountId"]) {
352
- const value = meta[key];
353
- 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
+ }
354
500
  }
355
501
  return null;
356
502
  };
357
- var runChecks = async (config, probes) => {
358
- const checks = [];
359
- const health = await getJson(config, "/health");
360
- checks.push(
361
- health.value === null ? {
362
- label: "server",
363
- state: "failed",
364
- detail: `cannot reach ${config.url}`,
365
- fix: "start the Mend server"
366
- } : {
367
- label: "server",
368
- state: "ok",
369
- detail: `${config.url} \xB7 mend ${health.value.version}`,
370
- 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
+ })
371
528
  }
372
- );
373
- const projects = health.value !== null && config.token !== null ? await getJson(config, "/projects") : null;
374
- if (health.value === null) checks.push(notChecked("signed in"));
375
- else if (config.token === null) {
376
- checks.push({
377
- label: "signed in",
378
- state: "failed",
379
- detail: "no token saved",
380
- fix: "mend login"
381
- });
382
- } else if (projects !== null && projects.value !== null) {
383
- checks.push({ label: "signed in", state: "ok", detail: "token accepted", fix: null });
384
- } else {
385
- const status = projects === null ? null : projects.status;
386
- checks.push(
387
- status === 401 || status === 403 ? { label: "signed in", state: "failed", detail: "token rejected", fix: "mend login" } : {
388
- label: "signed in",
389
- state: "failed",
390
- detail: `GET /projects \u2192 ${status ?? "no answer"}`,
391
- fix: null
392
- }
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
393
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;
394
606
  }
395
- const signedIn = projects !== null && projects.value !== null;
396
- const connection = signedIn ? await getJson(config, "/sealant/connection") : null;
397
- if (connection === null) checks.push(notChecked("sealant"));
398
- else if (connection.value === null) {
399
- checks.push(
400
- connection.status === 404 ? notChecked("sealant") : {
401
- label: "sealant",
402
- state: "failed",
403
- detail: `GET /sealant/connection \u2192 ${connection.status ?? "no answer"}`,
404
- fix: null
405
- }
406
- );
407
- } else if (connection.value.status === "connected") {
408
- checks.push({
409
- label: "sealant",
410
- state: "ok",
411
- detail: `connected \xB7 ${connection.value.baseUrl}`,
412
- fix: null
413
- });
414
- } else {
415
- checks.push({
416
- label: "sealant",
417
- state: "failed",
418
- detail: `${connection.value.status} \xB7 ${connection.value.detail ?? connection.value.baseUrl}`,
419
- fix: null
420
- });
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 [];
421
620
  }
422
- const identity = signedIn ? await getJson(config, "/me/sealant") : null;
423
- const platform = identity === null ? null : identity.value;
424
- for (const { provider } of HARNESS_CLIS) {
425
- if (platform === null) {
426
- checks.push(notChecked(provider));
427
- 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") ?? ""
428
631
  }
429
- const account = platform.accounts.find((row) => row.provider === provider && row.name === "default") ?? platform.accounts.find((row) => row.provider === provider);
430
- if (account === void 0) {
431
- checks.push({
432
- label: provider,
433
- state: "todo",
434
- detail: "not connected",
435
- fix: `mend connect ${provider}`
436
- });
437
- 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);
438
662
  }
439
- const who = identityOf(account);
440
- checks.push(
441
- account.status === "active" ? {
442
- label: provider,
443
- state: "ok",
444
- detail: who === null ? "connected" : `connected \xB7 ${who}`,
445
- fix: null
446
- } : {
447
- label: provider,
448
- state: "todo",
449
- detail: account.status,
450
- 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());
451
747
  }
452
- );
453
- }
454
- const grant = probes.claudeGrant();
455
- if (grant !== null) {
456
- const facts = claudeGrantFacts(grant);
457
- if (facts === null) {
458
- checks.push({
459
- label: "grant",
460
- state: "failed",
461
- detail: "unreadable",
462
- fix: "mend connect claude"
463
- });
464
- } else if (!facts.hasRefreshToken) {
465
- checks.push({
466
- label: "grant",
467
- state: "failed",
468
- detail: "signed out",
469
- fix: "mend connect claude"
470
- });
471
- } else if (facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now()) {
472
- checks.push({
473
- label: "grant",
474
- state: "todo",
475
- detail: `expired ${day(facts.refreshExpiresAt)}`,
476
- fix: "mend connect claude"
477
- });
478
- } else {
479
- checks.push({
480
- label: "grant",
481
- state: "ok",
482
- detail: facts.refreshExpiresAt === null ? "Mend's own" : `Mend's own \xB7 expires ${day(facts.refreshExpiresAt)}`,
483
- fix: null
484
- });
485
748
  }
749
+ return [{ path: "docker.txt", content: `${sections.join("\n")}
750
+ ` }];
486
751
  }
487
- const adopted = projects === null ? null : projects.value;
488
- if (adopted === null) checks.push(notChecked("projects"));
489
- else if (adopted.length === 0) {
490
- checks.push({ label: "projects", state: "todo", detail: "none adopted", fix: "mend adopt" });
491
- } else {
492
- checks.push({ label: "projects", state: "ok", detail: `${adopted.length} adopted`, fix: null });
493
- }
494
- for (const { command, provider } of HARNESS_CLIS) {
495
- const label = `${command} cli`;
496
- if (!probes.onPath(command)) {
497
- checks.push({ label, state: "todo", detail: "not on PATH", fix: null });
498
- continue;
499
- }
500
- const credential = probes.localCredential(provider);
501
- checks.push(
502
- credential === null ? {
503
- label,
504
- state: "todo",
505
- detail: "on PATH \xB7 no credential here",
506
- fix: LOGIN_COMMANDS[provider]
507
- } : { label, state: "ok", detail: "on PATH \xB7 credential present", fix: null }
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"
508
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
+ ];
768
+ }
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;
509
783
  }
510
- const machine = signedIn ? await getJson(config, "/machine") : null;
511
- const exposure = machine === null || machine.value === null ? void 0 : machine.value.exposure;
512
- checks.push(exposure === void 0 ? notChecked("exposure") : exposureCheck(exposure));
513
- return checks;
514
- };
515
- var onPath = (command) => {
516
- for (const directory of (process.env["PATH"] ?? "").split(path2.delimiter)) {
517
- if (directory === "") continue;
518
- try {
519
- fs2.accessSync(path2.join(directory, command), fs2.constants.X_OK);
520
- return true;
521
- } 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 };
522
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
+ };
523
822
  }
524
- return false;
525
823
  };
526
- var paintMark = (state, mark) => {
527
- if (process.stdout.isTTY !== true) return mark;
528
- const code = state === "ok" ? "32" : state === "todo" ? "33" : "31";
529
- 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
+ `;
530
850
  };
531
- var doctorCommand = async (config, localCredential2, claudeGrant2) => {
532
- const checks = await runChecks(config, { localCredential: localCredential2, claudeGrant: claudeGrant2, onPath });
533
- for (const check of checks) process.stdout.write(`${formatCheck(check, paintMark)}
534
- `);
535
- 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
+ ];
536
983
  };
537
984
 
538
- // src/dotfiles.ts
539
- import * as fs3 from "node:fs";
540
- import * as path3 from "node:path";
541
- var DOTFILE_CANDIDATES = [
542
- {
543
- group: "shell",
544
- paths: [
545
- ".zshrc",
546
- ".zshenv",
547
- ".zprofile",
548
- ".bashrc",
549
- ".bash_profile",
550
- ".profile",
551
- ".aliases",
552
- ".config/fish/config.fish",
553
- ".config/starship.toml"
554
- ]
555
- },
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 ──────────────────────────────────────────────────────────────
556
1011
  {
557
- group: "git",
558
- paths: [".gitconfig", ".gitignore_global", ".config/git/config", ".config/git/ignore"]
559
- },
560
- {
561
- group: "editors",
562
- paths: [".vimrc", ".ideavimrc", ".editorconfig", ".config/helix/config.toml"]
563
- },
564
- {
565
- group: "terminal",
566
- paths: [".tmux.conf", ".config/tmux/tmux.conf", ".inputrc", ".dir_colors"]
567
- },
568
- {
569
- group: "tools",
570
- paths: [
571
- ".ripgreprc",
572
- ".config/bat/config",
573
- ".config/lazygit/config.yml",
574
- ".config/mise/config.toml",
575
- ".config/atuin/config.toml",
576
- ".config/zellij/config.kdl"
577
- ]
578
- }
579
- ];
580
- var MAX_FILE_BYTES = 1024 * 1024;
581
- var statFile = (home, relative4) => {
582
- try {
583
- const stat = fs3.statSync(path3.join(home, relative4));
584
- return stat.isFile() ? stat : null;
585
- } catch {
586
- return null;
587
- }
588
- };
589
- var scanDotfileCandidates = (home) => DOTFILE_CANDIDATES.flatMap(
590
- ({ group, paths }) => paths.flatMap((relative4) => {
591
- const stat = statFile(home, relative4);
592
- return stat === null ? [] : [{ path: relative4, group, bytes: stat.size }];
593
- })
594
- );
595
- var homeRelativePath = (home, requested) => {
596
- const relative4 = path3.relative(home, path3.resolve(home, requested));
597
- const outside = relative4 === "" || relative4 === ".." || relative4.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative4);
598
- return outside ? null : relative4.split(path3.sep).join("/");
599
- };
600
- var readSyncFiles = (home, paths_) => {
601
- const files = [];
602
- for (const requested of paths_) {
603
- const relative4 = homeRelativePath(home, requested);
604
- if (relative4 === null) {
605
- return {
606
- error: `${requested} is not under ${home} \u2014 only files in your home directory sync`
607
- };
608
- }
609
- const stat = statFile(home, relative4);
610
- if (stat === null) {
611
- return { error: `${relative4} is not a file under ${home}` };
612
- }
613
- if (stat.size > MAX_FILE_BYTES) {
614
- return { error: `${relative4} is over 1MB \u2014 dotfiles are text; trim the selection` };
615
- }
616
- files.push({
617
- path: relative4,
618
- contentsBase64: fs3.readFileSync(path3.join(home, relative4)).toString("base64"),
619
- // eslint-disable-next-line no-bitwise -- permission bits are the point
620
- mode: (stat.mode & 511).toString(8)
621
- });
622
- }
623
- return { files };
624
- };
625
- var DOTFILES_MANAGERS = ["auto", "copy", "stow", "chezmoi"];
626
- var VALUE_FLAGS = ["--ref", "--subdirectory", "--manager"];
627
- var parseDotfilesRepoArgs = (args) => {
628
- if (args.includes("--clear")) {
629
- return args.length === 1 ? { kind: "clear" } : { kind: "error", error: "--clear takes no URL or other options" };
630
- }
631
- let url = null;
632
- const values = /* @__PURE__ */ new Map();
633
- let bootstrap = true;
634
- for (let index = 0; index < args.length; index += 1) {
635
- const arg = args[index] ?? "";
636
- if (arg === "--no-bootstrap") {
637
- bootstrap = false;
638
- continue;
639
- }
640
- const flag = VALUE_FLAGS.find((candidate) => candidate === arg);
641
- if (flag !== void 0) {
642
- const value = args[index + 1];
643
- if (value === void 0 || value.trim() === "" || value.startsWith("--")) {
644
- return { kind: "error", error: `${flag} needs a value` };
645
- }
646
- values.set(flag, value.trim());
647
- index += 1;
648
- continue;
649
- }
650
- if (arg.startsWith("-")) return { kind: "error", error: `unknown flag ${arg}` };
651
- if (url !== null) return { kind: "error", error: "one repository URL only" };
652
- url = arg.trim();
653
- }
654
- if (url === null || url === "") {
655
- return { kind: "error", error: "name the repository URL, or --clear to remove it" };
656
- }
657
- const requested = values.get("--manager") ?? "auto";
658
- const manager = DOTFILES_MANAGERS.find((candidate) => candidate === requested);
659
- if (manager === void 0) {
660
- return { kind: "error", error: `--manager must be one of ${DOTFILES_MANAGERS.join(", ")}` };
661
- }
662
- return {
663
- kind: "set",
664
- repository: {
665
- url,
666
- ref: values.get("--ref") ?? null,
667
- subdirectory: values.get("--subdirectory") ?? null,
668
- manager,
669
- bootstrap
670
- }
671
- };
672
- };
673
- var dotfilesRepositoryFacts = (repository) => [
674
- repository.ref ?? "default branch",
675
- ...repository.subdirectory === null ? [] : [`${repository.subdirectory}/`],
676
- `manager ${repository.manager}`,
677
- repository.bootstrap ? "install.sh on" : "install.sh off"
678
- ].join(" \xB7 ");
679
-
680
- // src/env.ts
681
- var formatLoadReport = (report, paint6) => {
682
- const width = Math.max(
683
- 0,
684
- ...report.loaded.map((entry) => entry.name.length),
685
- ...report.rejected.map((entry) => entry.name.length)
686
- );
687
- const lines = [];
688
- for (const entry of report.loaded) {
689
- lines.push(
690
- ` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint6.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
691
- );
692
- }
693
- for (const entry of report.rejected) {
694
- lines.push(
695
- ` ${entry.name.padEnd(width)} ${paint6.warn("rejected")} ${paint6.dim(`\xB7 ${entry.reason}`)}`
696
- );
697
- }
698
- return lines;
699
- };
700
-
701
- // src/help.ts
702
- var SECTIONS = [
703
- "start",
704
- "sessions",
705
- "services",
706
- "project setup",
707
- "organization",
708
- "this machine"
709
- ];
710
- var project = (what = "the project") => ({
711
- flag: "--project <p>",
712
- text: `${what} by name, when the current directory is not inside it`
713
- });
714
- var sessionArg = "With no id, the one live session is taken; with several, a picker opens. A prefix of the id is enough.";
715
- 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.";
716
- var noTunnel = {
717
- flag: "--no-tunnel",
718
- text: "do not tunnel the session's Services to this machine"
719
- };
720
- var COMMANDS = [
721
- // ── start ──────────────────────────────────────────────────────────────
722
- {
723
- name: "login",
724
- section: "start",
725
- summary: "sign this terminal in through the browser",
726
- synopsis: ["[--url <server>]"],
727
- description: [
728
- "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."
729
- ],
730
- options: [
731
- {
732
- flag: "--url <server>",
733
- text: "the Mend server. Default: MEND_URL, then http://localhost:3105"
734
- }
735
- ],
736
- examples: [
737
- { command: "mend login --url http://10.0.0.216:3105", text: "sign in to a LAN server" }
738
- ],
739
- 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"]
740
1029
  },
741
1030
  {
742
1031
  name: "connect",
@@ -847,12 +1136,29 @@ var COMMANDS = [
847
1136
  {
848
1137
  name: "doctor",
849
1138
  section: "start",
850
- summary: "check this machine's setup",
851
- synopsis: [],
1139
+ summary: "check this machine's setup, or bundle it for a bug report",
1140
+ synopsis: ["", "--bundle [--out <path>] [--tail <n>]"],
852
1141
  description: [
853
- "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" }
854
1160
  ],
855
- see: ["login", "connect", "keys init"]
1161
+ see: ["login", "connect", "keys init", "server logs"]
856
1162
  },
857
1163
  // ── sessions ───────────────────────────────────────────────────────────
858
1164
  {
@@ -862,7 +1168,7 @@ var COMMANDS = [
862
1168
  synopsis: ["[--no-tunnel]"],
863
1169
  description: [
864
1170
  "A full-screen view of every project and session, updating live. The session pane takes three quarters of the screen: a read-only detail for the selected session with the conversation record it has written so far. The remaining quarter is a sidebar of three stacked sections, projects then worktrees then sessions, where the section you are in stands open and the other two fold to the line that says what is selected. Bare mend with no command opens the same thing.",
865
- "Moving the selection only ever navigates: arrows or j/k move inside the open section, enter and the arrows move between the sidebar and the session pane, and nothing takes this terminal until you ask. Reading the record leaves the sidebar as it was. The verbs are a to attach a live session, r to resume a settled one, n for another session in the selected worktree, w for a new worktree, e to rename, v to review the change, o to open it in the browser, Shift+K to stop, Shift+D to remove, and Shift+R to refresh.",
1171
+ "Moving the selection only ever navigates: arrows or j/k move inside the open section, enter and the arrows move between the sidebar and the session pane, and nothing takes this terminal until you ask. Reading the record leaves the sidebar as it was. The verbs are a to attach a live session, r to resume a settled one, n for another session in the selected worktree, w for a new worktree, e to rename, v to review the change, o to open it in the browser, Shift+K to stop (on a row whose agent has stopped while its Services keep the workspace up, Shift+K stops those Services), Shift+D to remove, and Shift+R to refresh.",
866
1172
  "Nothing is ever squeezed. A terminal too narrow for both gives the whole width to the side you are on, a terminal too short for three drawn panes shows the open section alone, and a one-line breadcrumb states whatever did not fit.",
867
1173
  "On a server that is not this machine, the selected session's live Services declared --http or --https are tunneled to this machine's loopback while it stays selected, and the session pane shows where each one opens, for example web \u2192 http://localhost:5173.",
868
1174
  "The dashboard needs Node 26 or newer for its terminal. Every other command works on Node 22."
@@ -899,12 +1205,17 @@ var COMMANDS = [
899
1205
  name: "stop",
900
1206
  section: "sessions",
901
1207
  summary: "stop the agent; the record and the review remain",
902
- synopsis: ["[session-id-prefix]", "--all [--project <p>]"],
1208
+ synopsis: ["[session-id-prefix]", "--all [--project <p>]", "--services [session-id-prefix]"],
903
1209
  description: [
904
1210
  "Ends the agent process. The workspace harvests the harness state and closes. The worktree, the record, and the review stay, and mend resume brings the conversation back.",
1211
+ "Services keep running after a stop, and a running Service keeps the workspace up. The stop then prints, for example, agent stopped \xB7 3 services keep the workspace up. --services stops all of them. Once nothing is live, the workspace closes.",
905
1212
  sessionArg
906
1213
  ],
907
- options: [{ flag: "--all", text: "every live session" }, project("limit --all to one project")],
1214
+ options: [
1215
+ { flag: "--all", text: "every live session" },
1216
+ project("limit --all to one project"),
1217
+ { flag: "--services", text: "stop the session's Services instead of its agent" }
1218
+ ],
908
1219
  see: ["resume", "attach"]
909
1220
  },
910
1221
  {
@@ -1017,18 +1328,26 @@ var COMMANDS = [
1017
1328
  name: "land",
1018
1329
  section: "sessions",
1019
1330
  summary: "push a session's change to origin and open its pull request",
1020
- synopsis: ["<session> [--branch <name>] [--no-pr] [--title <text>] [--project <p>]"],
1331
+ synopsis: [
1332
+ "<session> [--branch <name>] [--no-pr] [--title <text>] [--project <p>]",
1333
+ "<session> --check [--project <p>]"
1334
+ ],
1021
1335
  description: [
1022
1336
  "Only the change's owner lands it: the owner of the worktree's first session, not someone who started a session in it later. Mend takes a checkpoint, commits what the agent left uncommitted on top of the agent's own commits and the last landing, and pushes that to origin with the project's git access. The agent's commits are pushed as they are. The session's branch, and the worktree's files, index and HEAD, are not touched.",
1023
1337
  "The push only fast-forwards. When origin's branch has commits Mend has not seen, or origin refuses the push, nothing is pushed and the command prints the remote's own words. Mend never force-pushes.",
1024
1338
  "When origin is on GitHub, Mend then opens a pull request into the session's base branch, or updates the one it opened before. That call runs in a workspace with your GitHub account (mend connect github). The description holds Mend's review summary when there is one, the changed files and links back to the session, and an update replaces only Mend's section of it. For any other origin the push happens and the pull request is reported as unavailable, with the reason.",
1025
1339
  "Landing again after more work adds commits to the same branch and updates the same pull request. When nothing is new since the last landing, nothing is pushed and the command says so. The branch on origin is never the project's default branch or the pull request's base. The command then prints what Mend observed: the push, the pull request and its state, what changed since, and pushes the agent made itself. It exits 1 when the push was refused or a step failed.",
1340
+ "A pull request someone opened outside Mend, the agent through gh included, is found and recorded as the change's own: Mend looks after the agent pushes a branch and when its turn ends, and --check looks now. It asks GitHub, as you, about the worktree's branch, every branch the agent pushed, and the agent's head commit. A landing then pushes that pull request's branch and updates it. A pull request from a fork is shown and never updated: Mend pushes to origin only, and opens no second pull request beside it.",
1026
1341
  "<session> is a prefix of the session id or the worktree's name. Settled sessions count."
1027
1342
  ],
1028
1343
  options: [
1029
1344
  {
1030
1345
  flag: "--branch <name>",
1031
- text: "the branch on origin. Default: where the change last pushed to, else mend/<worktree>"
1346
+ text: "the branch on origin. Default: where the change last pushed to, the branch the agent pushed, an adopted pull request's branch, else mend/<worktree>"
1347
+ },
1348
+ {
1349
+ flag: "--check",
1350
+ text: "only look on GitHub for a pull request opened outside Mend, and record it; push nothing"
1032
1351
  },
1033
1352
  { flag: "--no-pr", text: "push only; open or update no pull request" },
1034
1353
  {
@@ -1293,6 +1612,22 @@ var COMMANDS = [
1293
1612
  options: [{ flag: "--all", text: "every known config file" }],
1294
1613
  see: ["dotfiles", "dotfiles repo"]
1295
1614
  },
1615
+ {
1616
+ name: "git-author",
1617
+ section: "project setup",
1618
+ summary: "the name and email your workspaces commit as",
1619
+ synopsis: ["[<name> <email>]", "--clear"],
1620
+ description: [
1621
+ "Commits the agent makes in your workspaces name this author. Until you set one, it is the name and email you registered with. Without arguments, prints it and where it comes from.",
1622
+ "Each workspace receives it as system git config before the agent starts, so a user section in your dotfiles' .gitconfig or in a repository's own config still decides. Sessions launched after a change commit as the new author."
1623
+ ],
1624
+ options: [{ flag: "--clear", text: "go back to your account's name and email" }],
1625
+ examples: [
1626
+ { command: 'mend git-author "Anna Example" anna@example.com', text: "" },
1627
+ { command: "mend git-author --clear", text: "" }
1628
+ ],
1629
+ see: ["dotfiles", "keys mode"]
1630
+ },
1296
1631
  {
1297
1632
  name: "keys init",
1298
1633
  section: "project setup",
@@ -1992,13 +2327,608 @@ var renderManIndex = (version) => {
1992
2327
  return `${out.join("\n")}
1993
2328
  `;
1994
2329
  };
1995
- var manFileName = (doc) => doc === null ? "mend.1" : `mend-${doc.name.replace(/ /g, "-")}.1`;
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;
2910
+ };
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"}`;
1996
2926
 
1997
2927
  // src/landing.ts
1998
2928
  import { spawnSync as spawnSync2 } from "node:child_process";
1999
- import * as fs4 from "node:fs";
2000
- import * as os2 from "node:os";
2001
- 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";
2002
2932
  var paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
2003
2933
  var dim = paint("2");
2004
2934
  var green = paint("32");
@@ -2038,17 +2968,22 @@ var parseFlags = (args, valued, switches) => {
2038
2968
  return { positional, values, on };
2039
2969
  };
2040
2970
  var parseLandArgs = (args) => {
2041
- const flags = parseFlags(args, ["--branch", "--title", "--project"], ["--no-pr"]);
2971
+ const flags = parseFlags(args, ["--branch", "--title", "--project"], ["--no-pr", "--check"]);
2042
2972
  if ("error" in flags) return flags;
2043
2973
  const [session, extra] = flags.positional;
2044
2974
  if (session === void 0) return { error: "name the session to land" };
2045
2975
  if (extra !== void 0) return { error: `one session only; "${extra}" is extra` };
2976
+ const check = flags.on.has("--check");
2977
+ if (check && (flags.values.has("--branch") || flags.values.has("--title") || flags.on.has("--no-pr"))) {
2978
+ return { error: "--check pushes nothing; it takes no --branch, --title or --no-pr" };
2979
+ }
2046
2980
  const branch = flags.values.get("--branch")?.trim() ?? null;
2047
2981
  if (branch === "") return { error: "--branch needs a name" };
2048
2982
  const title = flags.values.get("--title")?.trim() ?? null;
2049
2983
  return {
2050
2984
  args: {
2051
2985
  session,
2986
+ check,
2052
2987
  branch,
2053
2988
  pullRequest: !flags.on.has("--no-pr"),
2054
2989
  title: title === "" ? null : title,
@@ -2157,10 +3092,40 @@ var readFacts = (wire) => {
2157
3092
  return fail("the server's landing facts are in a shape this CLI cannot read \xB7 update mend");
2158
3093
  }
2159
3094
  };
3095
+ var checkLine = (check) => {
3096
+ const pullRequest = check.landing?.pullRequest ?? null;
3097
+ switch (check.outcome) {
3098
+ case "adopted":
3099
+ return pullRequest === null ? "pull request recorded \xB7 opened outside Mend" : `pull request #${pullRequest.number} recorded \xB7 opened outside Mend \xB7 ${pullRequest.url}`;
3100
+ case "observed":
3101
+ return pullRequest === null ? "pull request already recorded" : `pull request #${pullRequest.number} already recorded \xB7 ${pullRequest.state} \xB7 observed`;
3102
+ case "none":
3103
+ return "no pull request on GitHub for the change's branches or the agent's commit";
3104
+ case "skipped":
3105
+ return `GitHub not checked \xB7 ${check.reason ?? "no reason given"}`;
3106
+ }
3107
+ };
2160
3108
  var landCommand = async (api2, args) => {
2161
3109
  const parsed = parseLandArgs(args);
2162
3110
  if ("error" in parsed) return fail(parsed.error);
2163
3111
  const { session, project: project2 } = await findSession(api2, parsed.args.session, parsed.args.project);
3112
+ if (parsed.args.check) {
3113
+ const before = await api2("GET", `/sessions/${session.id}/landings`);
3114
+ if (before.changeId === null) return fail("the session's worktree holds no change yet");
3115
+ const check = await api2(
3116
+ "POST",
3117
+ `/changes/${before.changeId}/pull-request/check`
3118
+ );
3119
+ const mark = check.outcome === "adopted" || check.outcome === "observed" ? green("\u2713") : dim("\xB7");
3120
+ say(`${mark} ${checkLine(check)}`);
3121
+ const after = await api2("GET", `/sessions/${session.id}/landings`);
3122
+ const facts = readFacts(after.facts);
3123
+ if (facts.length > 0) {
3124
+ say(dim(" observed"));
3125
+ for (const fact of facts) say(` ${landingFactLine(fact, /* @__PURE__ */ new Date())}`);
3126
+ }
3127
+ return;
3128
+ }
2164
3129
  const steps = parsed.args.pullRequest ? "checkpoint \xB7 commit \xB7 push \xB7 pull request" : "checkpoint \xB7 commit \xB7 push";
2165
3130
  say(
2166
3131
  dim(
@@ -2201,9 +3166,9 @@ var gitRemotes = (cwd) => {
2201
3166
  var remoteForOrigin = (remotes, originUrl, cwd) => {
2202
3167
  if (originUrl === null) return null;
2203
3168
  const origin = normalizeRemoteUrl(originUrl);
2204
- const originPath = origin === null ? path4.resolve(originUrl.replace(/^file:\/\//, "")) : null;
3169
+ const originPath = origin === null ? path7.resolve(originUrl.replace(/^file:\/\//, "")) : null;
2205
3170
  return remotes.find(
2206
- (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
2207
3172
  ) ?? null;
2208
3173
  };
2209
3174
  var LOG_LINES = 10;
@@ -2226,10 +3191,10 @@ var fetchBundle = (cwd, bundle) => {
2226
3191
  const before = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
2227
3192
  const previous = before.status === 0 ? before.stdout.trim() : null;
2228
3193
  if (previous !== bundle.tip) {
2229
- const dir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "mend-pull-"));
3194
+ const dir = fs7.mkdtempSync(path7.join(os3.tmpdir(), "mend-pull-"));
2230
3195
  try {
2231
- const file = path4.join(dir, "change.bundle");
2232
- fs4.writeFileSync(file, bundle.bytes, { mode: 384 });
3196
+ const file = path7.join(dir, "change.bundle");
3197
+ fs7.writeFileSync(file, bundle.bytes, { mode: 384 });
2233
3198
  const fetched = git(cwd, [
2234
3199
  "fetch",
2235
3200
  "--no-tags",
@@ -2239,7 +3204,7 @@ var fetchBundle = (cwd, bundle) => {
2239
3204
  ]);
2240
3205
  if (fetched.status !== 0) return refusedWith(gitWords(fetched));
2241
3206
  } finally {
2242
- fs4.rmSync(dir, { recursive: true, force: true });
3207
+ fs7.rmSync(dir, { recursive: true, force: true });
2243
3208
  }
2244
3209
  }
2245
3210
  const after = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).stdout.trim();
@@ -2277,7 +3242,7 @@ var fetchedLines = (fetched) => {
2277
3242
  lines.push(`${dim(" switch to it")} git switch ${fetched.branch}`);
2278
3243
  return lines;
2279
3244
  };
2280
- var formatBytes = (bytes) => {
3245
+ var formatBytes2 = (bytes) => {
2281
3246
  if (bytes < 1024) return `${bytes} bytes`;
2282
3247
  const units = ["KiB", "MiB", "GiB"];
2283
3248
  let value = bytes / 1024;
@@ -2290,13 +3255,13 @@ var formatBytes = (bytes) => {
2290
3255
  };
2291
3256
  var numberField = (value, key) => {
2292
3257
  if (typeof value !== "object" || value === null || !(key in value)) return null;
2293
- const field = Reflect.get(value, key);
2294
- return typeof field === "number" ? field : null;
3258
+ const field2 = Reflect.get(value, key);
3259
+ return typeof field2 === "number" ? field2 : null;
2295
3260
  };
2296
- var stringField = (value, key) => {
3261
+ var stringField2 = (value, key) => {
2297
3262
  if (typeof value !== "object" || value === null || !(key in value)) return null;
2298
- const field = Reflect.get(value, key);
2299
- return typeof field === "string" ? field : null;
3263
+ const field2 = Reflect.get(value, key);
3264
+ return typeof field2 === "string" ? field2 : null;
2300
3265
  };
2301
3266
  var parseJson = (bytes) => {
2302
3267
  try {
@@ -2310,9 +3275,9 @@ var bundleRefusal = (downloaded) => {
2310
3275
  const size = numberField(body, "size");
2311
3276
  const limit = numberField(body, "limit");
2312
3277
  if (downloaded.status === 413 && size !== null && limit !== null) {
2313
- 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`;
2314
3279
  }
2315
- return stringField(body, "message") ?? `the bundle request answered ${downloaded.status}`;
3280
+ return stringField2(body, "message") ?? `the bundle request answered ${downloaded.status}`;
2316
3281
  };
2317
3282
  var bundleFactsOf = (downloaded) => {
2318
3283
  const branch = downloaded.header(BUNDLE_HEADERS.branch);
@@ -2357,8 +3322,8 @@ var pullCommand = async (api2, download2, args, cwd = process.cwd()) => {
2357
3322
  };
2358
3323
 
2359
3324
  // src/login.ts
2360
- import { spawn } from "node:child_process";
2361
- import * as os3 from "node:os";
3325
+ import { spawn as spawn2 } from "node:child_process";
3326
+ import * as os4 from "node:os";
2362
3327
 
2363
3328
  // src/pair.ts
2364
3329
  var paint2 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
@@ -2421,23 +3386,23 @@ var qrCommand = async (args) => {
2421
3386
  };
2422
3387
 
2423
3388
  // src/login.ts
2424
- var parseCliAuthStart = (json) => {
2425
- if (typeof json !== "object" || json === null) return null;
2426
- 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)) {
2427
3392
  return null;
2428
3393
  }
2429
- const { deviceCode, code, verifyPath, expiresAt, intervalSeconds } = json;
3394
+ const { deviceCode, code, verifyPath, expiresAt, intervalSeconds } = json2;
2430
3395
  if (typeof deviceCode !== "string" || typeof code !== "string" || typeof verifyPath !== "string" || typeof expiresAt !== "string" || typeof intervalSeconds !== "number") {
2431
3396
  return null;
2432
3397
  }
2433
3398
  return { deviceCode, code, verifyPath, expiresAt, intervalSeconds };
2434
3399
  };
2435
- var parseCliAuthPoll = (json) => {
2436
- if (typeof json !== "object" || json === null || !("status" in json)) return null;
2437
- if (json.status === "pending") return { status: "pending" };
2438
- if (json.status !== "approved") return null;
2439
- if (!("token" in json && "user" in json && "device" in json)) return null;
2440
- 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;
2441
3406
  if (typeof token !== "string") return null;
2442
3407
  if (typeof user !== "object" || user === null) return null;
2443
3408
  if (!("id" in user && "name" in user && "email" in user)) return null;
@@ -2497,7 +3462,7 @@ var openBrowser = (url) => {
2497
3462
  const opener = browserCommand(process.platform);
2498
3463
  if (opener === null || process.stdout.isTTY !== true) return;
2499
3464
  try {
2500
- const child = spawn(opener.command, [...opener.args, url], {
3465
+ const child = spawn2(opener.command, [...opener.args, url], {
2501
3466
  stdio: "ignore",
2502
3467
  detached: true
2503
3468
  });
@@ -2518,21 +3483,21 @@ var post = async (url, body) => {
2518
3483
  return fail3(`cannot reach the Mend server at ${new URL(url).origin} \u2014 is it running?`);
2519
3484
  }
2520
3485
  const text = await response.text();
2521
- let json = null;
3486
+ let json2 = null;
2522
3487
  try {
2523
- json = JSON.parse(text);
3488
+ json2 = JSON.parse(text);
2524
3489
  } catch {
2525
3490
  }
2526
- return { status: response.status, json };
3491
+ return { status: response.status, json: json2 };
2527
3492
  };
2528
- var retryAfterSecondsOf = (json) => {
2529
- if (typeof json === "object" && json !== null && "retryAfterSeconds" in json) {
2530
- 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);
2531
3496
  if (Number.isFinite(seconds) && seconds > 0) return seconds;
2532
3497
  }
2533
3498
  return 5;
2534
3499
  };
2535
- var sleep2 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
3500
+ var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
2536
3501
  var resolveServerUrl = async (args, deps) => {
2537
3502
  const given = takeFlagValue2(args, "--url") ?? deps.configuredUrl;
2538
3503
  if (given !== null) {
@@ -2550,7 +3515,7 @@ var resolveServerUrl = async (args, deps) => {
2550
3515
  };
2551
3516
  var loginCommand = async (args, deps) => {
2552
3517
  const base = await resolveServerUrl(args, deps);
2553
- const started = await post(`${base}/api/cli/auth`, { name: os3.hostname() });
3518
+ const started = await post(`${base}/api/cli/auth`, { name: os4.hostname() });
2554
3519
  if (started.status === 429) {
2555
3520
  return fail3(
2556
3521
  `the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
@@ -2601,8 +3566,8 @@ var loginCommand = async (args, deps) => {
2601
3566
  };
2602
3567
 
2603
3568
  // src/organization.ts
2604
- import * as fs5 from "node:fs";
2605
- import * as path5 from "node:path";
3569
+ import * as fs8 from "node:fs";
3570
+ import * as path8 from "node:path";
2606
3571
  var paint4 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
2607
3572
  var dim4 = paint4("2");
2608
3573
  var green4 = paint4("32");
@@ -2666,7 +3631,7 @@ var scanFolder = (dir) => {
2666
3631
  const files = [];
2667
3632
  const skipped = [];
2668
3633
  const walk = (relative4) => {
2669
- 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));
2670
3635
  for (const entry of entries) {
2671
3636
  const child = relative4 === "" ? entry.name : `${relative4}/${entry.name}`;
2672
3637
  if (entry.isSymbolicLink()) {
@@ -2675,7 +3640,7 @@ var scanFolder = (dir) => {
2675
3640
  if (SKIPPED_DIRECTORIES.has(entry.name)) skipped.push({ path: child, reason: "skipped" });
2676
3641
  else walk(child);
2677
3642
  } else if (entry.isFile()) {
2678
- const size = fs5.statSync(path5.join(dir, child)).size;
3643
+ const size = fs8.statSync(path8.join(dir, child)).size;
2679
3644
  if (size > FOLDER_FILE_BYTES) skipped.push({ path: child, reason: "over 1 MiB" });
2680
3645
  else files.push({ path: child, size });
2681
3646
  }
@@ -2728,7 +3693,7 @@ var folderCommand = async (api2, args) => {
2728
3693
  }
2729
3694
  case "push": {
2730
3695
  if (dir === void 0) return fail4("usage: mend folder push <name> <dir> [--replace]");
2731
- if (!fs5.existsSync(dir) || !fs5.statSync(dir).isDirectory()) {
3696
+ if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) {
2732
3697
  return fail4(`${dir} is not a directory`);
2733
3698
  }
2734
3699
  const folder = await folderNamed(api2, name);
@@ -2743,7 +3708,7 @@ var folderCommand = async (api2, args) => {
2743
3708
  files: batch.map((file) => ({
2744
3709
  path: file.path,
2745
3710
  // Read one batch at a time, so a large tree never sits in memory at once.
2746
- contentsBase64: fs5.readFileSync(path5.join(dir, file.path)).toString("base64")
3711
+ contentsBase64: fs8.readFileSync(path8.join(dir, file.path)).toString("base64")
2747
3712
  })),
2748
3713
  // --replace empties the folder with the first batch; the rest add to it.
2749
3714
  merge: !(replace && index === 0)
@@ -2927,8 +3892,8 @@ var operatorCommand = async (api2, tryApi, baseUrl, args) => {
2927
3892
  import { createHmac, randomBytes } from "node:crypto";
2928
3893
  import { readFile } from "node:fs/promises";
2929
3894
  import * as net2 from "node:net";
2930
- import * as os5 from "node:os";
2931
- import * as path8 from "node:path";
3895
+ import * as os6 from "node:os";
3896
+ import * as path10 from "node:path";
2932
3897
 
2933
3898
  // src/server-docker-volumes.ts
2934
3899
  import { createHash as createHash2 } from "node:crypto";
@@ -2971,14 +3936,14 @@ var docker = async (runtime, context, args) => {
2971
3936
  }
2972
3937
  };
2973
3938
  var listNames = async (runtime, context, kind, project2) => {
2974
- const field = kind === "container" ? "Names" : "Name";
3939
+ const field2 = kind === "container" ? "Names" : "Name";
2975
3940
  const output = await docker(runtime, context, [
2976
3941
  kind,
2977
3942
  "ls",
2978
3943
  ...kind === "container" ? ["--all"] : [],
2979
3944
  ...project2 === void 0 ? [] : ["--filter", `label=com.docker.compose.project=${project2}`],
2980
3945
  "--format",
2981
- `{{json .${field}}}`
3946
+ `{{json .${field2}}}`
2982
3947
  ]);
2983
3948
  if (output._tag === "error") return output;
2984
3949
  const names = [];
@@ -3144,157 +4109,11 @@ var ownership = async (runtime, input, mode) => {
3144
4109
  var claimServerDockerVolumes = (runtime, input) => ownership(runtime, input, "claim");
3145
4110
  var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "verify");
3146
4111
 
3147
- // src/server-runtime.ts
3148
- import { spawn as spawn2 } from "node:child_process";
3149
- import * as fs6 from "node:fs";
3150
- import * as path6 from "node:path";
3151
- var serverProcessDeadlines = {
3152
- ordinary: 3e4,
3153
- pull: 10 * 6e4,
3154
- startup: 3 * 6e4,
3155
- stop: 9e4,
3156
- dump: 15 * 6e4,
3157
- composeWaitSeconds: 120
3158
- };
3159
- var runServerProcess = (command, args, parentEnvironment, options = {}) => {
3160
- const timeoutMs = options?.timeoutMs ?? serverProcessDeadlines.ordinary;
3161
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
3162
- return Promise.resolve({
3163
- status: null,
3164
- stdout: "",
3165
- stderr: "",
3166
- error: "Invalid process timeout"
3167
- });
3168
- }
3169
- if (options.stdoutFile !== void 0 && options.stdout === "inherit") {
3170
- return Promise.resolve({
3171
- status: null,
3172
- stdout: "",
3173
- stderr: "",
3174
- error: "Invalid process output options"
3175
- });
3176
- }
3177
- const env = {};
3178
- for (const key of [
3179
- "PATH",
3180
- "HOME",
3181
- "USER",
3182
- "LOGNAME",
3183
- "XDG_CONFIG_HOME",
3184
- "XDG_RUNTIME_DIR",
3185
- "DOCKER_CONFIG",
3186
- "SSH_AUTH_SOCK",
3187
- "TMPDIR",
3188
- "TMP",
3189
- "TEMP"
3190
- ]) {
3191
- if (parentEnvironment[key] !== void 0) env[key] = parentEnvironment[key];
3192
- }
3193
- return new Promise((resolve6) => {
3194
- let fd;
3195
- try {
3196
- if (options.stdoutFile !== void 0) fd = fs6.openSync(options.stdoutFile, "wx", 384);
3197
- const child = spawn2(command, [...args], {
3198
- env,
3199
- detached: process.platform !== "win32",
3200
- stdio: ["ignore", fd ?? (options.stdout === "inherit" ? "inherit" : "pipe"), "pipe"]
3201
- });
3202
- let stdout = "";
3203
- let stderr = "";
3204
- let stdoutBytes = 0;
3205
- let stderrBytes = 0;
3206
- let failure2;
3207
- const terminate = () => {
3208
- if (process.platform === "win32" || child.pid === void 0) {
3209
- child.kill("SIGKILL");
3210
- return;
3211
- }
3212
- try {
3213
- process.kill(-child.pid, "SIGKILL");
3214
- } catch (cause) {
3215
- if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) {
3216
- failure2 = "Could not terminate the process group; retain the installation lock until its processes stop.";
3217
- child.kill("SIGKILL");
3218
- }
3219
- }
3220
- };
3221
- child.stdout?.on("data", (chunk) => {
3222
- stdoutBytes += chunk.length;
3223
- if (stdoutBytes <= 4 * 1024 * 1024) stdout += chunk.toString();
3224
- else {
3225
- failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
3226
- terminate();
3227
- }
3228
- });
3229
- child.stderr?.on("data", (chunk) => {
3230
- stderrBytes += chunk.length;
3231
- if (stderrBytes <= 64 * 1024) stderr += chunk.toString();
3232
- else {
3233
- failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
3234
- terminate();
3235
- }
3236
- });
3237
- const timer = setTimeout(() => {
3238
- failure2 ??= `Process timed out after ${timeoutMs}ms`;
3239
- terminate();
3240
- }, timeoutMs);
3241
- child.once("error", (error) => {
3242
- failure2 ??= error.message;
3243
- });
3244
- child.once("close", (status) => {
3245
- globalThis.clearTimeout(timer);
3246
- try {
3247
- if (fd !== void 0) fs6.fsyncSync(fd);
3248
- } catch {
3249
- failure2 ??= "Could not durably write process output.";
3250
- } finally {
3251
- try {
3252
- if (fd !== void 0) fs6.closeSync(fd);
3253
- } catch {
3254
- failure2 ??= "Could not close process output file.";
3255
- }
3256
- }
3257
- resolve6({
3258
- status: failure2 === void 0 ? status : null,
3259
- stdout,
3260
- stderr,
3261
- ...failure2 === void 0 ? {} : { error: failure2 }
3262
- });
3263
- });
3264
- } catch (cause) {
3265
- try {
3266
- if (fd !== void 0) fs6.closeSync(fd);
3267
- } catch {
3268
- }
3269
- resolve6({
3270
- status: null,
3271
- stdout: "",
3272
- stderr: "",
3273
- error: cause instanceof Error ? cause.message : "Could not start process."
3274
- });
3275
- }
3276
- });
3277
- };
3278
- var serverComposeArgs = (installation, command) => [
3279
- "--context",
3280
- installation.dockerContext,
3281
- "compose",
3282
- "--project-name",
3283
- "mend",
3284
- "--project-directory",
3285
- installation.directory,
3286
- "--env-file",
3287
- path6.join(installation.directory, "server.env"),
3288
- "-f",
3289
- path6.join(installation.directory, "compose.yaml"),
3290
- ...command
3291
- ];
3292
-
3293
4112
  // src/server-store.ts
3294
4113
  import { randomUUID } from "node:crypto";
3295
- import * as fs7 from "node:fs";
3296
- import * as os4 from "node:os";
3297
- 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";
3298
4117
  var ServerStoreError = class extends Error {
3299
4118
  _tag = "ServerStoreError";
3300
4119
  };
@@ -3315,28 +4134,28 @@ var attempt = (operation) => {
3315
4134
  var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
3316
4135
  var readOptional = (file) => {
3317
4136
  try {
3318
- return fs7.readFileSync(file, "utf8");
4137
+ return fs9.readFileSync(file, "utf8");
3319
4138
  } catch (cause) {
3320
4139
  if (hasCode(cause, "ENOENT")) return null;
3321
4140
  throw cause;
3322
4141
  }
3323
4142
  };
3324
4143
  var syncDirectory = (directory) => {
3325
- const fd = fs7.openSync(directory, "r");
4144
+ const fd = fs9.openSync(directory, "r");
3326
4145
  try {
3327
- fs7.fsyncSync(fd);
4146
+ fs9.fsyncSync(fd);
3328
4147
  } finally {
3329
- fs7.closeSync(fd);
4148
+ fs9.closeSync(fd);
3330
4149
  }
3331
4150
  };
3332
4151
  var writeDurable = (file, content, mode = 384) => {
3333
- const fd = fs7.openSync(file, "wx", mode);
4152
+ const fd = fs9.openSync(file, "wx", mode);
3334
4153
  try {
3335
- fs7.writeFileSync(fd, content, "utf8");
3336
- fs7.fchmodSync(fd, mode);
3337
- fs7.fsyncSync(fd);
4154
+ fs9.writeFileSync(fd, content, "utf8");
4155
+ fs9.fchmodSync(fd, mode);
4156
+ fs9.fsyncSync(fd);
3338
4157
  } finally {
3339
- fs7.closeSync(fd);
4158
+ fs9.closeSync(fd);
3340
4159
  }
3341
4160
  };
3342
4161
  var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
@@ -3356,11 +4175,11 @@ var parseLockOwner = (raw) => {
3356
4175
  };
3357
4176
  var describeOwner = (lockDir) => {
3358
4177
  try {
3359
- 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"));
3360
4179
  const owner = parseLockOwner(raw);
3361
4180
  if (owner === null) return "owner metadata is invalid";
3362
4181
  const label = `PID ${owner.pid} on ${owner.hostname}`;
3363
- if (owner.hostname !== os4.hostname()) return label;
4182
+ if (owner.hostname !== os5.hostname()) return label;
3364
4183
  try {
3365
4184
  process.kill(owner.pid, 0);
3366
4185
  return `${label} is still live`;
@@ -3376,28 +4195,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
3376
4195
  );
3377
4196
  var acquireLock = (configDir, create) => {
3378
4197
  if (create) {
3379
- fs7.mkdirSync(configDir, { recursive: true, mode: 448 });
3380
- fs7.chmodSync(configDir, 448);
3381
- } else if (!fs7.existsSync(configDir)) {
4198
+ fs9.mkdirSync(configDir, { recursive: true, mode: 448 });
4199
+ fs9.chmodSync(configDir, 448);
4200
+ } else if (!fs9.existsSync(configDir)) {
3382
4201
  throw new ServerStoreError(
3383
4202
  "No Mend server is configured. Run mend server setup explicitly to install one."
3384
4203
  );
3385
4204
  }
3386
- const lockDir = path7.join(configDir, "server.lock");
4205
+ const lockDir = path9.join(configDir, "server.lock");
3387
4206
  try {
3388
- fs7.mkdirSync(lockDir, { mode: 448 });
4207
+ fs9.mkdirSync(lockDir, { mode: 448 });
3389
4208
  } catch (cause) {
3390
4209
  if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
3391
4210
  throw cause;
3392
4211
  }
3393
- const owned = fs7.statSync(lockDir);
3394
- const ownerFile = path7.join(lockDir, "owner.json");
3395
- 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() })}
3396
4215
  `;
3397
4216
  let open = true;
3398
4217
  const assertOwned = () => {
3399
- const current = fs7.statSync(lockDir);
3400
- 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) {
3401
4220
  throw new ServerStoreError(
3402
4221
  "Server lock ownership was lost. Stop and check for another server command before retrying."
3403
4222
  );
@@ -3413,8 +4232,8 @@ var acquireLock = (configDir, create) => {
3413
4232
  release: () => {
3414
4233
  const result = attempt(() => {
3415
4234
  assertOwned();
3416
- fs7.unlinkSync(ownerFile);
3417
- fs7.rmdirSync(lockDir);
4235
+ fs9.unlinkSync(ownerFile);
4236
+ fs9.rmdirSync(lockDir);
3418
4237
  });
3419
4238
  open = false;
3420
4239
  return result;
@@ -3423,7 +4242,7 @@ var acquireLock = (configDir, create) => {
3423
4242
  };
3424
4243
  var readIdentity = (paths) => {
3425
4244
  const identity = readOptional(paths.identity);
3426
- 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)) {
3427
4246
  throw new ServerStoreError(
3428
4247
  "Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
3429
4248
  );
@@ -3433,7 +4252,7 @@ var readIdentity = (paths) => {
3433
4252
  var readActive = (paths) => {
3434
4253
  let target;
3435
4254
  try {
3436
- target = fs7.readlinkSync(paths.active);
4255
+ target = fs9.readlinkSync(paths.active);
3437
4256
  } catch (cause) {
3438
4257
  if (hasCode(cause, "ENOENT")) return null;
3439
4258
  throw cause;
@@ -3443,13 +4262,13 @@ var readActive = (paths) => {
3443
4262
  "Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
3444
4263
  );
3445
4264
  }
3446
- const directory = path7.join(paths.configDir, target);
4265
+ const directory = path9.join(paths.configDir, target);
3447
4266
  const files = {
3448
- identity: fs7.readFileSync(path7.join(directory, fileNames.identity), "utf8"),
3449
- config: fs7.readFileSync(path7.join(directory, fileNames.config), "utf8"),
3450
- env: fs7.readFileSync(path7.join(directory, fileNames.env), "utf8"),
3451
- compose: fs7.readFileSync(path7.join(directory, fileNames.compose), "utf8"),
3452
- 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")
3453
4272
  };
3454
4273
  if (readIdentity(paths) !== files.identity) {
3455
4274
  throw new ServerStoreError(
@@ -3459,20 +4278,20 @@ var readActive = (paths) => {
3459
4278
  return { directory, files };
3460
4279
  };
3461
4280
  var publishIdentity = (paths, identity) => {
3462
- const temporary = path7.join(paths.configDir, `.identity-${randomUUID()}`);
4281
+ const temporary = path9.join(paths.configDir, `.identity-${randomUUID()}`);
3463
4282
  writeDurable(temporary, identity);
3464
- fs7.linkSync(temporary, paths.identity);
4283
+ fs9.linkSync(temporary, paths.identity);
3465
4284
  syncDirectory(paths.configDir);
3466
- fs7.unlinkSync(temporary);
4285
+ fs9.unlinkSync(temporary);
3467
4286
  };
3468
4287
  var prepareGeneration = (paths, files) => {
3469
- fs7.mkdirSync(paths.generations, { recursive: true, mode: 448 });
4288
+ fs9.mkdirSync(paths.generations, { recursive: true, mode: 448 });
3470
4289
  const generationName = `gen-${randomUUID()}`;
3471
- const directory = path7.join(paths.generations, generationName);
3472
- fs7.mkdirSync(directory, { mode: 448 });
4290
+ const directory = path9.join(paths.generations, generationName);
4291
+ fs9.mkdirSync(directory, { mode: 448 });
3473
4292
  for (const key of fileKeys) {
3474
4293
  writeDurable(
3475
- path7.join(directory, fileNames[key]),
4294
+ path9.join(directory, fileNames[key]),
3476
4295
  files[key],
3477
4296
  key === "postgresInit" ? 493 : 384
3478
4297
  );
@@ -3483,18 +4302,18 @@ var prepareGeneration = (paths, files) => {
3483
4302
  return { directory, files };
3484
4303
  };
3485
4304
  var activateGeneration = (paths, generation) => {
3486
- const relative4 = path7.relative(paths.configDir, generation.directory);
3487
- 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")) {
3488
4307
  throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
3489
4308
  }
3490
4309
  for (const key of fileKeys) {
3491
- 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]) {
3492
4311
  throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
3493
4312
  }
3494
4313
  }
3495
- const pointer = path7.join(paths.configDir, `.active-${randomUUID()}`);
3496
- fs7.symlinkSync(relative4, pointer);
3497
- 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);
3498
4317
  syncDirectory(paths.configDir);
3499
4318
  };
3500
4319
  var prepareFiles = (paths, files) => {
@@ -3503,7 +4322,7 @@ var prepareFiles = (paths, files) => {
3503
4322
  throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
3504
4323
  }
3505
4324
  const active = readActive(paths);
3506
- 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)
3507
4326
  return active;
3508
4327
  if (identity === null) publishIdentity(paths, files.identity);
3509
4328
  return prepareGeneration(paths, files);
@@ -3516,9 +4335,9 @@ var commitGeneration = (paths, files) => {
3516
4335
  var createStore = (configDir, lock) => {
3517
4336
  const paths = {
3518
4337
  configDir,
3519
- identity: path7.join(configDir, "identity.env"),
3520
- generations: path7.join(configDir, "generations"),
3521
- active: path7.join(configDir, "active")
4338
+ identity: path9.join(configDir, "identity.env"),
4339
+ generations: path9.join(configDir, "generations"),
4340
+ active: path9.join(configDir, "active")
3522
4341
  };
3523
4342
  const whileOwned = (operation) => attempt(() => {
3524
4343
  lock.assertOwned();
@@ -3531,13 +4350,13 @@ var createStore = (configDir, lock) => {
3531
4350
  prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
3532
4351
  activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
3533
4352
  createBackup: (previous, target) => whileOwned(() => {
3534
- const backups = path7.join(configDir, "backups");
3535
- fs7.mkdirSync(backups, { recursive: true, mode: 448 });
3536
- fs7.chmodSync(backups, 448);
3537
- const directory = path7.join(backups, `upgrade-${randomUUID()}`);
3538
- 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 });
3539
4358
  writeDurable(
3540
- path7.join(directory, "recovery.json"),
4359
+ path9.join(directory, "recovery.json"),
3541
4360
  `${JSON.stringify(
3542
4361
  {
3543
4362
  previousGeneration: previous.directory,
@@ -3553,21 +4372,21 @@ var createStore = (configDir, lock) => {
3553
4372
  syncDirectory(directory);
3554
4373
  syncDirectory(backups);
3555
4374
  syncDirectory(configDir);
3556
- const partialFile = path7.join(directory, "database.sql.partial");
4375
+ const partialFile = path9.join(directory, "database.sql.partial");
3557
4376
  return {
3558
4377
  directory,
3559
4378
  partialFile,
3560
4379
  complete: () => whileOwned(() => {
3561
- if (fs7.statSync(partialFile).size === 0)
4380
+ if (fs9.statSync(partialFile).size === 0)
3562
4381
  throw new ServerStoreError("Database backup is empty; refusing target startup.");
3563
- const fd = fs7.openSync(partialFile, "r");
4382
+ const fd = fs9.openSync(partialFile, "r");
3564
4383
  try {
3565
- fs7.fchmodSync(fd, 384);
3566
- fs7.fsyncSync(fd);
4384
+ fs9.fchmodSync(fd, 384);
4385
+ fs9.fsyncSync(fd);
3567
4386
  } finally {
3568
- fs7.closeSync(fd);
4387
+ fs9.closeSync(fd);
3569
4388
  }
3570
- fs7.renameSync(partialFile, path7.join(directory, "database.sql"));
4389
+ fs9.renameSync(partialFile, path9.join(directory, "database.sql"));
3571
4390
  syncDirectory(directory);
3572
4391
  })
3573
4392
  };
@@ -3576,7 +4395,7 @@ var createStore = (configDir, lock) => {
3576
4395
  };
3577
4396
  var refuseFlatLayout = (configDir) => {
3578
4397
  if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
3579
- (name) => fs7.existsSync(path7.join(configDir, name))
4398
+ (name) => fs9.existsSync(path9.join(configDir, name))
3580
4399
  )) {
3581
4400
  throw new ServerStoreError(
3582
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."
@@ -3584,7 +4403,7 @@ var refuseFlatLayout = (configDir) => {
3584
4403
  }
3585
4404
  };
3586
4405
  var withServerStore = async (directory, operation, options = {}) => {
3587
- const configDir = path7.resolve(directory);
4406
+ const configDir = path9.resolve(directory);
3588
4407
  let lock;
3589
4408
  let result;
3590
4409
  try {
@@ -3594,16 +4413,16 @@ var withServerStore = async (directory, operation, options = {}) => {
3594
4413
  } catch (cause) {
3595
4414
  result = { _tag: "error", error: storeError(cause) };
3596
4415
  } finally {
3597
- const release = lock?.release();
3598
- if (release?._tag === "error") result = release;
4416
+ const release2 = lock?.release();
4417
+ if (release2?._tag === "error") result = release2;
3599
4418
  }
3600
4419
  return result;
3601
4420
  };
3602
4421
 
3603
4422
  // src/version.ts
3604
- import * as fs8 from "node:fs";
4423
+ import * as fs10 from "node:fs";
3605
4424
  var cliVersion = () => {
3606
- 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");
3607
4426
  const parsed = JSON.parse(raw);
3608
4427
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
3609
4428
  const version = parsed.version;
@@ -3773,7 +4592,7 @@ var parseHttpOrigin = (input, label) => {
3773
4592
  return parsed.origin;
3774
4593
  };
3775
4594
  var isLoopbackBind = (bind) => bind === "127.0.0.1" || bind === "::1" || bind.startsWith("127.");
3776
- 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.");
3777
4596
  var resolveAppUrl = (existing, options, appPort) => {
3778
4597
  const oldLocalUrl = existing === null ? null : `http://localhost:${existing.appPort}`;
3779
4598
  const portChangedOnLocalhost = options.appPort !== void 0 && existing?.appUrl === oldLocalUrl;
@@ -4235,6 +5054,30 @@ var readServerInstallation = (store) => {
4235
5054
  };
4236
5055
  }
4237
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
+ };
4238
5081
  var persistSetup = (store, config, secrets, assets) => storeValue(
4239
5082
  store.prepare({
4240
5083
  identity: renderIdentity(secrets),
@@ -4282,7 +5125,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
4282
5125
  return version;
4283
5126
  };
4284
5127
  var parseDockerSocketPath = (socket) => {
4285
- if (!path8.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
5128
+ if (!path10.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
4286
5129
  throw setupError(
4287
5130
  "--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
4288
5131
  );
@@ -4308,8 +5151,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
4308
5151
  if (options.assetsDir !== void 0) {
4309
5152
  try {
4310
5153
  const [compose2, postgresInit2] = await Promise.all([
4311
- readFile(path8.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
4312
- 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")
4313
5156
  ]);
4314
5157
  validateComposeAsset(compose2);
4315
5158
  validatePostgresAsset(postgresInit2);
@@ -4822,10 +5665,10 @@ var manageServer = async (command, args, runtime, store) => {
4822
5665
  };
4823
5666
  var nodeServerRuntime = () => {
4824
5667
  const environment = { ...process.env };
4825
- const home = os5.homedir();
4826
- 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");
4827
5670
  return {
4828
- configDir: path8.join(configHome, "mend"),
5671
+ configDir: path10.join(configHome, "mend"),
4829
5672
  platform: process.platform,
4830
5673
  cliVersion: cliVersion(),
4831
5674
  run: (command, args, options) => runServerProcess(command, args, environment, options),
@@ -4842,7 +5685,7 @@ var nodeServerRuntime = () => {
4842
5685
  }
4843
5686
  },
4844
5687
  randomBytes,
4845
- sleep: (milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)),
5688
+ sleep: (milliseconds) => new Promise((resolve7) => setTimeout(resolve7, milliseconds)),
4846
5689
  writeLine: (line) => process.stdout.write(`${line}
4847
5690
  `)
4848
5691
  };
@@ -5064,9 +5907,9 @@ var touchesSessionServices = (payload, sessionId) => {
5064
5907
  return false;
5065
5908
  }
5066
5909
  if (typeof parsed !== "object" || parsed === null) return false;
5067
- const type = "type" in parsed ? parsed.type : void 0;
5910
+ const type2 = "type" in parsed ? parsed.type : void 0;
5068
5911
  const session = "sessionId" in parsed ? parsed.sessionId : void 0;
5069
- return (type === "session-process" || type === "session") && session === sessionId;
5912
+ return (type2 === "session-process" || type2 === "session") && session === sessionId;
5070
5913
  };
5071
5914
  var pumpConnection = (socket, url) => {
5072
5915
  socket.pause();
@@ -5092,7 +5935,7 @@ var pumpConnection = (socket, url) => {
5092
5935
  () => socket.destroy()
5093
5936
  );
5094
5937
  };
5095
- var listenLocal = (server, port, fallback) => new Promise((resolve6, reject) => {
5938
+ var listenLocal = (server, port, fallback) => new Promise((resolve7, reject) => {
5096
5939
  const attempt2 = (candidate, lastTry) => {
5097
5940
  const onError = (error) => {
5098
5941
  if (!lastTry) {
@@ -5105,7 +5948,7 @@ var listenLocal = (server, port, fallback) => new Promise((resolve6, reject) =>
5105
5948
  server.listen(candidate, "127.0.0.1", () => {
5106
5949
  server.removeListener("error", onError);
5107
5950
  const address = server.address();
5108
- resolve6(typeof address === "object" && address !== null ? address.port : candidate);
5951
+ resolve7(typeof address === "object" && address !== null ? address.port : candidate);
5109
5952
  });
5110
5953
  };
5111
5954
  attempt2(port, !fallback || port === 0);
@@ -5134,7 +5977,7 @@ var serverEvents = (config) => async (handlers, signal) => {
5134
5977
  if (signal.aborted) return;
5135
5978
  }
5136
5979
  connected = true;
5137
- await new Promise((resolve6) => setTimeout(resolve6, 2e3));
5980
+ await new Promise((resolve7) => setTimeout(resolve7, 2e3));
5138
5981
  }
5139
5982
  };
5140
5983
  var createServiceTunnels = (options) => {
@@ -5277,8 +6120,8 @@ var createServiceTunnels = (options) => {
5277
6120
  };
5278
6121
 
5279
6122
  // src/skills.ts
5280
- import * as fs9 from "node:fs";
5281
- import * as path9 from "node:path";
6123
+ import * as fs11 from "node:fs";
6124
+ import * as path11 from "node:path";
5282
6125
  var DEFAULT_SKILLS_DIR = ".agents/skills";
5283
6126
  var MAX_SKILL_FILES = 64;
5284
6127
  var MAX_SKILL_FILE_BYTES = 512 * 1024;
@@ -5301,12 +6144,12 @@ var readFrontmatterDescription = (contents) => {
5301
6144
  var walkFiles = (root) => {
5302
6145
  const found = [];
5303
6146
  const visit = (dir) => {
5304
- for (const entry of fs9.readdirSync(dir, { withFileTypes: true })) {
6147
+ for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
5305
6148
  if (IGNORED_DIRS.has(entry.name)) continue;
5306
- const absolute = path9.join(dir, entry.name);
6149
+ const absolute = path11.join(dir, entry.name);
5307
6150
  let stat;
5308
6151
  try {
5309
- stat = fs9.statSync(absolute);
6152
+ stat = fs11.statSync(absolute);
5310
6153
  } catch {
5311
6154
  continue;
5312
6155
  }
@@ -5320,7 +6163,7 @@ var walkFiles = (root) => {
5320
6163
  var scanSkillLibrary = (root) => {
5321
6164
  let entries;
5322
6165
  try {
5323
- entries = fs9.readdirSync(root, { withFileTypes: true });
6166
+ entries = fs11.readdirSync(root, { withFileTypes: true });
5324
6167
  } catch {
5325
6168
  return { error: `${root} is not a readable directory` };
5326
6169
  }
@@ -5328,23 +6171,23 @@ var scanSkillLibrary = (root) => {
5328
6171
  const notes = [];
5329
6172
  for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
5330
6173
  if (entry.name.startsWith(".")) continue;
5331
- const skillDir = path9.join(root, entry.name);
6174
+ const skillDir = path11.join(root, entry.name);
5332
6175
  let stat;
5333
6176
  try {
5334
- stat = fs9.statSync(skillDir);
6177
+ stat = fs11.statSync(skillDir);
5335
6178
  } catch {
5336
6179
  continue;
5337
6180
  }
5338
6181
  if (!stat.isDirectory()) continue;
5339
- if (!fs9.existsSync(path9.join(skillDir, "SKILL.md"))) {
6182
+ if (!fs11.existsSync(path11.join(skillDir, "SKILL.md"))) {
5340
6183
  notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
5341
6184
  continue;
5342
6185
  }
5343
6186
  const files = [];
5344
6187
  let bytes = 0;
5345
6188
  for (const absolute of walkFiles(skillDir)) {
5346
- const relative4 = path9.relative(skillDir, absolute).split(path9.sep).join("/");
5347
- const buffer = fs9.readFileSync(absolute);
6189
+ const relative4 = path11.relative(skillDir, absolute).split(path11.sep).join("/");
6190
+ const buffer = fs11.readFileSync(absolute);
5348
6191
  if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
5349
6192
  notes.push({ skill: entry.name, message: `${relative4} is over 512KB \u2014 skipped` });
5350
6193
  continue;
@@ -5379,16 +6222,16 @@ var scanSkillLibrary = (root) => {
5379
6222
  };
5380
6223
 
5381
6224
  // src/ssh-setup.ts
5382
- import * as os7 from "node:os";
5383
- import * as path11 from "node:path";
6225
+ import * as os8 from "node:os";
6226
+ import * as path13 from "node:path";
5384
6227
 
5385
6228
  // ../../packages/workspace-ssh/src/index.ts
5386
6229
  import { spawnSync as spawnSync3 } from "node:child_process";
5387
6230
  import { createHash as createHash3 } from "node:crypto";
5388
- import * as fs10 from "node:fs";
6231
+ import * as fs12 from "node:fs";
5389
6232
  import { isIP as isIP2 } from "node:net";
5390
- import * as os6 from "node:os";
5391
- import * as path10 from "node:path";
6233
+ import * as os7 from "node:os";
6234
+ import * as path12 from "node:path";
5392
6235
  var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
5393
6236
  var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
5394
6237
  var WorkspaceSshError = class extends Error {
@@ -5405,16 +6248,16 @@ var WorkspaceSshError = class extends Error {
5405
6248
  };
5406
6249
  var success = (value) => ({ ok: true, value });
5407
6250
  var failure = (error) => ({ ok: false, error });
5408
- 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;
5409
6252
  var hashIdentity = (identity) => createHash3("sha256").update(identity).digest("hex").slice(0, 24);
5410
- var aliasLabel = (hostname6) => {
5411
- 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);
5412
6255
  return label === "" ? "server" : label;
5413
6256
  };
5414
6257
  var parseHostOverride = (raw) => {
5415
- const hostname6 = raw.trim();
5416
- if (hostname6 === "") return failure(new WorkspaceSshError("target", "SSH hostname is empty."));
5417
- 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)) {
5418
6261
  return failure(
5419
6262
  new WorkspaceSshError(
5420
6263
  "target",
@@ -5422,7 +6265,7 @@ var parseHostOverride = (raw) => {
5422
6265
  )
5423
6266
  );
5424
6267
  }
5425
- if (/^[^:]+:\d+$/.test(hostname6)) {
6268
+ if (/^[^:]+:\d+$/.test(hostname7)) {
5426
6269
  return failure(
5427
6270
  new WorkspaceSshError(
5428
6271
  "target",
@@ -5430,11 +6273,11 @@ var parseHostOverride = (raw) => {
5430
6273
  )
5431
6274
  );
5432
6275
  }
5433
- if ((hostname6.startsWith("[") || hostname6.endsWith("]")) && !/^\[[^\]]+\]$/.test(hostname6)) {
6276
+ if ((hostname7.startsWith("[") || hostname7.endsWith("]")) && !/^\[[^\]]+\]$/.test(hostname7)) {
5434
6277
  return failure(new WorkspaceSshError("target", "SSH hostname contains invalid IPv6 brackets."));
5435
6278
  }
5436
- const unbracketed = stripIpv6Brackets(hostname6);
5437
- if ((hostname6.startsWith("[") || unbracketed.includes(":")) && isIP2(unbracketed) !== 6) {
6279
+ const unbracketed = stripIpv6Brackets(hostname7);
6280
+ if ((hostname7.startsWith("[") || unbracketed.includes(":")) && isIP2(unbracketed) !== 6) {
5438
6281
  return failure(
5439
6282
  new WorkspaceSshError("target", "SSH hostname contains an invalid IPv6 address.")
5440
6283
  );
@@ -5475,8 +6318,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
5475
6318
  var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
5476
6319
  var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
5477
6320
  var parseIdentityPath = (value) => {
5478
- const resolved = path10.resolve(
5479
- 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
5480
6323
  );
5481
6324
  if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
5482
6325
  return failure(
@@ -5632,7 +6475,7 @@ var agentKeys = () => {
5632
6475
  };
5633
6476
  var readText = (file) => {
5634
6477
  try {
5635
- return success(fs10.readFileSync(file, "utf8"));
6478
+ return success(fs12.readFileSync(file, "utf8"));
5636
6479
  } catch (cause) {
5637
6480
  const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
5638
6481
  return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
@@ -5690,19 +6533,19 @@ var readRequiredKey = (keyPath, source) => {
5690
6533
  var readOptionalKey = (keyPath) => {
5691
6534
  const parsed = parseIdentityPath(keyPath);
5692
6535
  if (!parsed.ok) return parsed;
5693
- if (!fs10.existsSync(parsed.value) && !fs10.existsSync(publicKeyPath(parsed.value)))
6536
+ if (!fs12.existsSync(parsed.value) && !fs12.existsSync(publicKeyPath(parsed.value)))
5694
6537
  return success(null);
5695
6538
  return readRequiredKey(parsed.value, "existing");
5696
6539
  };
5697
6540
  var pinAgentKey = (configHome, publicKey) => {
5698
- const identityFile = path10.resolve(
6541
+ const identityFile = path12.resolve(
5699
6542
  configHome,
5700
6543
  "ssh",
5701
6544
  `agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
5702
6545
  );
5703
6546
  try {
5704
- fs10.mkdirSync(path10.dirname(identityFile), { recursive: true, mode: 448 });
5705
- fs10.writeFileSync(identityFile, `${publicKey}
6547
+ fs12.mkdirSync(path12.dirname(identityFile), { recursive: true, mode: 448 });
6548
+ fs12.writeFileSync(identityFile, `${publicKey}
5706
6549
  `, { mode: 384 });
5707
6550
  return success({ publicKey, identityFile, source: "agent" });
5708
6551
  } catch (cause) {
@@ -5713,15 +6556,15 @@ var pinAgentKey = (configHome, publicKey) => {
5713
6556
  };
5714
6557
  var generateDedicatedKey = (privatePath) => {
5715
6558
  try {
5716
- fs10.mkdirSync(path10.dirname(privatePath), { recursive: true, mode: 448 });
6559
+ fs12.mkdirSync(path12.dirname(privatePath), { recursive: true, mode: 448 });
5717
6560
  } catch (cause) {
5718
6561
  return failure(
5719
- new WorkspaceSshError("key", `Could not create ${path10.dirname(privatePath)}.`, cause)
6562
+ new WorkspaceSshError("key", `Could not create ${path12.dirname(privatePath)}.`, cause)
5720
6563
  );
5721
6564
  }
5722
6565
  const generated = spawnSync3(
5723
6566
  "ssh-keygen",
5724
- ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os6.hostname()}`],
6567
+ ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os7.hostname()}`],
5725
6568
  { encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
5726
6569
  );
5727
6570
  if (generated.status !== 0) {
@@ -5745,7 +6588,7 @@ var generateDedicatedKey = (privatePath) => {
5745
6588
  return fresh;
5746
6589
  };
5747
6590
  var dedicatedKey = (configHome, create) => {
5748
- const privatePath = path10.join(configHome, "ssh", "id_ed25519");
6591
+ const privatePath = path12.join(configHome, "ssh", "id_ed25519");
5749
6592
  const existing = readOptionalKey(privatePath);
5750
6593
  if (existing.ok === false) return failure(existing.error);
5751
6594
  if (existing.value !== null || !create) return existing;
@@ -5793,10 +6636,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
5793
6636
  const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
5794
6637
  if (reconciled.ok === false) return failure(reconciled.error);
5795
6638
  try {
5796
- fs10.mkdirSync(path10.dirname(configFile), { recursive: true, mode: 448 });
5797
- fs10.chmodSync(path10.dirname(configFile), 448);
5798
- fs10.writeFileSync(configFile, reconciled.value, { mode: 384 });
5799
- 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);
5800
6643
  return success(void 0);
5801
6644
  } catch (cause) {
5802
6645
  return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
@@ -5809,7 +6652,7 @@ var dim5 = ansi("2");
5809
6652
  var green5 = ansi("32");
5810
6653
  var warn = ansi("33");
5811
6654
  var say5 = (line) => console.log(line);
5812
- var sshConfigPath = () => path11.join(os7.homedir(), ".ssh", "config");
6655
+ var sshConfigPath = () => path13.join(os8.homedir(), ".ssh", "config");
5813
6656
  var flagValue = (args, flag) => {
5814
6657
  const index = args.indexOf(flag);
5815
6658
  return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
@@ -5879,7 +6722,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
5879
6722
  if (picked.value === null) return showFailure("No workspace SSH key is available.");
5880
6723
  const registered = await api2("POST", "/workspace-ssh/keys", {
5881
6724
  publicKey: picked.value.publicKey,
5882
- name: os7.hostname()
6725
+ name: os8.hostname()
5883
6726
  });
5884
6727
  const sourceLabel = {
5885
6728
  explicit: "from --key",
@@ -5921,8 +6764,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
5921
6764
  };
5922
6765
 
5923
6766
  // src/uninstall.ts
5924
- import * as fs11 from "node:fs";
5925
- import * as path12 from "node:path";
6767
+ import * as fs13 from "node:fs";
6768
+ import * as path14 from "node:path";
5926
6769
  var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
5927
6770
  var parseUninstallArgs = (args) => {
5928
6771
  let scope = null;
@@ -5944,14 +6787,14 @@ var parseUninstallArgs = (args) => {
5944
6787
  var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
5945
6788
  var countEntries = (directory) => {
5946
6789
  try {
5947
- return fs11.readdirSync(directory).length;
6790
+ return fs13.readdirSync(directory).length;
5948
6791
  } catch {
5949
6792
  return 0;
5950
6793
  }
5951
6794
  };
5952
6795
  var exists = (file) => {
5953
6796
  try {
5954
- fs11.lstatSync(file);
6797
+ fs13.lstatSync(file);
5955
6798
  return true;
5956
6799
  } catch {
5957
6800
  return false;
@@ -5960,7 +6803,7 @@ var exists = (file) => {
5960
6803
  var managedBlockCount = (sshConfigFile) => {
5961
6804
  let config;
5962
6805
  try {
5963
- config = fs11.readFileSync(sshConfigFile, "utf8");
6806
+ config = fs13.readFileSync(sshConfigFile, "utf8");
5964
6807
  } catch {
5965
6808
  return 0;
5966
6809
  }
@@ -5984,27 +6827,27 @@ var describeUninstall = async (runtime, scope) => {
5984
6827
  if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
5985
6828
  server = "none";
5986
6829
  } else if (read.value === null) {
5987
- server = exists(path12.join(configDir, "identity.env")) ? {
6830
+ server = exists(path14.join(configDir, "identity.env")) ? {
5988
6831
  version: "(no active generation)",
5989
6832
  appUrl: "",
5990
6833
  dockerContext: "",
5991
- generations: countEntries(path12.join(configDir, "generations")),
5992
- backups: countEntries(path12.join(configDir, "backups"))
6834
+ generations: countEntries(path14.join(configDir, "generations")),
6835
+ backups: countEntries(path14.join(configDir, "backups"))
5993
6836
  } : "none";
5994
6837
  } else {
5995
6838
  server = {
5996
6839
  version: read.value.config.serverVersion,
5997
6840
  appUrl: read.value.config.appUrl,
5998
6841
  dockerContext: read.value.config.dockerContext,
5999
- generations: countEntries(path12.join(configDir, "generations")),
6000
- backups: countEntries(path12.join(configDir, "backups"))
6842
+ generations: countEntries(path14.join(configDir, "generations")),
6843
+ backups: countEntries(path14.join(configDir, "backups"))
6001
6844
  };
6002
6845
  }
6003
6846
  }
6004
6847
  let home = null;
6005
6848
  if (scope !== "server") {
6006
- const cliConfig = path12.join(runtime.cliHome, "cli.json");
6007
- const sshDirectory = path12.join(runtime.cliHome, "ssh");
6849
+ const cliConfig = path14.join(runtime.cliHome, "cli.json");
6850
+ const sshDirectory = path14.join(runtime.cliHome, "ssh");
6008
6851
  home = {
6009
6852
  cliConfig: exists(cliConfig) ? cliConfig : null,
6010
6853
  sshDirectory: exists(sshDirectory) ? sshDirectory : null,
@@ -6136,7 +6979,7 @@ var removeServer = async (runtime, failures, leftovers) => {
6136
6979
  }
6137
6980
  }
6138
6981
  for (const name of SERVER_FILES) {
6139
- fs11.rmSync(path12.join(configDir, name), { recursive: true, force: true });
6982
+ fs13.rmSync(path14.join(configDir, name), { recursive: true, force: true });
6140
6983
  }
6141
6984
  server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
6142
6985
  },
@@ -6147,20 +6990,20 @@ var removeServer = async (runtime, failures, leftovers) => {
6147
6990
  var removeHome = async (runtime, plan, failures, leftovers) => {
6148
6991
  const { server } = runtime;
6149
6992
  if (plan.cliConfig !== null) {
6150
- fs11.rmSync(plan.cliConfig, { force: true });
6993
+ fs13.rmSync(plan.cliConfig, { force: true });
6151
6994
  server.writeLine(`removed ${plan.cliConfig}`);
6152
6995
  }
6153
6996
  if (plan.sshDirectory !== null) {
6154
- fs11.rmSync(plan.sshDirectory, { recursive: true, force: true });
6997
+ fs13.rmSync(plan.sshDirectory, { recursive: true, force: true });
6155
6998
  server.writeLine(`removed ${plan.sshDirectory}`);
6156
6999
  }
6157
7000
  if (plan.managedSshBlocks > 0) {
6158
7001
  try {
6159
- const before = fs11.readFileSync(runtime.sshConfigFile, "utf8");
7002
+ const before = fs13.readFileSync(runtime.sshConfigFile, "utf8");
6160
7003
  const stripped = stripManagedWorkspaceSshBlocks(before);
6161
7004
  if (!stripped.ok) throw stripped.error;
6162
7005
  if (stripped.value.removed > 0) {
6163
- fs11.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
7006
+ fs13.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
6164
7007
  server.writeLine(
6165
7008
  `removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
6166
7009
  );
@@ -6173,12 +7016,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
6173
7016
  }
6174
7017
  let remaining = [];
6175
7018
  try {
6176
- remaining = fs11.readdirSync(runtime.cliHome);
7019
+ remaining = fs13.readdirSync(runtime.cliHome);
6177
7020
  } catch {
6178
7021
  return;
6179
7022
  }
6180
7023
  if (remaining.length === 0) {
6181
- fs11.rmdirSync(runtime.cliHome);
7024
+ fs13.rmdirSync(runtime.cliHome);
6182
7025
  server.writeLine(`removed ${runtime.cliHome}`);
6183
7026
  } else {
6184
7027
  leftovers.push(
@@ -6242,19 +7085,19 @@ var upgradeUrl = async (input) => {
6242
7085
  // src/main.ts
6243
7086
  var mendCliHome = () => {
6244
7087
  const xdg = process.env["XDG_CONFIG_HOME"];
6245
- const preferred = path13.join(
6246
- 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,
6247
7090
  "mend"
6248
7091
  );
6249
- const legacy = path13.join(os8.homedir(), ".mend");
6250
- 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;
6251
7094
  };
6252
- var CONFIG_PATH = path13.join(mendCliHome(), "cli.json");
7095
+ var CONFIG_PATH = path15.join(mendCliHome(), "cli.json");
6253
7096
  var loadConfig = () => {
6254
7097
  let fileConfig = {};
6255
- if (fs12.existsSync(CONFIG_PATH)) {
7098
+ if (fs14.existsSync(CONFIG_PATH)) {
6256
7099
  try {
6257
- fileConfig = JSON.parse(fs12.readFileSync(CONFIG_PATH, "utf8"));
7100
+ fileConfig = JSON.parse(fs14.readFileSync(CONFIG_PATH, "utf8"));
6258
7101
  } catch {
6259
7102
  fail6(`could not parse ${CONFIG_PATH}`);
6260
7103
  }
@@ -6445,7 +7288,7 @@ var adopt = async (config, args) => {
6445
7288
  }
6446
7289
  const sourceIssue = repositoryCloneUrlIssue(source);
6447
7290
  if (sourceIssue !== null) return fail6(sourceIssue);
6448
- const projectName = name ?? normalizeProjectName(path13.basename(source, ".git"));
7291
+ const projectName = name ?? normalizeProjectName(path15.basename(source, ".git"));
6449
7292
  const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
6450
7293
  if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
6451
7294
  return fail6(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
@@ -6494,7 +7337,7 @@ var findProject = async (config, explicit, adoptCwd = false) => {
6494
7337
  const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
6495
7338
  if (sourceIssue !== null) return fail6(sourceIssue);
6496
7339
  const adopted = await api(config, "POST", "/projects", {
6497
- name: normalizeProjectName(path13.basename(facts.repoRoot)),
7340
+ name: normalizeProjectName(path15.basename(facts.repoRoot)),
6498
7341
  source: facts.originUrl
6499
7342
  });
6500
7343
  say6(`${green6("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim6(adopted.storePath)}`);
@@ -6661,8 +7504,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
6661
7504
  const ws = new WebSocket(url);
6662
7505
  ws.binaryType = "arraybuffer";
6663
7506
  try {
6664
- await new Promise((resolve6, reject) => {
6665
- ws.addEventListener("open", () => resolve6(), { once: true });
7507
+ await new Promise((resolve7, reject) => {
7508
+ ws.addEventListener("open", () => resolve7(), { once: true });
6666
7509
  ws.addEventListener("error", () => reject(new Error("could not connect")), { once: true });
6667
7510
  });
6668
7511
  } catch {
@@ -6670,8 +7513,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
6670
7513
  }
6671
7514
  const stopHerdrHint = hintHerdrAttachment(harness);
6672
7515
  let finishAttachment;
6673
- const finished = new Promise((resolve6) => {
6674
- finishAttachment = resolve6;
7516
+ const finished = new Promise((resolve7) => {
7517
+ finishAttachment = resolve7;
6675
7518
  });
6676
7519
  const onClose = () => finishAttachment?.();
6677
7520
  if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
@@ -6899,6 +7742,12 @@ var attachPicked = async (config, session, tunnels) => {
6899
7742
  return exitAfterSessionEnd(config, session.id);
6900
7743
  };
6901
7744
  var stopCommand = async (config, args) => {
7745
+ if (args.includes("--services")) {
7746
+ const prefix2 = args.find((arg) => !arg.startsWith("--"));
7747
+ const session = await resolveLiveSession(config, prefix2, "stop --services");
7748
+ await stopServicesOf(config, session);
7749
+ return;
7750
+ }
6902
7751
  const all = args.includes("--all");
6903
7752
  const projectFlag = args.indexOf("--project");
6904
7753
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
@@ -6944,12 +7793,33 @@ var stopSessions = async (config, targets, summarise) => {
6944
7793
  say6(
6945
7794
  `${green6("\u2713")} stopped \xB7 ${session.harness} \xB7 ${dim6(session.id.slice(0, 8))} \xB7 ${session.branch}`
6946
7795
  );
7796
+ const after = await api(config, "GET", `/sessions/${session.id}`).catch(
7797
+ () => null
7798
+ );
7799
+ const hold = after === null ? null : servicesHoldOf(after.session, after.currentAgent, after.liveServices ?? 0);
7800
+ if (hold !== null) {
7801
+ say6(`${amber2(" " + hold)} \xB7 mend stop --services ${session.id.slice(0, 8)}`);
7802
+ }
6947
7803
  say6(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
6948
7804
  }
6949
7805
  if (summarise) {
6950
7806
  say6(`${green6("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
6951
7807
  }
6952
7808
  };
7809
+ var stopServicesOf = async (config, session) => {
7810
+ const result = await api(
7811
+ config,
7812
+ "POST",
7813
+ `/sessions/${session.id}/services/stop`
7814
+ );
7815
+ if (result.stopped === 0) {
7816
+ say6(`no live services \xB7 ${session.harness} \xB7 ${dim6(session.id.slice(0, 8))}`);
7817
+ return;
7818
+ }
7819
+ say6(
7820
+ `${green6("\u2713")} stopped ${result.stopped} service${result.stopped === 1 ? "" : "s"} \xB7 ${session.harness} \xB7 ${dim6(session.id.slice(0, 8))} \xB7 ${session.branch}`
7821
+ );
7822
+ };
6953
7823
  var pickSessionInteractively = async (rows) => {
6954
7824
  say6(dim6("more than one live session \u2014 pick one:"));
6955
7825
  rows.forEach((row, index) => {
@@ -7181,11 +8051,11 @@ var autoConnect = async (config, service) => {
7181
8051
  };
7182
8052
  var serviceRun = async (config, args) => {
7183
8053
  const dashdash = args.indexOf("--");
7184
- const usage = usageOf("service run");
8054
+ const usage2 = usageOf("service run");
7185
8055
  if (dashdash === -1) {
7186
8056
  const positionals = args.filter((a) => !a.startsWith("--"));
7187
8057
  const name2 = positionals.at(-1);
7188
- if (name2 === void 0) return fail6(usage);
8058
+ if (name2 === void 0) return fail6(usage2);
7189
8059
  const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
7190
8060
  const session2 = await resolveLiveSession(config, prefix2, "service run");
7191
8061
  const recipes = await api(
@@ -7214,19 +8084,19 @@ var serviceRun = async (config, args) => {
7214
8084
  return;
7215
8085
  }
7216
8086
  const argv = args.slice(dashdash + 1);
7217
- if (argv.length === 0) return fail6(usage);
8087
+ if (argv.length === 0) return fail6(usage2);
7218
8088
  const head = args.slice(0, dashdash);
7219
8089
  const portFlag = head.indexOf("--port");
7220
8090
  const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
7221
- if (!Number.isInteger(port) || port < 1 || port > 65535) return fail6(usage);
8091
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return fail6(usage2);
7222
8092
  const nameFlag = head.indexOf("--name");
7223
8093
  const name = nameFlag !== -1 && head[nameFlag + 1] !== void 0 ? String(head[nameFlag + 1]) : null;
7224
8094
  const protocol = head.includes("--udp") ? "udp" : "tcp";
7225
8095
  const http = head.includes("--http");
7226
8096
  const https = head.includes("--https");
7227
- if (http && https) return fail6(usage);
8097
+ if (http && https) return fail6(usage2);
7228
8098
  const browserScheme = https ? "https" : http ? "http" : null;
7229
- if (protocol === "udp" && browserScheme !== null) return fail6(usage);
8099
+ if (protocol === "udp" && browserScheme !== null) return fail6(usage2);
7230
8100
  const prefix = head.find(
7231
8101
  (a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
7232
8102
  );
@@ -7299,7 +8169,7 @@ var serviceLogs = async (config, args) => {
7299
8169
  cursor = page.nextFrom;
7300
8170
  const ended = page.status === "exited" || page.status === "failed";
7301
8171
  if (ended && !advanced) break;
7302
- if (!advanced) await new Promise((resolve6) => setTimeout(resolve6, 250));
8172
+ if (!advanced) await new Promise((resolve7) => setTimeout(resolve7, 250));
7303
8173
  }
7304
8174
  say6("");
7305
8175
  say6(dim6(`stream ended \xB7 next sequence ${cursor}`));
@@ -7317,13 +8187,13 @@ var serviceRestart = async (config, args) => {
7317
8187
  };
7318
8188
  var serviceInit = async (args) => {
7319
8189
  const root = gitTopLevel(process.cwd()) ?? process.cwd();
7320
- const target = path13.join(root, "mend.toml");
7321
- if (fs12.existsSync(target)) {
8190
+ const target = path15.join(root, "mend.toml");
8191
+ if (fs14.existsSync(target)) {
7322
8192
  return fail6(`${target} already exists \u2014 edit it directly (init never merges)`);
7323
8193
  }
7324
- const rootFiles = fs12.readdirSync(root);
8194
+ const rootFiles = fs14.readdirSync(root);
7325
8195
  const proposals = [];
7326
- const readRoot = (name) => fs12.readFileSync(path13.join(root, name), "utf8");
8196
+ const readRoot = (name) => fs14.readFileSync(path15.join(root, name), "utf8");
7327
8197
  if (rootFiles.includes("package.json")) {
7328
8198
  proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
7329
8199
  }
@@ -7333,13 +8203,13 @@ var serviceInit = async (args) => {
7333
8203
  );
7334
8204
  for (const glob of globs) {
7335
8205
  const parent = glob.replace(/\/?\*+$/, "");
7336
- 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];
7337
8207
  for (const dir of dirs) {
7338
- const manifest = path13.join(root, dir, "package.json");
7339
- if (!fs12.existsSync(manifest)) continue;
8208
+ const manifest = path15.join(root, dir, "package.json");
8209
+ if (!fs14.existsSync(manifest)) continue;
7340
8210
  for (const proposal of proposeFromWorkspacePackage(
7341
- path13.basename(dir),
7342
- fs12.readFileSync(manifest, "utf8"),
8211
+ path15.basename(dir),
8212
+ fs14.readFileSync(manifest, "utf8"),
7343
8213
  rootFiles
7344
8214
  )) {
7345
8215
  if (!proposals.some((existing) => existing.name === proposal.name)) {
@@ -7383,7 +8253,7 @@ var serviceInit = async (args) => {
7383
8253
  return;
7384
8254
  }
7385
8255
  }
7386
- fs12.writeFileSync(target, toml);
8256
+ fs14.writeFileSync(target, toml);
7387
8257
  say6(`${green6("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
7388
8258
  };
7389
8259
  var tunnelServices = async (config, services, portOverride) => {
@@ -7431,7 +8301,7 @@ var attachTunnels = (config, optOut) => {
7431
8301
  start: async (sessionId) => {
7432
8302
  await Promise.race([
7433
8303
  tunnels.focus(sessionId),
7434
- new Promise((resolve6) => setTimeout(resolve6, 3e3))
8304
+ new Promise((resolve7) => setTimeout(resolve7, 3e3))
7435
8305
  ]);
7436
8306
  },
7437
8307
  attached: (work) => {
@@ -7487,18 +8357,18 @@ var serviceCommand = async (config, args) => {
7487
8357
  }
7488
8358
  };
7489
8359
  var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
7490
- var readIfExists = (file) => fs12.existsSync(file) ? fs12.readFileSync(file, "utf8") : null;
8360
+ var readIfExists = (file) => fs14.existsSync(file) ? fs14.readFileSync(file, "utf8") : null;
7491
8361
  var localCredential = (provider) => {
7492
- const home = os8.homedir();
8362
+ const home = os9.homedir();
7493
8363
  switch (provider) {
7494
8364
  case "codex":
7495
8365
  return readIfExists(
7496
- 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")
7497
8367
  );
7498
8368
  case "claude":
7499
8369
  return readIfExists(
7500
- path13.join(
7501
- process.env["CLAUDE_CONFIG_DIR"] ?? path13.join(home, ".claude"),
8370
+ path15.join(
8371
+ process.env["CLAUDE_CONFIG_DIR"] ?? path15.join(home, ".claude"),
7502
8372
  ".credentials.json"
7503
8373
  )
7504
8374
  );
@@ -7522,19 +8392,64 @@ var accountLine = (account) => {
7522
8392
  ].filter((fact) => fact !== null);
7523
8393
  return `${account.provider.padEnd(8)} ${facts.join(" \xB7 ")}`;
7524
8394
  };
7525
- var accountsCommand = async (config) => {
7526
- const identity = await api(config, "GET", "/me/sealant");
7527
- process.stdout.write(`platform user ${identity.sealantUserId}
7528
- `);
8395
+ var accountsLines = async (config) => {
8396
+ const identity = await request(config, "GET", "/me/sealant");
7529
8397
  const providers = ["claude", "codex", "github"];
7530
- for (const provider of providers) {
7531
- const account = identity.accounts.find((row) => row.provider === provider && row.name === "default") ?? identity.accounts.find((row) => row.provider === provider);
7532
- process.stdout.write(
7533
- ` ${account === void 0 ? `${provider.padEnd(8)} not connected` : accountLine(account)}
7534
- `
7535
- );
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));
7536
8412
  }
8413
+ for (const line of lines) process.stdout.write(`${line}
8414
+ `);
7537
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
+ });
7538
8453
  var minuteOrUnknown = (at) => at === null ? "unknown" : at.toISOString().slice(0, 16);
7539
8454
  var staleGrantReason = (secret) => {
7540
8455
  const facts = claudeGrantFacts(secret);
@@ -7567,7 +8482,7 @@ var claudeGrant = async () => {
7567
8482
  return null;
7568
8483
  }
7569
8484
  }
7570
- if (status.configDirectory !== null && path13.resolve(status.configDirectory) !== path13.resolve(dir)) {
8485
+ if (status.configDirectory !== null && path15.resolve(status.configDirectory) !== path15.resolve(dir)) {
7571
8486
  fail6(
7572
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`
7573
8488
  );
@@ -7630,7 +8545,7 @@ var connectCommand = async (config, args) => {
7630
8545
  }
7631
8546
  let secret;
7632
8547
  if (flags.includes("--from-stdin")) {
7633
- secret = fs12.readFileSync(0, "utf8").trim();
8548
+ secret = fs14.readFileSync(0, "utf8").trim();
7634
8549
  if (secret === "") return fail6("nothing on stdin");
7635
8550
  } else if (provider === "claude" && !flags.includes("--use-my-login")) {
7636
8551
  secret = await claudeGrant();
@@ -7676,14 +8591,14 @@ var takeFlagValue4 = (args, flag) => {
7676
8591
  return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
7677
8592
  };
7678
8593
  var saveCliConfig = (next) => {
7679
- fs12.mkdirSync(path13.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
7680
- fs12.writeFileSync(
8594
+ fs14.mkdirSync(path15.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
8595
+ fs14.writeFileSync(
7681
8596
  CONFIG_PATH,
7682
8597
  `${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
7683
8598
  `,
7684
8599
  { mode: 384 }
7685
8600
  );
7686
- fs12.chmodSync(CONFIG_PATH, 384);
8601
+ fs14.chmodSync(CONFIG_PATH, 384);
7687
8602
  };
7688
8603
  var login = async (config, args) => {
7689
8604
  await loginCommand(args, {
@@ -7696,7 +8611,7 @@ var login = async (config, args) => {
7696
8611
  });
7697
8612
  };
7698
8613
  var logout = async (config) => {
7699
- if (!fs12.existsSync(CONFIG_PATH) && config.token === null) {
8614
+ if (!fs14.existsSync(CONFIG_PATH) && config.token === null) {
7700
8615
  say6(dim6("nothing saved \u2014 already signed out"));
7701
8616
  return;
7702
8617
  }
@@ -7749,8 +8664,8 @@ var uninstallCommand = async (config, args) => {
7749
8664
  const runtime = {
7750
8665
  server,
7751
8666
  cliHome: mendCliHome(),
7752
- sshConfigFile: path13.join(os8.homedir(), ".ssh", "config"),
7753
- 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 },
7754
8669
  revokeDevice: async () => {
7755
8670
  if (config.deviceId === null) return "no device id saved";
7756
8671
  try {
@@ -7814,7 +8729,7 @@ var keysShare = async (config) => {
7814
8729
  return fail6("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
7815
8730
  }
7816
8731
  await shareAgent({
7817
- url: () => socketUrl(config, "keys-bridge", { host: os8.hostname() }),
8732
+ url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
7818
8733
  agentSock,
7819
8734
  signal: new AbortController().signal,
7820
8735
  onEvent: (event) => {
@@ -7845,6 +8760,34 @@ var keysShare = async (config) => {
7845
8760
  }
7846
8761
  });
7847
8762
  };
8763
+ var gitAuthorCommand = async (config, args) => {
8764
+ const asked = parseGitAuthorArgs(args);
8765
+ switch (asked.kind) {
8766
+ case "usage":
8767
+ return fail6(usageOf("git-author"));
8768
+ case "invalid":
8769
+ return fail6(asked.message);
8770
+ case "show":
8771
+ say6(gitAuthorLine(await api(config, "GET", "/me/git-author")));
8772
+ return;
8773
+ case "clear": {
8774
+ const author = await api(config, "DELETE", "/me/git-author");
8775
+ say6(`${green6("\u2713")} git author \xB7 ${gitAuthorLine(author)}`);
8776
+ return;
8777
+ }
8778
+ case "set": {
8779
+ const author = await api(config, "PUT", "/me/git-author", {
8780
+ name: asked.name,
8781
+ email: asked.email
8782
+ });
8783
+ say6(`${green6("\u2713")} git author \xB7 ${gitAuthorLine(author)}`);
8784
+ say6(
8785
+ dim6(" sessions launched from now on commit as this, unless their dotfiles say otherwise")
8786
+ );
8787
+ return;
8788
+ }
8789
+ }
8790
+ };
7848
8791
  var keysMode = async (config, args) => {
7849
8792
  const [value] = args;
7850
8793
  if (value !== void 0 && value !== "mend-key" && value !== "bridge") {
@@ -7916,7 +8859,7 @@ ${usageOf("dotfiles repo")}`);
7916
8859
  var dotfilesSync = async (config, args) => {
7917
8860
  const all = args.includes("--all");
7918
8861
  const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
7919
- const home = os8.homedir();
8862
+ const home = os9.homedir();
7920
8863
  if (!all && requestedPaths.length === 0) {
7921
8864
  const found = scanDotfileCandidates(home);
7922
8865
  if (found.length === 0) {
@@ -7943,13 +8886,13 @@ var dotfilesSync = async (config, args) => {
7943
8886
  if ("error" in read) return fail6(read.error);
7944
8887
  const result = await api(config, "POST", "/dotfiles/snapshot", {
7945
8888
  files: read.files,
7946
- source: os8.hostname(),
8889
+ source: os9.hostname(),
7947
8890
  merge: false
7948
8891
  });
7949
8892
  const snapshot = result.snapshot;
7950
8893
  if (snapshot === null) return fail6("the server accepted the sync but reports no snapshot");
7951
8894
  say6(
7952
- `${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")}`
7953
8896
  );
7954
8897
  };
7955
8898
  var envLoad = async (config, args) => {
@@ -7963,10 +8906,10 @@ var envLoad = async (config, args) => {
7963
8906
  const positional = args.filter(
7964
8907
  (arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
7965
8908
  );
7966
- const file = path13.resolve(positional[0] ?? ".env");
8909
+ const file = path15.resolve(positional[0] ?? ".env");
7967
8910
  let contents;
7968
8911
  try {
7969
- contents = fs12.readFileSync(file, "utf8");
8912
+ contents = fs14.readFileSync(file, "utf8");
7970
8913
  } catch {
7971
8914
  return fail6(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
7972
8915
  }
@@ -7982,10 +8925,10 @@ var envLoad = async (config, args) => {
7982
8925
  { contents, allSecret, secretNames }
7983
8926
  );
7984
8927
  if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
7985
- say6(dim6(`${path13.basename(file)} has no variables`));
8928
+ say6(dim6(`${path15.basename(file)} has no variables`));
7986
8929
  return;
7987
8930
  }
7988
- say6(`${green6("\u2713")} loaded ${path13.basename(file)} into ${project2.name}`);
8931
+ say6(`${green6("\u2713")} loaded ${path15.basename(file)} into ${project2.name}`);
7989
8932
  for (const line of formatLoadReport(report, { dim: dim6, warn: amber2 })) say6(line);
7990
8933
  const plaintextUrls = report.loaded.filter(
7991
8934
  (entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
@@ -8059,7 +9002,7 @@ var envCluster = async (config, args) => {
8059
9002
  const explicitProject = takeFlagValue4(args, "--project");
8060
9003
  const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
8061
9004
  const [verb, ...rest] = positional;
8062
- const usage = usageOf("env cluster");
9005
+ const usage2 = usageOf("env cluster");
8063
9006
  const project2 = await findProject(config, explicitProject);
8064
9007
  const route = `/projects/${project2.id}/cluster-bindings`;
8065
9008
  const appliesLine = () => say6(
@@ -8071,7 +9014,7 @@ var envCluster = async (config, args) => {
8071
9014
  const [kind, objectName] = rest;
8072
9015
  if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
8073
9016
  return fail6(`env cluster add takes a kind and an object name
8074
- ${usage}`);
9017
+ ${usage2}`);
8075
9018
  }
8076
9019
  const result = await api(config, "POST", route, {
8077
9020
  kind,
@@ -8086,7 +9029,7 @@ ${usage}`);
8086
9029
  const [kind, objectName] = ref?.split("/", 2) ?? [];
8087
9030
  if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
8088
9031
  return fail6(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
8089
- ${usage}`);
9032
+ ${usage2}`);
8090
9033
  }
8091
9034
  const snapshot = await api(config, "GET", route);
8092
9035
  const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
@@ -8104,7 +9047,7 @@ ${usage}`);
8104
9047
  const [name] = rest;
8105
9048
  if (!clear && name === void 0)
8106
9049
  return fail6(`env cluster sa takes a name or --clear
8107
- ${usage}`);
9050
+ ${usage2}`);
8108
9051
  const result = await api(
8109
9052
  config,
8110
9053
  "PUT",
@@ -8124,7 +9067,7 @@ ${usage}`);
8124
9067
  return appliesLine();
8125
9068
  }
8126
9069
  return fail6(`unknown env cluster command "${verb ?? ""}"
8127
- ${usage}`);
9070
+ ${usage2}`);
8128
9071
  };
8129
9072
  var envCommand = async (config, args) => {
8130
9073
  const [verb, ...rest] = args;
@@ -8176,7 +9119,7 @@ var skillsList = async (config, args) => {
8176
9119
  };
8177
9120
  var skillsPush = async (config, args) => {
8178
9121
  const dirFlag = takeFlagValue4(args, "--dir");
8179
- 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);
8180
9123
  const scanned = scanSkillLibrary(root);
8181
9124
  if ("error" in scanned) return fail6(scanned.error);
8182
9125
  for (const note of scanned.notes) say6(dim6(` ${note.skill}: ${note.message}`));
@@ -8248,6 +9191,7 @@ _mend() {
8248
9191
  'server:local server setup, lifecycle and upgrades'
8249
9192
  'uninstall:remove the server, local Mend files, or both'
8250
9193
  'keys:the machine Mend deploy key \u2014 init, show, share'
9194
+ 'git-author:the name and email your workspaces commit as'
8251
9195
  'skills:skill libraries \u2014 list, push'
8252
9196
  'accounts:your connected accounts on the platform'
8253
9197
  'pair:pair a phone or a second machine' 'doctor:read-only checklist of this setup'
@@ -8288,7 +9232,7 @@ _mend "$@"
8288
9232
  var BASH_COMPLETIONS = `_mend() {
8289
9233
  local cur=\${COMP_WORDS[COMP_CWORD]}
8290
9234
  if [ "$COMP_CWORD" -eq 1 ]; then
8291
- COMPREPLY=( $(compgen -W "adopt codex claude opencode run attach stop shell service server uninstall keys skills pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
9235
+ COMPREPLY=( $(compgen -W "adopt codex claude opencode run attach stop shell service server uninstall keys git-author skills pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
8292
9236
  return
8293
9237
  fi
8294
9238
  case \${COMP_WORDS[1]} in
@@ -8600,11 +9544,14 @@ var refreshCommand = async (config, args) => {
8600
9544
  }
8601
9545
  if (branches.length > 12) say6(dim6(` \u2026 ${branches.length - 12} more`));
8602
9546
  };
9547
+ var rowHold = (row) => row.annotation === void 0 ? null : servicesHoldOf(row.session, row.annotation.currentAgent, row.annotation.liveServices ?? 0);
8603
9548
  var printSessionRow = (row) => {
8604
9549
  const { session, annotation } = row;
8605
9550
  const live = ACTIVE_STATUSES.has(session.status);
8606
9551
  const status = session.status.padEnd(9);
8607
9552
  const facts = [];
9553
+ const hold = rowHold(row);
9554
+ if (hold !== null) facts.push(amber2(hold));
8608
9555
  if (session.label !== null) facts.push(session.label);
8609
9556
  if (annotation !== void 0 && annotation.openComments > 0) {
8610
9557
  facts.push(amber2(`${annotation.openComments} open`));
@@ -8624,7 +9571,7 @@ var sessionsCommand = async (config, args) => {
8624
9571
  say6(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
8625
9572
  return;
8626
9573
  }
8627
- const json = args.includes("--json");
9574
+ const json2 = args.includes("--json");
8628
9575
  const projectFlag = args.indexOf("--project");
8629
9576
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
8630
9577
  const projects = await api(config, "GET", "/projects");
@@ -8633,19 +9580,22 @@ var sessionsCommand = async (config, args) => {
8633
9580
  return fail6(`no adopted project named "${projectName}"`);
8634
9581
  }
8635
9582
  let rows;
8636
- if (all || projectName !== null) {
9583
+ if (all || projectName !== null || !json2) {
8637
9584
  const details = await Promise.all(
8638
9585
  scope.map(
8639
9586
  (p) => api(config, "GET", `/projects/${p.id}${all ? "?deadEnds=include" : ""}`)
8640
9587
  )
8641
9588
  );
8642
- rows = details.flatMap(
9589
+ const detailed = details.flatMap(
8643
9590
  (detail) => detail.sessions.map((session) => ({
8644
9591
  session,
8645
9592
  projectName: detail.project.name,
8646
9593
  annotation: detail.annotations.find((a) => a.sessionId === session.id)
8647
9594
  }))
8648
9595
  );
9596
+ rows = all || projectName !== null ? detailed : detailed.filter(
9597
+ (row) => ACTIVE_STATUSES.has(row.session.status) || rowHold(row) !== null
9598
+ );
8649
9599
  } else {
8650
9600
  const active = await api(config, "GET", "/sessions");
8651
9601
  const nameById = new Map(projects.map((p) => [p.id, p.name]));
@@ -8656,7 +9606,7 @@ var sessionsCommand = async (config, args) => {
8656
9606
  }));
8657
9607
  }
8658
9608
  if (rows.length === 0) {
8659
- if (json) {
9609
+ if (json2) {
8660
9610
  say6(JSON.stringify({ version: 1, sessions: [] }, null, 2));
8661
9611
  return;
8662
9612
  }
@@ -8671,7 +9621,7 @@ var sessionsCommand = async (config, args) => {
8671
9621
  if (aLive !== bLive) return bLive - aLive;
8672
9622
  return b.session.createdAt.localeCompare(a.session.createdAt);
8673
9623
  });
8674
- if (json) {
9624
+ if (json2) {
8675
9625
  const payload = {
8676
9626
  version: 1,
8677
9627
  sessions: rows.map(({ session, projectName: rowProjectName, annotation }) => ({
@@ -8770,11 +9720,11 @@ var buildWorktreesJson = async (config, projectName) => {
8770
9720
  return { version: 2, worktrees };
8771
9721
  };
8772
9722
  var worktreesCommand = async (config, args) => {
8773
- const json = args.includes("--json");
9723
+ const json2 = args.includes("--json");
8774
9724
  const projectFlag = args.indexOf("--project");
8775
9725
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
8776
9726
  const payload = await buildWorktreesJson(config, projectName);
8777
- if (json) {
9727
+ if (json2) {
8778
9728
  say6(JSON.stringify(payload, null, 2));
8779
9729
  return;
8780
9730
  }
@@ -8831,7 +9781,7 @@ var dashboard = async (config, options = {}) => {
8831
9781
  );
8832
9782
  process.exit(rerun.status ?? 0);
8833
9783
  }
8834
- const { runDashboard } = await import("./dashboard-OF5UXII2.js");
9784
+ const { runDashboard } = await import("./dashboard-3HNFSQZD.js");
8835
9785
  const agentShare = await startShareIfBridge(config);
8836
9786
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
8837
9787
  listServices: () => fetchServices(config),
@@ -8865,7 +9815,7 @@ var startShareIfBridge = async (config) => {
8865
9815
  }
8866
9816
  if (access.mode !== "bridge") return null;
8867
9817
  return startAgentShare({
8868
- url: () => socketUrl(config, "keys-bridge", { host: os8.hostname() }),
9818
+ url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
8869
9819
  agentSock
8870
9820
  });
8871
9821
  };
@@ -8905,10 +9855,10 @@ var manCommand = (words) => {
8905
9855
  }
8906
9856
  const version = cliVersion();
8907
9857
  const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
8908
- const file = path13.join(os8.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
8909
- fs12.writeFileSync(file, page);
9858
+ const file = path15.join(os9.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
9859
+ fs14.writeFileSync(file, page);
8910
9860
  const result = spawnSync4("man", ["-l", file], { stdio: "inherit" });
8911
- fs12.rmSync(file, { force: true });
9861
+ fs14.rmSync(file, { force: true });
8912
9862
  if (result.error !== void 0 || result.status !== 0) {
8913
9863
  say6(doc === null ? renderIndex() : renderCommand(doc));
8914
9864
  }
@@ -8949,6 +9899,8 @@ var main = async () => {
8949
9899
  return uninstallCommand(config, rest);
8950
9900
  case "keys":
8951
9901
  return keysCommand(config, rest);
9902
+ case "git-author":
9903
+ return gitAuthorCommand(config, rest);
8952
9904
  case "dotfiles":
8953
9905
  return dotfilesCommand(config, rest);
8954
9906
  case "skills":
@@ -8979,10 +9931,8 @@ var main = async () => {
8979
9931
  case "qr":
8980
9932
  return qrCommand(rest);
8981
9933
  case "doctor":
8982
- return doctorCommand(config, localCredential, () => {
8983
- const read = readGrant(claudeGrantDir(mendCliHome()));
8984
- return read.kind === "missing" ? null : read.secret;
8985
- });
9934
+ if (rest.includes("--bundle")) return doctorBundle(config, rest);
9935
+ return doctorCommand(config, localCredential, claudeGrantSecret);
8986
9936
  case "env":
8987
9937
  return envCommand(config, rest);
8988
9938
  case "ssh":