@sealant/mend 0.27.5 → 0.29.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 (75) hide show
  1. package/README.md +36 -13
  2. package/dist/{chunk-A5I5EW2Y.js → chunk-F6AJNFVE.js} +238 -8
  3. package/dist/{dashboard-G52NHJCZ.js → dashboard-ZKCPU2QJ.js} +1388 -602
  4. package/dist/main.js +1473 -663
  5. package/man/mend-accounts.1 +1 -1
  6. package/man/mend-adopt.1 +8 -2
  7. package/man/mend-attach.1 +1 -1
  8. package/man/mend-codex.1 +1 -1
  9. package/man/mend-completions.1 +1 -1
  10. package/man/mend-connect.1 +9 -4
  11. package/man/mend-continue.1 +1 -1
  12. package/man/mend-doctor.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 +11 -0
  19. package/man/mend-folder-list.1 +11 -0
  20. package/man/mend-folder-push.1 +18 -0
  21. package/man/mend-folder-rm.1 +11 -0
  22. package/man/mend-help.1 +1 -1
  23. package/man/mend-invite.1 +26 -0
  24. package/man/mend-keys-init.1 +1 -1
  25. package/man/mend-keys-mode.1 +1 -1
  26. package/man/mend-keys-share.1 +1 -1
  27. package/man/mend-keys-show.1 +1 -1
  28. package/man/mend-login.1 +1 -1
  29. package/man/mend-logout.1 +1 -1
  30. package/man/mend-man.1 +1 -1
  31. package/man/mend-members.1 +11 -0
  32. package/man/mend-operator-exposure.1 +11 -0
  33. package/man/mend-operator-gate.1 +11 -0
  34. package/man/mend-operator-grant-owner.1 +11 -0
  35. package/man/mend-operator-org-create.1 +11 -0
  36. package/man/mend-operator-org-invite-owner.1 +15 -0
  37. package/man/mend-operator-org-list.1 +11 -0
  38. package/man/mend-operator-org-rename.1 +11 -0
  39. package/man/mend-operator-reset-link.1 +11 -0
  40. package/man/mend-pair.1 +1 -1
  41. package/man/mend-projects.1 +1 -1
  42. package/man/mend-refresh.1 +1 -1
  43. package/man/mend-rejoin.1 +1 -1
  44. package/man/mend-resume.1 +1 -1
  45. package/man/mend-run.1 +1 -1
  46. package/man/mend-server-logs.1 +1 -1
  47. package/man/mend-server-restart.1 +1 -1
  48. package/man/mend-server-setup.1 +2 -2
  49. package/man/mend-server-start.1 +1 -1
  50. package/man/mend-server-status.1 +1 -1
  51. package/man/mend-server-stop.1 +1 -1
  52. package/man/mend-server-upgrade.1 +1 -1
  53. package/man/mend-server.1 +1 -1
  54. package/man/mend-service-add.1 +1 -1
  55. package/man/mend-service-connect.1 +1 -1
  56. package/man/mend-service-init.1 +1 -1
  57. package/man/mend-service-list.1 +1 -1
  58. package/man/mend-service-logs.1 +1 -1
  59. package/man/mend-service-restart.1 +1 -1
  60. package/man/mend-service-run.1 +1 -1
  61. package/man/mend-service-stop.1 +1 -1
  62. package/man/mend-session-share.1 +14 -0
  63. package/man/mend-sessions.1 +1 -1
  64. package/man/mend-shell.1 +1 -1
  65. package/man/mend-skills-push.1 +1 -1
  66. package/man/mend-skills.1 +1 -1
  67. package/man/mend-ssh-setup.1 +1 -1
  68. package/man/mend-ssh.1 +1 -1
  69. package/man/mend-stop.1 +1 -1
  70. package/man/mend-ui.1 +6 -2
  71. package/man/mend-uninstall.1 +1 -1
  72. package/man/mend-version.1 +1 -1
  73. package/man/mend-worktrees.1 +1 -1
  74. package/man/mend.1 +48 -2
  75. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  LIVE_STATUSES,
5
5
  agentIsLive,
6
6
  agentOutcome,
7
+ claudeGrantFacts,
7
8
  cwdFacts,
8
9
  gitCurrentBranch,
9
10
  gitOriginUrl,
@@ -11,32 +12,27 @@ import {
11
12
  isDetachChunk,
12
13
  isPasteChunk,
13
14
  matchProjectByCwd,
15
+ narrowCredential,
14
16
  normalizeProjectName,
15
17
  parseLaunchArgs,
16
18
  pasteBytes,
17
19
  repositoryCloneUrlIssue,
20
+ sameGrant,
18
21
  sessionDisplayName,
19
22
  trackBracketedPaste
20
- } from "./chunk-A5I5EW2Y.js";
23
+ } from "./chunk-F6AJNFVE.js";
21
24
 
22
25
  // src/main.ts
23
- import { spawn as spawn3, spawnSync as spawnSync2 } from "node:child_process";
24
- import * as fs9 from "node:fs";
26
+ import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
27
+ import * as fs11 from "node:fs";
25
28
  import { createRequire } from "node:module";
26
29
  import * as net3 from "node:net";
27
- import * as os6 from "node:os";
28
- import * as path10 from "node:path";
30
+ import * as os7 from "node:os";
31
+ import * as path12 from "node:path";
29
32
 
30
33
  // src/agent-share.ts
31
34
  import * as net from "node:net";
32
35
  import { setTimeout as delay } from "node:timers/promises";
33
- var bridgeUrlOf = (serverUrl, token, host) => {
34
- const url = new URL(`${serverUrl}/api/keys/bridge/ws`);
35
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
36
- url.searchParams.set("host", host);
37
- if (token !== null) url.searchParams.set("token", token);
38
- return url;
39
- };
40
36
  var retryDelayMs = (attempt2) => Math.min(3e4, 1e3 * 2 ** Math.min(Math.max(attempt2 - 1, 0), 5));
41
37
  var SSH_AGENTC_REQUEST_IDENTITIES = 11;
42
38
  var SSH_AGENTC_SIGN_REQUEST = 13;
@@ -71,7 +67,17 @@ var shareAgent = async (options) => {
71
67
  let chain = Promise.resolve();
72
68
  let attempt2 = 0;
73
69
  while (!signal.aborted) {
74
- const ws = new WebSocket(url);
70
+ let target;
71
+ try {
72
+ target = await url();
73
+ } catch {
74
+ attempt2 += 1;
75
+ const retryMs2 = retryDelayMs(attempt2);
76
+ onEvent({ kind: "disconnected", retryMs: retryMs2 });
77
+ await sleep(retryMs2, signal);
78
+ continue;
79
+ }
80
+ const ws = new WebSocket(target);
75
81
  const onAbort = () => ws.close();
76
82
  signal.addEventListener("abort", onAbort, { once: true });
77
83
  const closed = new Promise((resolve4) => {
@@ -157,6 +163,75 @@ var startAgentShare = (options) => {
157
163
  };
158
164
  };
159
165
 
166
+ // src/claude-grant.ts
167
+ import { spawnSync } from "node:child_process";
168
+ import { createHash } from "node:crypto";
169
+ import * as fs from "node:fs";
170
+ import * as os from "node:os";
171
+ import * as path from "node:path";
172
+ var claudeGrantDir = (home) => path.join(home, "claude-grant");
173
+ var personalClaudeDir = (environment = process.env) => {
174
+ const configured = environment["CLAUDE_CONFIG_DIR"];
175
+ return configured === void 0 || configured === "" ? path.join(os.homedir(), ".claude") : configured;
176
+ };
177
+ var keychainService = (configDir) => {
178
+ const suffix = createHash("sha256").update(configDir).digest("hex").slice(0, 8);
179
+ return `Claude Code-credentials-${suffix}`;
180
+ };
181
+ var claudeCli = (environment = process.env) => ({
182
+ bin: environment["MEND_CLAUDE_BIN"] ?? "claude",
183
+ env: environment
184
+ });
185
+ var grantStatus = (cli, configDir) => {
186
+ const result = spawnSync(cli.bin, ["auth", "status", "--json"], {
187
+ encoding: "utf8",
188
+ env: { ...cli.env, CLAUDE_CONFIG_DIR: configDir },
189
+ timeout: 6e4
190
+ });
191
+ if (result.error !== void 0 || typeof result.stdout !== "string") return null;
192
+ const start = result.stdout.indexOf("{");
193
+ if (start === -1) return null;
194
+ try {
195
+ const parsed = JSON.parse(result.stdout.slice(start));
196
+ if (typeof parsed !== "object" || parsed === null) return null;
197
+ const record = { ...parsed };
198
+ const text = (key) => typeof record[key] === "string" ? record[key] : null;
199
+ return {
200
+ loggedIn: record["loggedIn"] === true,
201
+ authMethod: text("authMethod"),
202
+ configDirectory: text("configDirectory"),
203
+ email: text("email"),
204
+ subscriptionType: text("subscriptionType")
205
+ };
206
+ } catch {
207
+ return null;
208
+ }
209
+ };
210
+ var runClaudeLogin = (cli, configDir) => {
211
+ fs.mkdirSync(configDir, { recursive: true, mode: 448 });
212
+ const result = spawnSync(cli.bin, ["auth", "login", "--claudeai"], {
213
+ stdio: "inherit",
214
+ env: { ...cli.env, CLAUDE_CONFIG_DIR: configDir }
215
+ });
216
+ return result.error === void 0 && result.status === 0;
217
+ };
218
+ var readGrant = (configDir, platform = process.platform) => {
219
+ const file = path.join(configDir, ".credentials.json");
220
+ if (fs.existsSync(file)) {
221
+ const secret2 = fs.readFileSync(file, "utf8").trim();
222
+ if (secret2 !== "") return { kind: "file", secret: secret2, path: file };
223
+ }
224
+ if (platform !== "darwin") return { kind: "missing", triedPath: file, triedService: null };
225
+ const service = keychainService(configDir);
226
+ const result = spawnSync(
227
+ "security",
228
+ ["find-generic-password", "-a", os.userInfo().username, "-w", "-s", service],
229
+ { encoding: "utf8", timeout: 6e4 }
230
+ );
231
+ const secret = typeof result.stdout === "string" ? result.stdout.trim() : "";
232
+ return result.status === 0 && secret !== "" ? { kind: "keychain", secret, service } : { kind: "missing", triedPath: file, triedService: service };
233
+ };
234
+
160
235
  // src/clipboard.ts
161
236
  import { execFile } from "node:child_process";
162
237
  var IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
@@ -215,12 +290,13 @@ var readClipboardImage = async (platform = process.platform, env = process.env)
215
290
  };
216
291
 
217
292
  // src/doctor.ts
218
- import * as fs from "node:fs";
219
- import * as path from "node:path";
293
+ import * as fs2 from "node:fs";
294
+ import * as path2 from "node:path";
220
295
  var TIMEOUT_MS = 3e3;
221
296
  var MARKS = { ok: "\u2713", todo: "\u25CB", failed: "\u2717" };
222
297
  var LABEL_WIDTH = 11;
223
- var formatCheck = (check, paint4 = (_state, mark) => mark) => `${paint4(check.state, MARKS[check.state])} ${check.label.padEnd(LABEL_WIDTH)} ${check.detail}${check.fix === null ? "" : ` \u2192 ${check.fix}`}`;
298
+ var day = (at) => at.toISOString().slice(0, 10);
299
+ var formatCheck = (check, paint5 = (_state, mark) => mark) => `${paint5(check.state, MARKS[check.state])} ${check.label.padEnd(LABEL_WIDTH)} ${check.detail}${check.fix === null ? "" : ` \u2192 ${check.fix}`}`;
224
300
  var getJson = async (config, route) => {
225
301
  try {
226
302
  const response = await fetch(`${config.url}/api${route}`, {
@@ -233,6 +309,23 @@ var getJson = async (config, route) => {
233
309
  return { value: null, status: null };
234
310
  }
235
311
  };
312
+ var exposureCheck = (exposure) => {
313
+ const beyondMachine = exposure.declared !== "loopback" && exposure.originOnMachine !== true;
314
+ const gateOpen = beyondMachine ? exposure.gateOpen : 0;
315
+ const facts = [
316
+ `declared ${exposure.declared}`,
317
+ `${exposure.originScheme} origin`,
318
+ ...exposure.arrivedVia === "trusted-proxy" ? ["arrived via a trusted proxy"] : [],
319
+ ...gateOpen === 0 ? [] : [`${gateOpen} gate items open`]
320
+ ].join(" \xB7 ");
321
+ const plainBeyondMachine = beyondMachine && exposure.originScheme === "http";
322
+ return {
323
+ label: "exposure",
324
+ state: plainBeyondMachine ? "todo" : "ok",
325
+ detail: facts,
326
+ fix: plainBeyondMachine ? "serve it over https and set APP_URL to that origin" : gateOpen === 0 ? null : "mend operator exposure"
327
+ };
328
+ };
236
329
  var LOGIN_COMMANDS = {
237
330
  claude: "claude setup-token",
238
331
  codex: "codex login",
@@ -354,6 +447,39 @@ var runChecks = async (config, probes) => {
354
447
  }
355
448
  );
356
449
  }
450
+ const grant = probes.claudeGrant();
451
+ if (grant !== null) {
452
+ const facts = claudeGrantFacts(grant);
453
+ if (facts === null) {
454
+ checks.push({
455
+ label: "grant",
456
+ state: "failed",
457
+ detail: "unreadable",
458
+ fix: "mend connect claude"
459
+ });
460
+ } else if (!facts.hasRefreshToken) {
461
+ checks.push({
462
+ label: "grant",
463
+ state: "failed",
464
+ detail: "signed out",
465
+ fix: "mend connect claude"
466
+ });
467
+ } else if (facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now()) {
468
+ checks.push({
469
+ label: "grant",
470
+ state: "todo",
471
+ detail: `expired ${day(facts.refreshExpiresAt)}`,
472
+ fix: "mend connect claude"
473
+ });
474
+ } else {
475
+ checks.push({
476
+ label: "grant",
477
+ state: "ok",
478
+ detail: facts.refreshExpiresAt === null ? "Mend's own" : `Mend's own \xB7 expires ${day(facts.refreshExpiresAt)}`,
479
+ fix: null
480
+ });
481
+ }
482
+ }
357
483
  const adopted = projects === null ? null : projects.value;
358
484
  if (adopted === null) checks.push(notChecked("projects"));
359
485
  else if (adopted.length === 0) {
@@ -378,20 +504,15 @@ var runChecks = async (config, probes) => {
378
504
  );
379
505
  }
380
506
  const machine = signedIn ? await getJson(config, "/machine") : null;
381
- const tailnet = machine === null || machine.value === null ? null : machine.value.tailnet;
382
- if (tailnet === null) checks.push(notChecked("tailnet"));
383
- else if (tailnet.status === "reachable" && tailnet.address !== null) {
384
- checks.push({ label: "tailnet", state: "ok", detail: tailnet.address, fix: null });
385
- } else {
386
- checks.push({ label: "tailnet", state: "todo", detail: "not detected", fix: null });
387
- }
507
+ const exposure = machine === null || machine.value === null ? void 0 : machine.value.exposure;
508
+ checks.push(exposure === void 0 ? notChecked("exposure") : exposureCheck(exposure));
388
509
  return checks;
389
510
  };
390
511
  var onPath = (command) => {
391
- for (const directory of (process.env["PATH"] ?? "").split(path.delimiter)) {
512
+ for (const directory of (process.env["PATH"] ?? "").split(path2.delimiter)) {
392
513
  if (directory === "") continue;
393
514
  try {
394
- fs.accessSync(path.join(directory, command), fs.constants.X_OK);
515
+ fs2.accessSync(path2.join(directory, command), fs2.constants.X_OK);
395
516
  return true;
396
517
  } catch {
397
518
  }
@@ -403,16 +524,16 @@ var paintMark = (state, mark) => {
403
524
  const code = state === "ok" ? "32" : state === "todo" ? "33" : "31";
404
525
  return `\x1B[${code}m${mark}\x1B[0m`;
405
526
  };
406
- var doctorCommand = async (config, localCredential2) => {
407
- const checks = await runChecks(config, { localCredential: localCredential2, onPath });
527
+ var doctorCommand = async (config, localCredential2, claudeGrant2) => {
528
+ const checks = await runChecks(config, { localCredential: localCredential2, claudeGrant: claudeGrant2, onPath });
408
529
  for (const check of checks) process.stdout.write(`${formatCheck(check, paintMark)}
409
530
  `);
410
531
  if (checks.some((check) => check.state === "failed")) process.exitCode = 1;
411
532
  };
412
533
 
413
534
  // src/dotfiles.ts
414
- import * as fs2 from "node:fs";
415
- import * as path2 from "node:path";
535
+ import * as fs3 from "node:fs";
536
+ import * as path3 from "node:path";
416
537
  var DOTFILE_CANDIDATES = [
417
538
  {
418
539
  group: "shell",
@@ -455,7 +576,7 @@ var DOTFILE_CANDIDATES = [
455
576
  var MAX_FILE_BYTES = 1024 * 1024;
456
577
  var statFile = (home, relative3) => {
457
578
  try {
458
- const stat = fs2.statSync(path2.join(home, relative3));
579
+ const stat = fs3.statSync(path3.join(home, relative3));
459
580
  return stat.isFile() ? stat : null;
460
581
  } catch {
461
582
  return null;
@@ -479,7 +600,7 @@ var readSyncFiles = (home, paths_) => {
479
600
  }
480
601
  files.push({
481
602
  path: relative3,
482
- contentsBase64: fs2.readFileSync(path2.join(home, relative3)).toString("base64"),
603
+ contentsBase64: fs3.readFileSync(path3.join(home, relative3)).toString("base64"),
483
604
  // eslint-disable-next-line no-bitwise -- permission bits are the point
484
605
  mode: (stat.mode & 511).toString(8)
485
606
  });
@@ -488,7 +609,7 @@ var readSyncFiles = (home, paths_) => {
488
609
  };
489
610
 
490
611
  // src/env.ts
491
- var formatLoadReport = (report, paint4) => {
612
+ var formatLoadReport = (report, paint5) => {
492
613
  const width = Math.max(
493
614
  0,
494
615
  ...report.loaded.map((entry) => entry.name.length),
@@ -497,12 +618,12 @@ var formatLoadReport = (report, paint4) => {
497
618
  const lines = [];
498
619
  for (const entry of report.loaded) {
499
620
  lines.push(
500
- ` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint4.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
621
+ ` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint5.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
501
622
  );
502
623
  }
503
624
  for (const entry of report.rejected) {
504
625
  lines.push(
505
- ` ${entry.name.padEnd(width)} ${paint4.warn("rejected")} ${paint4.dim(`\xB7 ${entry.reason}`)}`
626
+ ` ${entry.name.padEnd(width)} ${paint5.warn("rejected")} ${paint5.dim(`\xB7 ${entry.reason}`)}`
506
627
  );
507
628
  }
508
629
  return lines;
@@ -514,6 +635,7 @@ var SECTIONS = [
514
635
  "sessions",
515
636
  "services",
516
637
  "project setup",
638
+ "organization",
517
639
  "this machine"
518
640
  ];
519
641
  var project = (what = "the project") => ({
@@ -546,16 +668,21 @@ var COMMANDS = [
546
668
  name: "connect",
547
669
  section: "start",
548
670
  summary: "send this machine's provider credential to the platform",
549
- synopsis: ["<claude|codex|github> [--from-stdin] [--remove]"],
671
+ synopsis: ["<claude|codex|github> [--use-my-login] [--from-stdin] [--remove]"],
550
672
  description: [
551
- "Sessions run on the platform, so the platform needs your provider login. This reads the file the provider's own CLI wrote when you logged in (claude, codex) or asks gh for its token (github) and stores it under your own user. Nothing is shared with other users."
673
+ "Sessions run on the platform, so the platform needs your provider login. This reads the file the provider's own CLI wrote when you logged in (codex) or asks gh for its token (github) and stores it under your own user. Nothing is shared with other users.",
674
+ "Claude is different: Mend gets a login of its own. Claude rotates its refresh token, so two copies of one login fight and the one that refreshes second is signed out, and Mend refreshes on a schedule. So this logs in once against a directory Mend keeps, and your own Claude login stays as it is. Run it again whenever Mend says the grant expired."
552
675
  ],
553
676
  options: [
677
+ {
678
+ flag: "--use-my-login",
679
+ text: "claude: send the login this machine already uses; both sides then share one grant"
680
+ },
554
681
  { flag: "--from-stdin", text: "paste a credential instead of reading the provider's file" },
555
682
  { flag: "--remove", text: "disconnect the provider" }
556
683
  ],
557
684
  examples: [
558
- { command: "mend connect claude", text: "after `claude login` on this machine" },
685
+ { command: "mend connect claude", text: "a Claude login of Mend's own, through the browser" },
559
686
  { command: "mend connect github --remove", text: "" }
560
687
  ],
561
688
  see: ["accounts"]
@@ -564,14 +691,19 @@ var COMMANDS = [
564
691
  name: "adopt",
565
692
  section: "start",
566
693
  summary: "adopt a repository into the store",
567
- synopsis: ["[git-url] [--name <name>] [--auth ambient|mend-key|bridge]"],
694
+ synopsis: ["[git-url] [--name <name>] [--auth ambient|mend-key|bridge] [--private|--shared]"],
568
695
  description: [
569
696
  "Clones a network Git repository into Mend's store. Every session then gets its own worktree of it. With no argument, Mend uses the current checkout's origin URL. HTTP(S), SSH, git://, and SCP-style URLs work; local paths and file:// URLs do not.",
570
697
  "--auth says how the store fetches from the remote. Default: your mode from mend keys mode. mend-key signs with your Mend key on the server (see mend keys). bridge relays this machine's ssh-agent while a mend command runs here, so hardware keys stay on your desk. ambient uses the server's own credentials."
571
698
  ],
572
699
  options: [
573
700
  { flag: "--name <name>", text: "the project's name in Mend. Default: the repository's" },
574
- { flag: "--auth <mode>", text: "mend-key, bridge, or ambient. Default: mend keys mode" }
701
+ { flag: "--auth <mode>", text: "mend-key, bridge, or ambient. Default: mend keys mode" },
702
+ { flag: "--private", text: "only you see the project. Default" },
703
+ {
704
+ flag: "--shared",
705
+ text: "everyone in your organization sees it and can start sessions in it"
706
+ }
575
707
  ],
576
708
  examples: [
577
709
  { command: "mend adopt", text: "the current repository's origin URL" },
@@ -648,7 +780,9 @@ var COMMANDS = [
648
780
  summary: "the dashboard: every project and session, live",
649
781
  synopsis: [],
650
782
  description: [
651
- "A full-screen view of every project and session, updating live. Pick a session to attach, stop, or take over. Bare mend with no command opens the same thing.",
783
+ "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.",
784
+ "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.",
785
+ "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.",
652
786
  "The dashboard needs Node 26 or newer for its terminal. Every other command works on Node 22."
653
787
  ],
654
788
  see: ["sessions", "attach"]
@@ -1013,7 +1147,166 @@ var COMMANDS = [
1013
1147
  ],
1014
1148
  see: ["keys mode", "adopt"]
1015
1149
  },
1150
+ // ── organization ───────────────────────────────────────────────────────
1151
+ {
1152
+ name: "members",
1153
+ section: "organization",
1154
+ summary: "who belongs to your organization, and their roles",
1155
+ synopsis: [],
1156
+ description: [
1157
+ "Prints the organization's name and one row per member: name, email, role and the day they joined. The row marked with an arrow is you. Owners change roles and remove members in Settings on the web."
1158
+ ],
1159
+ see: ["invite"]
1160
+ },
1161
+ {
1162
+ name: "invite",
1163
+ section: "organization",
1164
+ summary: "print a one-time link that adds someone to your organization",
1165
+ synopsis: ["[--role member|owner] [--email <address>] [--days <n>]"],
1166
+ description: [
1167
+ "Owners only. Prints a link that works once. Whoever opens it creates an account and joins with the role you pick. Mend sends no email: you share the link yourself. Bind it to an email when it must not be forwarded."
1168
+ ],
1169
+ options: [
1170
+ { flag: "--role <role>", text: "member or owner. Default: member" },
1171
+ { flag: "--email <address>", text: "only an account with this email may accept it" },
1172
+ { flag: "--days <n>", text: "days until the link expires. Default: 7, at most 30" }
1173
+ ],
1174
+ examples: [{ command: "mend invite --email sam@acme.dev", text: "a member link for Sam only" }],
1175
+ see: ["members"]
1176
+ },
1177
+ {
1178
+ name: "folder list",
1179
+ section: "organization",
1180
+ summary: "list your organization's folders",
1181
+ synopsis: [],
1182
+ description: [
1183
+ "Folders are directories Mend keeps for the organization. A project mounts the ones it selects at /workspace/home/<name>, read-only unless chosen otherwise, for its next sessions."
1184
+ ],
1185
+ see: ["folder create", "folder push"]
1186
+ },
1187
+ {
1188
+ name: "folder create",
1189
+ section: "organization",
1190
+ summary: "create an organization folder",
1191
+ synopsis: ["<name>"],
1192
+ description: [
1193
+ "Owners only. Names use lowercase letters, digits, dots, underscores and dashes. A project picks the folder in its setup on the web."
1194
+ ],
1195
+ see: ["folder push", "folder list"]
1196
+ },
1197
+ {
1198
+ name: "folder push",
1199
+ section: "organization",
1200
+ summary: "upload a local directory into a folder",
1201
+ synopsis: ["<name> <dir> [--replace]"],
1202
+ description: [
1203
+ "Owners only. Sends every file under <dir>, keeping paths relative to it. .git and node_modules directories, symlinks and files over 1 MiB are skipped and counted. Without --replace the files are added beside what the folder holds."
1204
+ ],
1205
+ options: [{ flag: "--replace", text: "empty the folder first, so it holds exactly <dir>" }],
1206
+ examples: [{ command: "mend folder push fixtures ./test/fixtures --replace", text: "" }],
1207
+ see: ["folder create", "folder rm"]
1208
+ },
1209
+ {
1210
+ name: "folder rm",
1211
+ section: "organization",
1212
+ summary: "remove an organization folder",
1213
+ synopsis: ["<name>"],
1214
+ description: [
1215
+ "Owners only. A folder a project still mounts is refused: deselect it in that project's setup first."
1216
+ ],
1217
+ see: ["folder list"]
1218
+ },
1219
+ {
1220
+ name: "session share",
1221
+ section: "organization",
1222
+ summary: "let everyone who can see a session steer it, or stop",
1223
+ synopsis: ["<session> on|off"],
1224
+ description: [
1225
+ "The session's owner turns shared control on or off; an organization owner may turn it off. While it is on, anyone who can see the project sends turns, answers approvals, interrupts and types in the terminal, using your provider logins and Git access. Every act is recorded with who did it. A prefix of the session id is enough."
1226
+ ],
1227
+ examples: [{ command: "mend session share 3f2a on", text: "" }],
1228
+ see: ["sessions"]
1229
+ },
1016
1230
  // ── this machine ───────────────────────────────────────────────────────
1231
+ {
1232
+ name: "operator org list",
1233
+ section: "this machine",
1234
+ summary: "list organizations with their member and owner counts",
1235
+ synopsis: [],
1236
+ description: [
1237
+ "Operator only. One line per organization. An organization with no owner says so: bring one in with mend operator org invite-owner or mend operator grant-owner. The operator administers the instance and reads no organization content."
1238
+ ],
1239
+ see: ["operator org invite-owner", "operator grant-owner"]
1240
+ },
1241
+ {
1242
+ name: "operator exposure",
1243
+ section: "this machine",
1244
+ summary: "how this instance is exposed, as declared and as observed",
1245
+ synopsis: [],
1246
+ description: [
1247
+ "Operator only. States MEND_EXPOSURE as declared (loopback, private or public) and one line per item of the public exposure gate, with how it was established: observed (this server read it), carried (this build contains it, and the server cannot see it in effect), declared (you stated it and the server cannot check it), or open. MEND_EXPOSURE=public refuses to start while an item the server can observe is open. Items it cannot observe say what would verify them; once you have verified core-private or edge-tls from outside, name it in MEND_EXPOSURE_DECLARED. The report is what was observed; it is not a statement that the instance is fit to expose."
1248
+ ],
1249
+ see: ["operator gate", "doctor"]
1250
+ },
1251
+ {
1252
+ name: "operator gate",
1253
+ section: "this machine",
1254
+ summary: "what MEND_TENANCY=multi still needs on this instance",
1255
+ synopsis: [],
1256
+ description: [
1257
+ "Operator only. One line per item of the multi mode gate: what this instance shows, and what would satisfy an open item. Multi tenancy refuses to start while any item is open."
1258
+ ],
1259
+ see: ["operator org list"]
1260
+ },
1261
+ {
1262
+ name: "operator org create",
1263
+ section: "this machine",
1264
+ summary: "create an organization (multi tenancy only)",
1265
+ synopsis: ["<name>"],
1266
+ description: [
1267
+ "Operator only, and only when MEND_TENANCY=multi. The organization starts empty: invite its first owner next."
1268
+ ],
1269
+ see: ["operator org invite-owner"]
1270
+ },
1271
+ {
1272
+ name: "operator org rename",
1273
+ section: "this machine",
1274
+ summary: "rename an organization",
1275
+ synopsis: ["<org> <name>"],
1276
+ description: ["Operator only. Recorded in the organization's audit log."],
1277
+ see: ["operator org list"]
1278
+ },
1279
+ {
1280
+ name: "operator org invite-owner",
1281
+ section: "this machine",
1282
+ summary: "print a one-time owner invitation for an organization",
1283
+ synopsis: ["<org> [--email <address>]"],
1284
+ description: [
1285
+ "Operator only. Prints a link that works once and makes whoever opens it an owner of the organization. Recorded in its audit log."
1286
+ ],
1287
+ options: [{ flag: "--email <address>", text: "only an account with this email may accept it" }],
1288
+ see: ["operator grant-owner"]
1289
+ },
1290
+ {
1291
+ name: "operator grant-owner",
1292
+ section: "this machine",
1293
+ summary: "make an existing member an owner",
1294
+ synopsis: ["<org> <email>"],
1295
+ description: [
1296
+ "Operator only. For an organization whose owners are gone or locked out. The account must already be a member. Recorded in the organization's audit log."
1297
+ ],
1298
+ see: ["operator org invite-owner"]
1299
+ },
1300
+ {
1301
+ name: "operator reset-link",
1302
+ section: "this machine",
1303
+ summary: "print a one-time password reset link for an account",
1304
+ synopsis: ["<email>"],
1305
+ description: [
1306
+ "Operator only. Mend sends no email, so you hand the link over. It works once, expires after a day, and setting a password with it signs the account out everywhere. Owners reset their members' passwords from Settings."
1307
+ ],
1308
+ see: ["operator grant-owner"]
1309
+ },
1017
1310
  {
1018
1311
  name: "server",
1019
1312
  section: "this machine",
@@ -1079,7 +1372,7 @@ var COMMANDS = [
1079
1372
  { command: "mend server setup", text: "localhost, using the current local Docker context" },
1080
1373
  {
1081
1374
  command: "mend server setup --context orbstack --bind 0.0.0.0 --url http://100.70.80.90:3105",
1082
- text: "explicit tailnet exposure"
1375
+ text: "explicit private-network exposure (a tailnet, a LAN, a VPN)"
1083
1376
  },
1084
1377
  {
1085
1378
  command: "mend server setup --version 0.25.0 --assets-dir ./release-assets --offline",
@@ -1516,7 +1809,7 @@ var manFileName = (doc) => doc === null ? "mend.1" : `mend-${doc.name.replace(/
1516
1809
 
1517
1810
  // src/login.ts
1518
1811
  import { spawn } from "node:child_process";
1519
- import * as os from "node:os";
1812
+ import * as os2 from "node:os";
1520
1813
 
1521
1814
  // src/pair.ts
1522
1815
  var paint = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
@@ -1708,7 +2001,7 @@ var resolveServerUrl = async (args, deps) => {
1708
2001
  };
1709
2002
  var loginCommand = async (args, deps) => {
1710
2003
  const base = await resolveServerUrl(args, deps);
1711
- const started = await post(`${base}/api/cli/auth`, { name: os.hostname() });
2004
+ const started = await post(`${base}/api/cli/auth`, { name: os2.hostname() });
1712
2005
  if (started.status === 429) {
1713
2006
  return fail2(
1714
2007
  `the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
@@ -1758,15 +2051,338 @@ var loginCommand = async (args, deps) => {
1758
2051
  return fail2("the authorize request expired before anyone approved \u2014 run mend login again");
1759
2052
  };
1760
2053
 
2054
+ // src/organization.ts
2055
+ import * as fs4 from "node:fs";
2056
+ import * as path4 from "node:path";
2057
+ var paint3 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
2058
+ var dim3 = paint3("2");
2059
+ var green3 = paint3("32");
2060
+ var say3 = (line) => process.stdout.write(`${line}
2061
+ `);
2062
+ var fail3 = (message) => {
2063
+ process.stderr.write(`mend: ${message}
2064
+ `);
2065
+ process.exit(1);
2066
+ };
2067
+ var takeFlagValue3 = (args, flag) => {
2068
+ const at = args.indexOf(flag);
2069
+ return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
2070
+ };
2071
+ var day2 = (iso) => iso.slice(0, 10);
2072
+ var organizationOf = (api2) => api2("GET", "/organization");
2073
+ var renderMembers = (view, members) => {
2074
+ const nameWidth = Math.max(4, ...members.map((member) => member.name.length));
2075
+ const emailWidth = Math.max(5, ...members.map((member) => member.email.length));
2076
+ return [
2077
+ `${view.organization.name} \xB7 ${members.length} ${members.length === 1 ? "member" : "members"}`,
2078
+ ...members.map(
2079
+ (member) => `${member.userId === view.userId ? "\u25B8" : " "} ${member.name.padEnd(nameWidth)} ${member.email.padEnd(emailWidth)} ${member.role.padEnd(6)} joined ${day2(member.joinedAt)}`
2080
+ )
2081
+ ];
2082
+ };
2083
+ var membersCommand = async (api2) => {
2084
+ const view = await organizationOf(api2);
2085
+ const members = await api2("GET", "/organization/members");
2086
+ for (const line of renderMembers(view, members)) say3(line);
2087
+ };
2088
+ var inviteCommand = async (api2, baseUrl, args) => {
2089
+ const role = takeFlagValue3(args, "--role") ?? "member";
2090
+ if (role !== "member" && role !== "owner") {
2091
+ return fail3(`--role takes "member" or "owner", not "${role}"`);
2092
+ }
2093
+ const email = takeFlagValue3(args, "--email");
2094
+ const daysValue = takeFlagValue3(args, "--days");
2095
+ const days = daysValue === null ? null : Number.parseInt(daysValue, 10);
2096
+ if (days !== null && (Number.isNaN(days) || days < 1)) {
2097
+ return fail3(`--days takes a whole number of days, not "${daysValue}"`);
2098
+ }
2099
+ const view = await organizationOf(api2);
2100
+ if (view.role !== "owner") return fail3("only an owner can invite; mend members shows who");
2101
+ const created = await api2("POST", "/organization/invitations", {
2102
+ role,
2103
+ ...email === null ? {} : { email },
2104
+ ...days === null ? {} : { expiresInDays: days }
2105
+ });
2106
+ say3(`${baseUrl.replace(/\/+$/, "")}${created.path}`);
2107
+ say3(
2108
+ dim3(
2109
+ `works once \xB7 ${role}${email === null ? "" : ` \xB7 for ${email}`} \xB7 expires ${day2(created.invitation.expiresAt)}`
2110
+ )
2111
+ );
2112
+ };
2113
+ var FOLDER_FILE_BYTES = 1024 * 1024;
2114
+ var FOLDER_REQUEST_BYTES = 4 * 1024 * 1024;
2115
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
2116
+ var scanFolder = (dir) => {
2117
+ const files = [];
2118
+ const skipped = [];
2119
+ const walk = (relative3) => {
2120
+ const entries = fs4.readdirSync(path4.join(dir, relative3), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
2121
+ for (const entry of entries) {
2122
+ const child = relative3 === "" ? entry.name : `${relative3}/${entry.name}`;
2123
+ if (entry.isSymbolicLink()) {
2124
+ skipped.push({ path: child, reason: "symlink" });
2125
+ } else if (entry.isDirectory()) {
2126
+ if (SKIPPED_DIRECTORIES.has(entry.name)) skipped.push({ path: child, reason: "skipped" });
2127
+ else walk(child);
2128
+ } else if (entry.isFile()) {
2129
+ const size = fs4.statSync(path4.join(dir, child)).size;
2130
+ if (size > FOLDER_FILE_BYTES) skipped.push({ path: child, reason: "over 1 MiB" });
2131
+ else files.push({ path: child, size });
2132
+ }
2133
+ }
2134
+ };
2135
+ walk("");
2136
+ return { files, skipped };
2137
+ };
2138
+ var uploadBatches = (files, cap = FOLDER_REQUEST_BYTES) => {
2139
+ const batches = [];
2140
+ let current = [];
2141
+ let size = 0;
2142
+ for (const file of files) {
2143
+ if (current.length > 0 && size + file.size > cap) {
2144
+ batches.push(current);
2145
+ current = [];
2146
+ size = 0;
2147
+ }
2148
+ current.push(file);
2149
+ size += file.size;
2150
+ }
2151
+ if (current.length > 0) batches.push(current);
2152
+ return batches;
2153
+ };
2154
+ var folderNamed = async (api2, name) => {
2155
+ if (name === void 0) return fail3("name the folder \xB7 mend folder list shows them");
2156
+ const folders = await api2("GET", "/organization/folders");
2157
+ const folder = folders.find((candidate) => candidate.name === name);
2158
+ return folder ?? fail3(`no folder named "${name}" \xB7 mend folder list shows them`);
2159
+ };
2160
+ var folderCommand = async (api2, args) => {
2161
+ const [verb, name, dir] = args.filter((arg) => !arg.startsWith("--"));
2162
+ switch (verb) {
2163
+ case void 0:
2164
+ case "list": {
2165
+ const folders = await api2("GET", "/organization/folders");
2166
+ if (folders.length === 0) {
2167
+ say3(dim3("no folders yet \xB7 an owner creates one with mend folder create <name>"));
2168
+ return;
2169
+ }
2170
+ for (const folder of folders)
2171
+ say3(`${folder.name} ${dim3(`created ${day2(folder.createdAt)}`)}`);
2172
+ return;
2173
+ }
2174
+ case "create": {
2175
+ if (name === void 0) return fail3("usage: mend folder create <name>");
2176
+ const folder = await api2("POST", "/organization/folders", { name });
2177
+ say3(`${green3("\u2713")} created folder ${folder.name}`);
2178
+ return;
2179
+ }
2180
+ case "push": {
2181
+ if (dir === void 0) return fail3("usage: mend folder push <name> <dir> [--replace]");
2182
+ if (!fs4.existsSync(dir) || !fs4.statSync(dir).isDirectory()) {
2183
+ return fail3(`${dir} is not a directory`);
2184
+ }
2185
+ const folder = await folderNamed(api2, name);
2186
+ const scanned = scanFolder(dir);
2187
+ const replace = args.includes("--replace");
2188
+ const batches = uploadBatches(scanned.files);
2189
+ if (batches.length === 0 && replace) {
2190
+ await api2("POST", `/organization/folders/${folder.id}/files`, { files: [], merge: false });
2191
+ }
2192
+ for (const [index, batch] of batches.entries()) {
2193
+ await api2("POST", `/organization/folders/${folder.id}/files`, {
2194
+ files: batch.map((file) => ({
2195
+ path: file.path,
2196
+ // Read one batch at a time, so a large tree never sits in memory at once.
2197
+ contentsBase64: fs4.readFileSync(path4.join(dir, file.path)).toString("base64")
2198
+ })),
2199
+ // --replace empties the folder with the first batch; the rest add to it.
2200
+ merge: !(replace && index === 0)
2201
+ });
2202
+ }
2203
+ const reasons = [...new Set(scanned.skipped.map((entry) => entry.reason))];
2204
+ say3(
2205
+ `${green3("\u2713")} pushed ${scanned.files.length} ${scanned.files.length === 1 ? "file" : "files"} to ${folder.name}${scanned.skipped.length === 0 ? "" : dim3(` \xB7 ${scanned.skipped.length} skipped (${reasons.join(", ")})`)}`
2206
+ );
2207
+ return;
2208
+ }
2209
+ case "rm": {
2210
+ const folder = await folderNamed(api2, name);
2211
+ await api2("DELETE", `/organization/folders/${folder.id}`);
2212
+ say3(`${green3("\u2713")} removed folder ${folder.name}`);
2213
+ return;
2214
+ }
2215
+ default:
2216
+ return fail3(`unknown folder command "${verb}" \xB7 mend help folder`);
2217
+ }
2218
+ };
2219
+ var sessionShareCommand = async (api2, args) => {
2220
+ const [prefix, state] = args.filter((arg) => !arg.startsWith("--"));
2221
+ if (prefix === void 0 || state !== "on" && state !== "off") {
2222
+ return fail3("usage: mend session share <session> on|off");
2223
+ }
2224
+ const sessions = await api2(
2225
+ "GET",
2226
+ "/sessions?retained=1"
2227
+ );
2228
+ const matches = sessions.filter((session2) => session2.id.startsWith(prefix));
2229
+ if (matches.length === 0) return fail3(`no live session starts with "${prefix}"`);
2230
+ if (matches.length > 1)
2231
+ return fail3(`"${prefix}" matches ${matches.length} sessions; type more of the id`);
2232
+ const [session] = matches;
2233
+ if (session === void 0) return fail3(`no live session starts with "${prefix}"`);
2234
+ await api2("PUT", `/sessions/${session.id}/shared-control`, { enabled: state === "on" });
2235
+ say3(
2236
+ state === "on" ? `${green3("\u2713")} shared control on \xB7 ${session.harness} ${dim3(session.id.slice(0, 8))} \xB7 everyone who can see the project steers it on your credentials` : `${green3("\u2713")} shared control off \xB7 ${session.harness} ${dim3(session.id.slice(0, 8))}`
2237
+ );
2238
+ };
2239
+ var renderGate = (items) => {
2240
+ const width = Math.max(...items.map((item) => item.id.length));
2241
+ const open = items.filter((item) => !item.ok).length;
2242
+ return [
2243
+ ...items.map(
2244
+ (item) => `${item.ok ? green3("\u2713") : "\xB7"} ${item.id.padEnd(width)} ${item.detail}${item.fix === null ? "" : dim3(` \xB7 ${item.fix}`)}`
2245
+ ),
2246
+ open === 0 ? "every item is in place; MEND_TENANCY=multi may start" : `${open} of ${items.length} items open; MEND_TENANCY=multi refuses to start`
2247
+ ];
2248
+ };
2249
+ var EXPOSURE_WORDS = {
2250
+ loopback: "reached from this machine only",
2251
+ private: "reached over a network you control admission to",
2252
+ public: "reachable from the Internet"
2253
+ };
2254
+ var renderExposure = (report) => {
2255
+ const width = Math.max(...report.items.map((item) => item.id.length));
2256
+ const marks = {
2257
+ observed: "\u25CF",
2258
+ carried: "\u25D0",
2259
+ declared: "\u25CB",
2260
+ open: "\xB7"
2261
+ };
2262
+ const mark = (item) => marks[item.established];
2263
+ const open = report.items.filter((item) => item.established === "open");
2264
+ const blocking = open.filter((item) => item.blocksStart).length;
2265
+ const unobservable = open.length - blocking;
2266
+ return [
2267
+ `exposure \xB7 declared ${report.declared} \xB7 ${EXPOSURE_WORDS[report.declared]}`,
2268
+ ...report.items.map(
2269
+ (item) => `${mark(item)} ${item.id.padEnd(width)} ${item.established.padEnd(8)} ${item.detail}${item.fix === null ? "" : dim3(` \xB7 ${item.fix}`)}`
2270
+ ),
2271
+ open.length === 0 ? "nothing open \xB7 every item was observed here, is carried by this build, or was declared by the operator" : `${open.length} of ${report.items.length} items open \xB7 ${blocking} this build can observe${blocking === 0 ? "" : "; MEND_EXPOSURE=public refuses to start"} \xB7 ${unobservable} it cannot`
2272
+ ];
2273
+ };
2274
+ var renderOrganizations = (rows) => {
2275
+ if (rows.length === 0) return ["no organizations"];
2276
+ const width = Math.max(...rows.map((row) => row.organization.name.length));
2277
+ return rows.map(
2278
+ (row) => `${row.organization.name.padEnd(width)} ${row.memberCount} ${row.memberCount === 1 ? "member" : "members"} \xB7 ${row.ownerCount === 0 ? "no owner" : `${row.ownerCount} ${row.ownerCount === 1 ? "owner" : "owners"}`}`
2279
+ );
2280
+ };
2281
+ var printLink = (baseUrl, link, what) => {
2282
+ say3(`${baseUrl.replace(/\/+$/, "")}${link.path}`);
2283
+ say3(dim3(`${what} \xB7 works once \xB7 expires ${day2(link.expiresAt)}`));
2284
+ };
2285
+ var operatorCommand = async (api2, tryApi, baseUrl, args) => {
2286
+ const words = args.filter((arg, index) => !arg.startsWith("--") && args[index - 1] !== "--email");
2287
+ const refused = await tryApi("GET", "/operator/organizations").then(
2288
+ () => null,
2289
+ (error) => error instanceof Error ? error.message : String(error)
2290
+ );
2291
+ if (refused !== null) {
2292
+ return fail3(
2293
+ refused.endsWith("\u2192 404") ? "this account is not the operator of this Mend" : refused
2294
+ );
2295
+ }
2296
+ const organizationNamed = async (name) => {
2297
+ if (name === void 0)
2298
+ return fail3("name the organization \xB7 mend operator org list shows them");
2299
+ const rows = await api2("GET", "/operator/organizations");
2300
+ const row = rows.find((candidate) => candidate.organization.name === name);
2301
+ return row?.organization ?? fail3(`no organization named "${name}"`);
2302
+ };
2303
+ const [first, second, third, fourth] = words;
2304
+ if (first === "org") {
2305
+ switch (second) {
2306
+ case void 0:
2307
+ case "list": {
2308
+ const rows = await api2(
2309
+ "GET",
2310
+ "/operator/organizations"
2311
+ );
2312
+ for (const line of renderOrganizations(rows)) say3(line);
2313
+ return;
2314
+ }
2315
+ case "create": {
2316
+ if (third === void 0) return fail3("usage: mend operator org create <name>");
2317
+ const created = await api2("POST", "/operator/organizations", {
2318
+ name: third
2319
+ });
2320
+ say3(
2321
+ `${green3("\u2713")} created ${created.name} \xB7 mend operator org invite-owner ${created.name}`
2322
+ );
2323
+ return;
2324
+ }
2325
+ case "rename": {
2326
+ if (fourth === void 0) return fail3("usage: mend operator org rename <org> <name>");
2327
+ const organization = await organizationNamed(third);
2328
+ await api2("PUT", `/operator/organizations/${organization.id}/name`, { name: fourth });
2329
+ say3(`${green3("\u2713")} renamed ${organization.name} to ${fourth}`);
2330
+ return;
2331
+ }
2332
+ case "invite-owner": {
2333
+ const organization = await organizationNamed(third);
2334
+ const email = takeFlagValue3(args, "--email");
2335
+ const link = await api2(
2336
+ "POST",
2337
+ `/operator/organizations/${organization.id}/invitations`,
2338
+ email === null ? {} : { email }
2339
+ );
2340
+ printLink(
2341
+ baseUrl,
2342
+ link,
2343
+ `owner of ${organization.name}${email === null ? "" : ` \xB7 for ${email}`}`
2344
+ );
2345
+ return;
2346
+ }
2347
+ default:
2348
+ return fail3(`unknown operator org command "${second}" \xB7 mend help operator org list`);
2349
+ }
2350
+ }
2351
+ if (first === "exposure") {
2352
+ const report = await api2("GET", "/operator/exposure");
2353
+ for (const line of renderExposure(report)) say3(line);
2354
+ return;
2355
+ }
2356
+ if (first === "gate") {
2357
+ const items = await api2("GET", "/operator/gate");
2358
+ for (const line of renderGate(items)) say3(line);
2359
+ return;
2360
+ }
2361
+ if (first === "grant-owner") {
2362
+ if (third === void 0) return fail3("usage: mend operator grant-owner <org> <email>");
2363
+ const organization = await organizationNamed(second);
2364
+ await api2("POST", `/operator/organizations/${organization.id}/owners`, { email: third });
2365
+ say3(`${green3("\u2713")} ${third} is an owner of ${organization.name}`);
2366
+ return;
2367
+ }
2368
+ if (first === "reset-link") {
2369
+ if (second === void 0) return fail3("usage: mend operator reset-link <email>");
2370
+ const link = await api2("POST", "/operator/password-resets", { email: second });
2371
+ printLink(baseUrl, link, `password reset for ${second}`);
2372
+ return;
2373
+ }
2374
+ return fail3(`unknown operator command "${first ?? ""}" \xB7 mend help operator org list`);
2375
+ };
2376
+
1761
2377
  // src/server-setup.ts
1762
2378
  import { createHmac, randomBytes } from "node:crypto";
1763
2379
  import { readFile } from "node:fs/promises";
1764
2380
  import * as net2 from "node:net";
1765
- import * as os3 from "node:os";
1766
- import * as path5 from "node:path";
2381
+ import * as os4 from "node:os";
2382
+ import * as path7 from "node:path";
1767
2383
 
1768
2384
  // src/server-docker-volumes.ts
1769
- import { createHash } from "node:crypto";
2385
+ import { createHash as createHash2 } from "node:crypto";
1770
2386
  var MEND_DOCKER_NAMESPACE = {
1771
2387
  project: "mend",
1772
2388
  store: "mend-store",
@@ -1794,15 +2410,15 @@ var ServerVolumeOwnershipError = class extends Error {
1794
2410
  this.operation = operation;
1795
2411
  }
1796
2412
  };
1797
- var fail3 = (reason, operation) => ({ _tag: "error", error: new ServerVolumeOwnershipError(reason, operation) });
2413
+ var fail4 = (reason, operation) => ({ _tag: "error", error: new ServerVolumeOwnershipError(reason, operation) });
1798
2414
  var docker = async (runtime, context, args) => {
1799
2415
  try {
1800
2416
  const output = await runtime.run("docker", ["--context", context, ...args]);
1801
2417
  if (output.status !== 0 || output.error !== void 0)
1802
- return fail3("docker", args.slice(0, 2).join(" "));
2418
+ return fail4("docker", args.slice(0, 2).join(" "));
1803
2419
  return { _tag: "ok", value: output.stdout };
1804
2420
  } catch {
1805
- return fail3("docker", args.slice(0, 2).join(" "));
2421
+ return fail4("docker", args.slice(0, 2).join(" "));
1806
2422
  }
1807
2423
  };
1808
2424
  var listNames = async (runtime, context, kind, project2) => {
@@ -1821,11 +2437,11 @@ var listNames = async (runtime, context, kind, project2) => {
1821
2437
  for (const line of output.value.trim().split("\n").filter(Boolean)) {
1822
2438
  const value = JSON.parse(line);
1823
2439
  if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value))
1824
- return fail3("inspection", `${kind} list`);
2440
+ return fail4("inspection", `${kind} list`);
1825
2441
  names.push(value);
1826
2442
  }
1827
2443
  } catch {
1828
- return fail3("inspection", `${kind} list`);
2444
+ return fail4("inspection", `${kind} list`);
1829
2445
  }
1830
2446
  return { _tag: "ok", value: names };
1831
2447
  };
@@ -1841,16 +2457,16 @@ var inspectOwner = async (runtime, context, name, owner) => {
1841
2457
  try {
1842
2458
  const volume = JSON.parse(output.value);
1843
2459
  if (typeof volume !== "object" || volume === null || !("Name" in volume) || volume.Name !== name || !("Labels" in volume))
1844
- return fail3("inspection", "volume inspect");
2460
+ return fail4("inspection", "volume inspect");
1845
2461
  const labels = volume.Labels;
1846
- if (labels === null) return fail3("conflict", "volume owner");
2462
+ if (labels === null) return fail4("conflict", "volume owner");
1847
2463
  if (typeof labels !== "object" || Array.isArray(labels) || !Object.values(labels).every((v) => typeof v === "string"))
1848
- return fail3("inspection", "volume labels");
2464
+ return fail4("inspection", "volume labels");
1849
2465
  if (!(SERVER_VOLUME_OWNER_LABEL in labels) || labels[SERVER_VOLUME_OWNER_LABEL] !== owner)
1850
- return fail3("conflict", "volume owner");
2466
+ return fail4("conflict", "volume owner");
1851
2467
  return { _tag: "ok", value: void 0 };
1852
2468
  } catch {
1853
- return fail3("inspection", "volume inspect");
2469
+ return fail4("inspection", "volume inspect");
1854
2470
  }
1855
2471
  };
1856
2472
  var ensureVolume = async (runtime, context, name, owner) => {
@@ -1889,38 +2505,38 @@ var inspectForeignProject = async (runtime, context, kind, name, project2) => {
1889
2505
  try {
1890
2506
  const resource = JSON.parse(output.value);
1891
2507
  if (typeof resource !== "object" || resource === null || !("Name" in resource) || resource.Name !== (kind === "container" ? `/${name}` : name) || !("Labels" in resource))
1892
- return fail3("inspection", `${kind} inspect`);
2508
+ return fail4("inspection", `${kind} inspect`);
1893
2509
  const labels = resource.Labels;
1894
- if (labels === null) return fail3("unowned-data", `existing ${kind} without anchor`);
2510
+ if (labels === null) return fail4("unowned-data", `existing ${kind} without anchor`);
1895
2511
  if (typeof labels !== "object" || Array.isArray(labels) || !Object.values(labels).every((value) => typeof value === "string"))
1896
- return fail3("inspection", `${kind} labels`);
2512
+ return fail4("inspection", `${kind} labels`);
1897
2513
  if (!("com.docker.compose.project" in labels))
1898
- return fail3("unowned-data", `existing ${kind} without anchor`);
2514
+ return fail4("unowned-data", `existing ${kind} without anchor`);
1899
2515
  const foreignProject = labels["com.docker.compose.project"];
1900
2516
  if (typeof foreignProject !== "string" || !/^[a-z0-9][a-z0-9_-]*$/.test(foreignProject))
1901
- return fail3("inspection", `${kind} project label`);
2517
+ return fail4("inspection", `${kind} project label`);
1902
2518
  if (foreignProject === project2 || !inProjectNamespace(name, foreignProject))
1903
- return fail3("unowned-data", `existing ${kind} without anchor`);
2519
+ return fail4("unowned-data", `existing ${kind} without anchor`);
1904
2520
  return { _tag: "ok", value: void 0 };
1905
2521
  } catch {
1906
- return fail3("inspection", `${kind} inspect`);
2522
+ return fail4("inspection", `${kind} inspect`);
1907
2523
  }
1908
2524
  };
1909
2525
  var refuseOldData = async (runtime, context, namespace, volumes) => {
1910
2526
  if (volumes.some(
1911
2527
  (name) => secondaryVolumesOf(namespace).includes(name) || name.startsWith(`${namespace.project}_`)
1912
2528
  ))
1913
- return fail3("unowned-data", "existing volumes without anchor");
2529
+ return fail4("unowned-data", "existing volumes without anchor");
1914
2530
  for (const kind of ["volume", "container", "network"]) {
1915
2531
  const labelled = await listNames(runtime, context, kind, namespace.project);
1916
2532
  if (labelled._tag === "error") return labelled;
1917
- if (labelled.value.length > 0) return fail3("unowned-data", `existing ${kind} without anchor`);
2533
+ if (labelled.value.length > 0) return fail4("unowned-data", `existing ${kind} without anchor`);
1918
2534
  if (kind !== "volume") {
1919
2535
  const named = await listNames(runtime, context, kind);
1920
2536
  if (named._tag === "error") return named;
1921
2537
  for (const name of named.value) {
1922
2538
  if (reservedResourceName(name, kind, namespace))
1923
- return fail3("unowned-data", `existing ${kind} without anchor`);
2539
+ return fail4("unowned-data", `existing ${kind} without anchor`);
1924
2540
  if (name.startsWith(`${namespace.project}_`) || name.startsWith(`${namespace.project}-`)) {
1925
2541
  const foreign = await inspectForeignProject(
1926
2542
  runtime,
@@ -1941,8 +2557,8 @@ var parseOwnershipInput = (input) => {
1941
2557
  if (!input.dockerContext.trim() || input.identityBytes.byteLength === 0 || !/^[a-z0-9][a-z0-9_-]*$/.test(namespace.project) || ![namespace.store, ...secondaryVolumesOf(namespace)].every(
1942
2558
  (name) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/.test(name)
1943
2559
  ) || (/* @__PURE__ */ new Set([namespace.store, ...secondaryVolumesOf(namespace)])).size !== 1 + secondaryVolumesOf(namespace).length)
1944
- return fail3("invalid-input", "ownership inputs");
1945
- const owner = createHash("sha256").update(input.identityBytes).digest("hex");
2560
+ return fail4("invalid-input", "ownership inputs");
2561
+ const owner = createHash2("sha256").update(input.identityBytes).digest("hex");
1946
2562
  return { _tag: "ok", value: { owner, namespace } };
1947
2563
  };
1948
2564
  var ownership = async (runtime, input, mode) => {
@@ -1955,7 +2571,7 @@ var ownership = async (runtime, input, mode) => {
1955
2571
  const anchor = await inspectOwner(runtime, input.dockerContext, namespace.store, owner);
1956
2572
  if (anchor._tag === "error") return anchor;
1957
2573
  } else {
1958
- if (mode === "verify") return fail3("missing", "anchor");
2574
+ if (mode === "verify") return fail4("missing", "anchor");
1959
2575
  const empty = await refuseOldData(runtime, input.dockerContext, namespace, volumes.value);
1960
2576
  if (empty._tag === "error") return empty;
1961
2577
  const anchor = await ensureVolume(runtime, input.dockerContext, namespace.store, owner);
@@ -1969,7 +2585,7 @@ var ownership = async (runtime, input, mode) => {
1969
2585
  const owned = await inspectOwner(runtime, input.dockerContext, name, owner);
1970
2586
  if (owned._tag === "error") return owned;
1971
2587
  } else {
1972
- if (mode === "verify") return fail3("missing", role);
2588
+ if (mode === "verify") return fail4("missing", role);
1973
2589
  const created = await ensureVolume(runtime, input.dockerContext, name, owner);
1974
2590
  if (created._tag === "error") return created;
1975
2591
  }
@@ -1981,8 +2597,8 @@ var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "v
1981
2597
 
1982
2598
  // src/server-runtime.ts
1983
2599
  import { spawn as spawn2 } from "node:child_process";
1984
- import * as fs3 from "node:fs";
1985
- import * as path3 from "node:path";
2600
+ import * as fs5 from "node:fs";
2601
+ import * as path5 from "node:path";
1986
2602
  var serverProcessDeadlines = {
1987
2603
  ordinary: 3e4,
1988
2604
  pull: 10 * 6e4,
@@ -2028,7 +2644,7 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
2028
2644
  return new Promise((resolve4) => {
2029
2645
  let fd;
2030
2646
  try {
2031
- if (options.stdoutFile !== void 0) fd = fs3.openSync(options.stdoutFile, "wx", 384);
2647
+ if (options.stdoutFile !== void 0) fd = fs5.openSync(options.stdoutFile, "wx", 384);
2032
2648
  const child = spawn2(command, [...args], {
2033
2649
  env,
2034
2650
  detached: process.platform !== "win32",
@@ -2079,12 +2695,12 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
2079
2695
  child.once("close", (status) => {
2080
2696
  globalThis.clearTimeout(timer);
2081
2697
  try {
2082
- if (fd !== void 0) fs3.fsyncSync(fd);
2698
+ if (fd !== void 0) fs5.fsyncSync(fd);
2083
2699
  } catch {
2084
2700
  failure2 ??= "Could not durably write process output.";
2085
2701
  } finally {
2086
2702
  try {
2087
- if (fd !== void 0) fs3.closeSync(fd);
2703
+ if (fd !== void 0) fs5.closeSync(fd);
2088
2704
  } catch {
2089
2705
  failure2 ??= "Could not close process output file.";
2090
2706
  }
@@ -2098,7 +2714,7 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
2098
2714
  });
2099
2715
  } catch (cause) {
2100
2716
  try {
2101
- if (fd !== void 0) fs3.closeSync(fd);
2717
+ if (fd !== void 0) fs5.closeSync(fd);
2102
2718
  } catch {
2103
2719
  }
2104
2720
  resolve4({
@@ -2119,17 +2735,17 @@ var serverComposeArgs = (installation, command) => [
2119
2735
  "--project-directory",
2120
2736
  installation.directory,
2121
2737
  "--env-file",
2122
- path3.join(installation.directory, "server.env"),
2738
+ path5.join(installation.directory, "server.env"),
2123
2739
  "-f",
2124
- path3.join(installation.directory, "compose.yaml"),
2740
+ path5.join(installation.directory, "compose.yaml"),
2125
2741
  ...command
2126
2742
  ];
2127
2743
 
2128
2744
  // src/server-store.ts
2129
2745
  import { randomUUID } from "node:crypto";
2130
- import * as fs4 from "node:fs";
2131
- import * as os2 from "node:os";
2132
- import * as path4 from "node:path";
2746
+ import * as fs6 from "node:fs";
2747
+ import * as os3 from "node:os";
2748
+ import * as path6 from "node:path";
2133
2749
  var ServerStoreError = class extends Error {
2134
2750
  _tag = "ServerStoreError";
2135
2751
  };
@@ -2150,28 +2766,28 @@ var attempt = (operation) => {
2150
2766
  var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
2151
2767
  var readOptional = (file) => {
2152
2768
  try {
2153
- return fs4.readFileSync(file, "utf8");
2769
+ return fs6.readFileSync(file, "utf8");
2154
2770
  } catch (cause) {
2155
2771
  if (hasCode(cause, "ENOENT")) return null;
2156
2772
  throw cause;
2157
2773
  }
2158
2774
  };
2159
2775
  var syncDirectory = (directory) => {
2160
- const fd = fs4.openSync(directory, "r");
2776
+ const fd = fs6.openSync(directory, "r");
2161
2777
  try {
2162
- fs4.fsyncSync(fd);
2778
+ fs6.fsyncSync(fd);
2163
2779
  } finally {
2164
- fs4.closeSync(fd);
2780
+ fs6.closeSync(fd);
2165
2781
  }
2166
2782
  };
2167
2783
  var writeDurable = (file, content, mode = 384) => {
2168
- const fd = fs4.openSync(file, "wx", mode);
2784
+ const fd = fs6.openSync(file, "wx", mode);
2169
2785
  try {
2170
- fs4.writeFileSync(fd, content, "utf8");
2171
- fs4.fchmodSync(fd, mode);
2172
- fs4.fsyncSync(fd);
2786
+ fs6.writeFileSync(fd, content, "utf8");
2787
+ fs6.fchmodSync(fd, mode);
2788
+ fs6.fsyncSync(fd);
2173
2789
  } finally {
2174
- fs4.closeSync(fd);
2790
+ fs6.closeSync(fd);
2175
2791
  }
2176
2792
  };
2177
2793
  var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
@@ -2191,11 +2807,11 @@ var parseLockOwner = (raw) => {
2191
2807
  };
2192
2808
  var describeOwner = (lockDir) => {
2193
2809
  try {
2194
- const raw = JSON.parse(fs4.readFileSync(path4.join(lockDir, "owner.json"), "utf8"));
2810
+ const raw = JSON.parse(fs6.readFileSync(path6.join(lockDir, "owner.json"), "utf8"));
2195
2811
  const owner = parseLockOwner(raw);
2196
2812
  if (owner === null) return "owner metadata is invalid";
2197
2813
  const label = `PID ${owner.pid} on ${owner.hostname}`;
2198
- if (owner.hostname !== os2.hostname()) return label;
2814
+ if (owner.hostname !== os3.hostname()) return label;
2199
2815
  try {
2200
2816
  process.kill(owner.pid, 0);
2201
2817
  return `${label} is still live`;
@@ -2211,28 +2827,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
2211
2827
  );
2212
2828
  var acquireLock = (configDir, create) => {
2213
2829
  if (create) {
2214
- fs4.mkdirSync(configDir, { recursive: true, mode: 448 });
2215
- fs4.chmodSync(configDir, 448);
2216
- } else if (!fs4.existsSync(configDir)) {
2830
+ fs6.mkdirSync(configDir, { recursive: true, mode: 448 });
2831
+ fs6.chmodSync(configDir, 448);
2832
+ } else if (!fs6.existsSync(configDir)) {
2217
2833
  throw new ServerStoreError(
2218
2834
  "No Mend server is configured. Run mend server setup explicitly to install one."
2219
2835
  );
2220
2836
  }
2221
- const lockDir = path4.join(configDir, "server.lock");
2837
+ const lockDir = path6.join(configDir, "server.lock");
2222
2838
  try {
2223
- fs4.mkdirSync(lockDir, { mode: 448 });
2839
+ fs6.mkdirSync(lockDir, { mode: 448 });
2224
2840
  } catch (cause) {
2225
2841
  if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
2226
2842
  throw cause;
2227
2843
  }
2228
- const owned = fs4.statSync(lockDir);
2229
- const ownerFile = path4.join(lockDir, "owner.json");
2230
- const owner = `${JSON.stringify({ pid: process.pid, hostname: os2.hostname(), token: randomUUID() })}
2844
+ const owned = fs6.statSync(lockDir);
2845
+ const ownerFile = path6.join(lockDir, "owner.json");
2846
+ const owner = `${JSON.stringify({ pid: process.pid, hostname: os3.hostname(), token: randomUUID() })}
2231
2847
  `;
2232
2848
  let open = true;
2233
2849
  const assertOwned = () => {
2234
- const current = fs4.statSync(lockDir);
2235
- if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs4.readFileSync(ownerFile, "utf8") !== owner) {
2850
+ const current = fs6.statSync(lockDir);
2851
+ if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs6.readFileSync(ownerFile, "utf8") !== owner) {
2236
2852
  throw new ServerStoreError(
2237
2853
  "Server lock ownership was lost. Stop and check for another server command before retrying."
2238
2854
  );
@@ -2248,8 +2864,8 @@ var acquireLock = (configDir, create) => {
2248
2864
  release: () => {
2249
2865
  const result = attempt(() => {
2250
2866
  assertOwned();
2251
- fs4.unlinkSync(ownerFile);
2252
- fs4.rmdirSync(lockDir);
2867
+ fs6.unlinkSync(ownerFile);
2868
+ fs6.rmdirSync(lockDir);
2253
2869
  });
2254
2870
  open = false;
2255
2871
  return result;
@@ -2258,7 +2874,7 @@ var acquireLock = (configDir, create) => {
2258
2874
  };
2259
2875
  var readIdentity = (paths) => {
2260
2876
  const identity = readOptional(paths.identity);
2261
- if (identity === null && (fs4.existsSync(paths.active) || fs4.existsSync(paths.generations) && fs4.readdirSync(paths.generations).length > 0)) {
2877
+ if (identity === null && (fs6.existsSync(paths.active) || fs6.existsSync(paths.generations) && fs6.readdirSync(paths.generations).length > 0)) {
2262
2878
  throw new ServerStoreError(
2263
2879
  "Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
2264
2880
  );
@@ -2268,7 +2884,7 @@ var readIdentity = (paths) => {
2268
2884
  var readActive = (paths) => {
2269
2885
  let target;
2270
2886
  try {
2271
- target = fs4.readlinkSync(paths.active);
2887
+ target = fs6.readlinkSync(paths.active);
2272
2888
  } catch (cause) {
2273
2889
  if (hasCode(cause, "ENOENT")) return null;
2274
2890
  throw cause;
@@ -2278,13 +2894,13 @@ var readActive = (paths) => {
2278
2894
  "Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
2279
2895
  );
2280
2896
  }
2281
- const directory = path4.join(paths.configDir, target);
2897
+ const directory = path6.join(paths.configDir, target);
2282
2898
  const files = {
2283
- identity: fs4.readFileSync(path4.join(directory, fileNames.identity), "utf8"),
2284
- config: fs4.readFileSync(path4.join(directory, fileNames.config), "utf8"),
2285
- env: fs4.readFileSync(path4.join(directory, fileNames.env), "utf8"),
2286
- compose: fs4.readFileSync(path4.join(directory, fileNames.compose), "utf8"),
2287
- postgresInit: fs4.readFileSync(path4.join(directory, fileNames.postgresInit), "utf8")
2899
+ identity: fs6.readFileSync(path6.join(directory, fileNames.identity), "utf8"),
2900
+ config: fs6.readFileSync(path6.join(directory, fileNames.config), "utf8"),
2901
+ env: fs6.readFileSync(path6.join(directory, fileNames.env), "utf8"),
2902
+ compose: fs6.readFileSync(path6.join(directory, fileNames.compose), "utf8"),
2903
+ postgresInit: fs6.readFileSync(path6.join(directory, fileNames.postgresInit), "utf8")
2288
2904
  };
2289
2905
  if (readIdentity(paths) !== files.identity) {
2290
2906
  throw new ServerStoreError(
@@ -2294,20 +2910,20 @@ var readActive = (paths) => {
2294
2910
  return { directory, files };
2295
2911
  };
2296
2912
  var publishIdentity = (paths, identity) => {
2297
- const temporary = path4.join(paths.configDir, `.identity-${randomUUID()}`);
2913
+ const temporary = path6.join(paths.configDir, `.identity-${randomUUID()}`);
2298
2914
  writeDurable(temporary, identity);
2299
- fs4.linkSync(temporary, paths.identity);
2915
+ fs6.linkSync(temporary, paths.identity);
2300
2916
  syncDirectory(paths.configDir);
2301
- fs4.unlinkSync(temporary);
2917
+ fs6.unlinkSync(temporary);
2302
2918
  };
2303
2919
  var prepareGeneration = (paths, files) => {
2304
- fs4.mkdirSync(paths.generations, { recursive: true, mode: 448 });
2920
+ fs6.mkdirSync(paths.generations, { recursive: true, mode: 448 });
2305
2921
  const generationName = `gen-${randomUUID()}`;
2306
- const directory = path4.join(paths.generations, generationName);
2307
- fs4.mkdirSync(directory, { mode: 448 });
2922
+ const directory = path6.join(paths.generations, generationName);
2923
+ fs6.mkdirSync(directory, { mode: 448 });
2308
2924
  for (const key of fileKeys) {
2309
2925
  writeDurable(
2310
- path4.join(directory, fileNames[key]),
2926
+ path6.join(directory, fileNames[key]),
2311
2927
  files[key],
2312
2928
  key === "postgresInit" ? 493 : 384
2313
2929
  );
@@ -2318,18 +2934,18 @@ var prepareGeneration = (paths, files) => {
2318
2934
  return { directory, files };
2319
2935
  };
2320
2936
  var activateGeneration = (paths, generation) => {
2321
- const relative3 = path4.relative(paths.configDir, generation.directory);
2322
- if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative3) || readIdentity(paths) !== fs4.readFileSync(path4.join(generation.directory, "identity.env"), "utf8")) {
2937
+ const relative3 = path6.relative(paths.configDir, generation.directory);
2938
+ if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative3) || readIdentity(paths) !== fs6.readFileSync(path6.join(generation.directory, "identity.env"), "utf8")) {
2323
2939
  throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
2324
2940
  }
2325
2941
  for (const key of fileKeys) {
2326
- if (fs4.readFileSync(path4.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
2942
+ if (fs6.readFileSync(path6.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
2327
2943
  throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
2328
2944
  }
2329
2945
  }
2330
- const pointer = path4.join(paths.configDir, `.active-${randomUUID()}`);
2331
- fs4.symlinkSync(relative3, pointer);
2332
- fs4.renameSync(pointer, paths.active);
2946
+ const pointer = path6.join(paths.configDir, `.active-${randomUUID()}`);
2947
+ fs6.symlinkSync(relative3, pointer);
2948
+ fs6.renameSync(pointer, paths.active);
2333
2949
  syncDirectory(paths.configDir);
2334
2950
  };
2335
2951
  var prepareFiles = (paths, files) => {
@@ -2338,7 +2954,7 @@ var prepareFiles = (paths, files) => {
2338
2954
  throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
2339
2955
  }
2340
2956
  const active = readActive(paths);
2341
- if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs4.statSync(path4.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
2957
+ if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs6.statSync(path6.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
2342
2958
  return active;
2343
2959
  if (identity === null) publishIdentity(paths, files.identity);
2344
2960
  return prepareGeneration(paths, files);
@@ -2351,9 +2967,9 @@ var commitGeneration = (paths, files) => {
2351
2967
  var createStore = (configDir, lock) => {
2352
2968
  const paths = {
2353
2969
  configDir,
2354
- identity: path4.join(configDir, "identity.env"),
2355
- generations: path4.join(configDir, "generations"),
2356
- active: path4.join(configDir, "active")
2970
+ identity: path6.join(configDir, "identity.env"),
2971
+ generations: path6.join(configDir, "generations"),
2972
+ active: path6.join(configDir, "active")
2357
2973
  };
2358
2974
  const whileOwned = (operation) => attempt(() => {
2359
2975
  lock.assertOwned();
@@ -2366,13 +2982,13 @@ var createStore = (configDir, lock) => {
2366
2982
  prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
2367
2983
  activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
2368
2984
  createBackup: (previous, target) => whileOwned(() => {
2369
- const backups = path4.join(configDir, "backups");
2370
- fs4.mkdirSync(backups, { recursive: true, mode: 448 });
2371
- fs4.chmodSync(backups, 448);
2372
- const directory = path4.join(backups, `upgrade-${randomUUID()}`);
2373
- fs4.mkdirSync(directory, { mode: 448 });
2985
+ const backups = path6.join(configDir, "backups");
2986
+ fs6.mkdirSync(backups, { recursive: true, mode: 448 });
2987
+ fs6.chmodSync(backups, 448);
2988
+ const directory = path6.join(backups, `upgrade-${randomUUID()}`);
2989
+ fs6.mkdirSync(directory, { mode: 448 });
2374
2990
  writeDurable(
2375
- path4.join(directory, "recovery.json"),
2991
+ path6.join(directory, "recovery.json"),
2376
2992
  `${JSON.stringify(
2377
2993
  {
2378
2994
  previousGeneration: previous.directory,
@@ -2388,21 +3004,21 @@ var createStore = (configDir, lock) => {
2388
3004
  syncDirectory(directory);
2389
3005
  syncDirectory(backups);
2390
3006
  syncDirectory(configDir);
2391
- const partialFile = path4.join(directory, "database.sql.partial");
3007
+ const partialFile = path6.join(directory, "database.sql.partial");
2392
3008
  return {
2393
3009
  directory,
2394
3010
  partialFile,
2395
3011
  complete: () => whileOwned(() => {
2396
- if (fs4.statSync(partialFile).size === 0)
3012
+ if (fs6.statSync(partialFile).size === 0)
2397
3013
  throw new ServerStoreError("Database backup is empty; refusing target startup.");
2398
- const fd = fs4.openSync(partialFile, "r");
3014
+ const fd = fs6.openSync(partialFile, "r");
2399
3015
  try {
2400
- fs4.fchmodSync(fd, 384);
2401
- fs4.fsyncSync(fd);
3016
+ fs6.fchmodSync(fd, 384);
3017
+ fs6.fsyncSync(fd);
2402
3018
  } finally {
2403
- fs4.closeSync(fd);
3019
+ fs6.closeSync(fd);
2404
3020
  }
2405
- fs4.renameSync(partialFile, path4.join(directory, "database.sql"));
3021
+ fs6.renameSync(partialFile, path6.join(directory, "database.sql"));
2406
3022
  syncDirectory(directory);
2407
3023
  })
2408
3024
  };
@@ -2411,7 +3027,7 @@ var createStore = (configDir, lock) => {
2411
3027
  };
2412
3028
  var refuseFlatLayout = (configDir) => {
2413
3029
  if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
2414
- (name) => fs4.existsSync(path4.join(configDir, name))
3030
+ (name) => fs6.existsSync(path6.join(configDir, name))
2415
3031
  )) {
2416
3032
  throw new ServerStoreError(
2417
3033
  "Unreleased flat server configuration found. Preserve its credentials and volumes; migrate it to the generation layout before retrying. Setup will not replace this identity."
@@ -2419,7 +3035,7 @@ var refuseFlatLayout = (configDir) => {
2419
3035
  }
2420
3036
  };
2421
3037
  var withServerStore = async (directory, operation, options = {}) => {
2422
- const configDir = path4.resolve(directory);
3038
+ const configDir = path6.resolve(directory);
2423
3039
  let lock;
2424
3040
  let result;
2425
3041
  try {
@@ -2436,9 +3052,9 @@ var withServerStore = async (directory, operation, options = {}) => {
2436
3052
  };
2437
3053
 
2438
3054
  // src/version.ts
2439
- import * as fs5 from "node:fs";
3055
+ import * as fs7 from "node:fs";
2440
3056
  var cliVersion = () => {
2441
- const raw = fs5.readFileSync(new URL("../package.json", import.meta.url), "utf8");
3057
+ const raw = fs7.readFileSync(new URL("../package.json", import.meta.url), "utf8");
2442
3058
  const parsed = JSON.parse(raw);
2443
3059
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
2444
3060
  const version = parsed.version;
@@ -3117,7 +3733,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
3117
3733
  return version;
3118
3734
  };
3119
3735
  var parseDockerSocketPath = (socket) => {
3120
- if (!path5.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
3736
+ if (!path7.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
3121
3737
  throw setupError(
3122
3738
  "--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
3123
3739
  );
@@ -3143,8 +3759,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
3143
3759
  if (options.assetsDir !== void 0) {
3144
3760
  try {
3145
3761
  const [compose2, postgresInit2] = await Promise.all([
3146
- readFile(path5.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
3147
- readFile(path5.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
3762
+ readFile(path7.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
3763
+ readFile(path7.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
3148
3764
  ]);
3149
3765
  validateComposeAsset(compose2);
3150
3766
  validatePostgresAsset(postgresInit2);
@@ -3657,10 +4273,10 @@ var manageServer = async (command, args, runtime, store) => {
3657
4273
  };
3658
4274
  var nodeServerRuntime = () => {
3659
4275
  const environment = { ...process.env };
3660
- const home = os3.homedir();
3661
- const configHome = environment["XDG_CONFIG_HOME"] ?? path5.join(home, ".config");
4276
+ const home = os4.homedir();
4277
+ const configHome = environment["XDG_CONFIG_HOME"] ?? path7.join(home, ".config");
3662
4278
  return {
3663
- configDir: path5.join(configHome, "mend"),
4279
+ configDir: path7.join(configHome, "mend"),
3664
4280
  platform: process.platform,
3665
4281
  cliVersion: cliVersion(),
3666
4282
  run: (command, args, options) => runServerProcess(command, args, environment, options),
@@ -3886,8 +4502,8 @@ var renderMendToml = (proposals) => {
3886
4502
  };
3887
4503
 
3888
4504
  // src/skills.ts
3889
- import * as fs6 from "node:fs";
3890
- import * as path6 from "node:path";
4505
+ import * as fs8 from "node:fs";
4506
+ import * as path8 from "node:path";
3891
4507
  var DEFAULT_SKILLS_DIR = ".agents/skills";
3892
4508
  var MAX_SKILL_FILES = 64;
3893
4509
  var MAX_SKILL_FILE_BYTES = 512 * 1024;
@@ -3910,12 +4526,12 @@ var readFrontmatterDescription = (contents) => {
3910
4526
  var walkFiles = (root) => {
3911
4527
  const found = [];
3912
4528
  const visit = (dir) => {
3913
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
4529
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
3914
4530
  if (IGNORED_DIRS.has(entry.name)) continue;
3915
- const absolute = path6.join(dir, entry.name);
4531
+ const absolute = path8.join(dir, entry.name);
3916
4532
  let stat;
3917
4533
  try {
3918
- stat = fs6.statSync(absolute);
4534
+ stat = fs8.statSync(absolute);
3919
4535
  } catch {
3920
4536
  continue;
3921
4537
  }
@@ -3929,7 +4545,7 @@ var walkFiles = (root) => {
3929
4545
  var scanSkillLibrary = (root) => {
3930
4546
  let entries;
3931
4547
  try {
3932
- entries = fs6.readdirSync(root, { withFileTypes: true });
4548
+ entries = fs8.readdirSync(root, { withFileTypes: true });
3933
4549
  } catch {
3934
4550
  return { error: `${root} is not a readable directory` };
3935
4551
  }
@@ -3937,23 +4553,23 @@ var scanSkillLibrary = (root) => {
3937
4553
  const notes = [];
3938
4554
  for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
3939
4555
  if (entry.name.startsWith(".")) continue;
3940
- const skillDir = path6.join(root, entry.name);
4556
+ const skillDir = path8.join(root, entry.name);
3941
4557
  let stat;
3942
4558
  try {
3943
- stat = fs6.statSync(skillDir);
4559
+ stat = fs8.statSync(skillDir);
3944
4560
  } catch {
3945
4561
  continue;
3946
4562
  }
3947
4563
  if (!stat.isDirectory()) continue;
3948
- if (!fs6.existsSync(path6.join(skillDir, "SKILL.md"))) {
4564
+ if (!fs8.existsSync(path8.join(skillDir, "SKILL.md"))) {
3949
4565
  notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
3950
4566
  continue;
3951
4567
  }
3952
4568
  const files = [];
3953
4569
  let bytes = 0;
3954
4570
  for (const absolute of walkFiles(skillDir)) {
3955
- const relative3 = path6.relative(skillDir, absolute).split(path6.sep).join("/");
3956
- const buffer = fs6.readFileSync(absolute);
4571
+ const relative3 = path8.relative(skillDir, absolute).split(path8.sep).join("/");
4572
+ const buffer = fs8.readFileSync(absolute);
3957
4573
  if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
3958
4574
  notes.push({ skill: entry.name, message: `${relative3} is over 512KB \u2014 skipped` });
3959
4575
  continue;
@@ -3988,16 +4604,16 @@ var scanSkillLibrary = (root) => {
3988
4604
  };
3989
4605
 
3990
4606
  // src/ssh-setup.ts
3991
- import * as os5 from "node:os";
3992
- import * as path8 from "node:path";
4607
+ import * as os6 from "node:os";
4608
+ import * as path10 from "node:path";
3993
4609
 
3994
4610
  // ../../packages/workspace-ssh/src/index.ts
3995
- import { spawnSync } from "node:child_process";
3996
- import { createHash as createHash2 } from "node:crypto";
3997
- import * as fs7 from "node:fs";
4611
+ import { spawnSync as spawnSync2 } from "node:child_process";
4612
+ import { createHash as createHash3 } from "node:crypto";
4613
+ import * as fs9 from "node:fs";
3998
4614
  import { isIP as isIP2 } from "node:net";
3999
- import * as os4 from "node:os";
4000
- import * as path7 from "node:path";
4615
+ import * as os5 from "node:os";
4616
+ import * as path9 from "node:path";
4001
4617
  var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
4002
4618
  var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
4003
4619
  var WorkspaceSshError = class extends Error {
@@ -4015,7 +4631,7 @@ var WorkspaceSshError = class extends Error {
4015
4631
  var success = (value) => ({ ok: true, value });
4016
4632
  var failure = (error) => ({ ok: false, error });
4017
4633
  var stripIpv6Brackets = (hostname6) => hostname6.startsWith("[") && hostname6.endsWith("]") ? hostname6.slice(1, -1) : hostname6;
4018
- var hashIdentity = (identity) => createHash2("sha256").update(identity).digest("hex").slice(0, 24);
4634
+ var hashIdentity = (identity) => createHash3("sha256").update(identity).digest("hex").slice(0, 24);
4019
4635
  var aliasLabel = (hostname6) => {
4020
4636
  const label = hostname6.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
4021
4637
  return label === "" ? "server" : label;
@@ -4084,8 +4700,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
4084
4700
  var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
4085
4701
  var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
4086
4702
  var parseIdentityPath = (value) => {
4087
- const resolved = path7.resolve(
4088
- value.startsWith("~/") ? path7.join(os4.homedir(), value.slice(2)) : value
4703
+ const resolved = path9.resolve(
4704
+ value.startsWith("~/") ? path9.join(os5.homedir(), value.slice(2)) : value
4089
4705
  );
4090
4706
  if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
4091
4707
  return failure(
@@ -4220,7 +4836,7 @@ var workspaceSshPublicKeyFingerprint = (publicKey) => {
4220
4836
  return failure(new WorkspaceSshError("key", "SSH public key has no encoded key data."));
4221
4837
  }
4222
4838
  try {
4223
- const digest = createHash2("sha256").update(Buffer.from(encoded, "base64")).digest("base64");
4839
+ const digest = createHash3("sha256").update(Buffer.from(encoded, "base64")).digest("base64");
4224
4840
  return success(`SHA256:${digest.replace(/=+$/, "")}`);
4225
4841
  } catch (cause) {
4226
4842
  return failure(
@@ -4231,7 +4847,7 @@ var workspaceSshPublicKeyFingerprint = (publicKey) => {
4231
4847
  var publicKeyIdentity = (publicKey) => publicKey.trim().split(/\s+/).slice(0, 2).join(" ");
4232
4848
  var agentKeys = () => {
4233
4849
  if (!process.env["SSH_AUTH_SOCK"]) return [];
4234
- const listed = spawnSync("ssh-add", ["-L"], {
4850
+ const listed = spawnSync2("ssh-add", ["-L"], {
4235
4851
  encoding: "utf8",
4236
4852
  timeout: 3e3,
4237
4853
  stdio: ["ignore", "pipe", "pipe"],
@@ -4241,7 +4857,7 @@ var agentKeys = () => {
4241
4857
  };
4242
4858
  var readText = (file) => {
4243
4859
  try {
4244
- return success(fs7.readFileSync(file, "utf8"));
4860
+ return success(fs9.readFileSync(file, "utf8"));
4245
4861
  } catch (cause) {
4246
4862
  const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
4247
4863
  return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
@@ -4268,7 +4884,7 @@ var readRequiredKey = (keyPath, source) => {
4268
4884
  const privatePath = publicPath.slice(0, -".pub".length);
4269
4885
  const read = readText(publicPath);
4270
4886
  if (!read.ok) return read;
4271
- const derived = spawnSync("ssh-keygen", ["-y", "-P", "", "-f", privatePath], {
4887
+ const derived = spawnSync2("ssh-keygen", ["-y", "-P", "", "-f", privatePath], {
4272
4888
  encoding: "utf8",
4273
4889
  timeout: 3e3,
4274
4890
  stdio: ["ignore", "pipe", "pipe"],
@@ -4299,19 +4915,19 @@ var readRequiredKey = (keyPath, source) => {
4299
4915
  var readOptionalKey = (keyPath) => {
4300
4916
  const parsed = parseIdentityPath(keyPath);
4301
4917
  if (!parsed.ok) return parsed;
4302
- if (!fs7.existsSync(parsed.value) && !fs7.existsSync(publicKeyPath(parsed.value)))
4918
+ if (!fs9.existsSync(parsed.value) && !fs9.existsSync(publicKeyPath(parsed.value)))
4303
4919
  return success(null);
4304
4920
  return readRequiredKey(parsed.value, "existing");
4305
4921
  };
4306
4922
  var pinAgentKey = (configHome, publicKey) => {
4307
- const identityFile = path7.resolve(
4923
+ const identityFile = path9.resolve(
4308
4924
  configHome,
4309
4925
  "ssh",
4310
4926
  `agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
4311
4927
  );
4312
4928
  try {
4313
- fs7.mkdirSync(path7.dirname(identityFile), { recursive: true, mode: 448 });
4314
- fs7.writeFileSync(identityFile, `${publicKey}
4929
+ fs9.mkdirSync(path9.dirname(identityFile), { recursive: true, mode: 448 });
4930
+ fs9.writeFileSync(identityFile, `${publicKey}
4315
4931
  `, { mode: 384 });
4316
4932
  return success({ publicKey, identityFile, source: "agent" });
4317
4933
  } catch (cause) {
@@ -4322,15 +4938,15 @@ var pinAgentKey = (configHome, publicKey) => {
4322
4938
  };
4323
4939
  var generateDedicatedKey = (privatePath) => {
4324
4940
  try {
4325
- fs7.mkdirSync(path7.dirname(privatePath), { recursive: true, mode: 448 });
4941
+ fs9.mkdirSync(path9.dirname(privatePath), { recursive: true, mode: 448 });
4326
4942
  } catch (cause) {
4327
4943
  return failure(
4328
- new WorkspaceSshError("key", `Could not create ${path7.dirname(privatePath)}.`, cause)
4944
+ new WorkspaceSshError("key", `Could not create ${path9.dirname(privatePath)}.`, cause)
4329
4945
  );
4330
4946
  }
4331
- const generated = spawnSync(
4947
+ const generated = spawnSync2(
4332
4948
  "ssh-keygen",
4333
- ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os4.hostname()}`],
4949
+ ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os5.hostname()}`],
4334
4950
  { encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
4335
4951
  );
4336
4952
  if (generated.status !== 0) {
@@ -4354,7 +4970,7 @@ var generateDedicatedKey = (privatePath) => {
4354
4970
  return fresh;
4355
4971
  };
4356
4972
  var dedicatedKey = (configHome, create) => {
4357
- const privatePath = path7.join(configHome, "ssh", "id_ed25519");
4973
+ const privatePath = path9.join(configHome, "ssh", "id_ed25519");
4358
4974
  const existing = readOptionalKey(privatePath);
4359
4975
  if (existing.ok === false) return failure(existing.error);
4360
4976
  if (existing.value !== null || !create) return existing;
@@ -4402,10 +5018,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
4402
5018
  const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
4403
5019
  if (reconciled.ok === false) return failure(reconciled.error);
4404
5020
  try {
4405
- fs7.mkdirSync(path7.dirname(configFile), { recursive: true, mode: 448 });
4406
- fs7.chmodSync(path7.dirname(configFile), 448);
4407
- fs7.writeFileSync(configFile, reconciled.value, { mode: 384 });
4408
- fs7.chmodSync(configFile, 384);
5021
+ fs9.mkdirSync(path9.dirname(configFile), { recursive: true, mode: 448 });
5022
+ fs9.chmodSync(path9.dirname(configFile), 448);
5023
+ fs9.writeFileSync(configFile, reconciled.value, { mode: 384 });
5024
+ fs9.chmodSync(configFile, 384);
4409
5025
  return success(void 0);
4410
5026
  } catch (cause) {
4411
5027
  return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
@@ -4414,23 +5030,23 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
4414
5030
 
4415
5031
  // src/ssh-setup.ts
4416
5032
  var ansi = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
4417
- var dim3 = ansi("2");
4418
- var green3 = ansi("32");
5033
+ var dim4 = ansi("2");
5034
+ var green4 = ansi("32");
4419
5035
  var warn = ansi("33");
4420
- var say3 = (line) => console.log(line);
4421
- var sshConfigPath = () => path8.join(os5.homedir(), ".ssh", "config");
5036
+ var say4 = (line) => console.log(line);
5037
+ var sshConfigPath = () => path10.join(os6.homedir(), ".ssh", "config");
4422
5038
  var flagValue = (args, flag) => {
4423
5039
  const index = args.indexOf(flag);
4424
5040
  return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
4425
5041
  };
4426
5042
  var showFailure = (message) => {
4427
- say3(`mend: ${message}`);
5043
+ say4(`mend: ${message}`);
4428
5044
  process.exitCode = 1;
4429
5045
  };
4430
5046
  var showStatus = async (api2, cliHome, serverUrl, args) => {
4431
5047
  const view = await api2("GET", "/workspace-ssh");
4432
5048
  if (view.gateway === null) {
4433
- say3(`workspace ssh ${warn("no gateway")} ${dim3("\xB7 this deployment exposes none")}`);
5049
+ say4(`workspace ssh ${warn("no gateway")} ${dim4("\xB7 this deployment exposes none")}`);
4434
5050
  return;
4435
5051
  }
4436
5052
  const parsedTarget = parseWorkspaceSshTarget({
@@ -4453,16 +5069,16 @@ var showStatus = async (api2, cliHome, serverUrl, args) => {
4453
5069
  key: picked.value,
4454
5070
  registeredFingerprints: view.keys.map((key) => key.fingerprint)
4455
5071
  });
4456
- say3(
4457
- `gateway ${parsedTarget.value.hostname}:${parsedTarget.value.port} ${dim3(`\xB7 published for ${serverUrl}`)}`
5072
+ say4(
5073
+ `gateway ${parsedTarget.value.hostname}:${parsedTarget.value.port} ${dim4(`\xB7 published for ${serverUrl}`)}`
4458
5074
  );
4459
- say3(
4460
- picked.value === null ? `client key ${warn("none available")} ${dim3("\xB7 run: mend ssh setup")}` : readiness.keyRegistered ? `client key ${green3("\u25CF")} ${picked.value.fingerprint} ${dim3("\xB7 registered")}` : `client key ${warn("not registered")} ${dim3(`\xB7 ${picked.value.fingerprint}`)}`
5075
+ say4(
5076
+ picked.value === null ? `client key ${warn("none available")} ${dim4("\xB7 run: mend ssh setup")}` : readiness.keyRegistered ? `client key ${green4("\u25CF")} ${picked.value.fingerprint} ${dim4("\xB7 registered")}` : `client key ${warn("not registered")} ${dim4(`\xB7 ${picked.value.fingerprint}`)}`
4461
5077
  );
4462
- say3(
4463
- readiness.configReady ? `ssh config ${green3("\u25CF")} Host ${parsedTarget.value.alias} ${dim3(`\xB7 ${sshConfigPath()}`)}` : `ssh config ${warn("missing or stale")} ${dim3("\xB7 run: mend ssh setup")}`
5078
+ say4(
5079
+ readiness.configReady ? `ssh config ${green4("\u25CF")} Host ${parsedTarget.value.alias} ${dim4(`\xB7 ${sshConfigPath()}`)}` : `ssh config ${warn("missing or stale")} ${dim4("\xB7 run: mend ssh setup")}`
4464
5080
  );
4465
- say3(
5081
+ say4(
4466
5082
  "host trust not checked \xB7 status checks config and client-key registration, not an SSH connection"
4467
5083
  );
4468
5084
  };
@@ -4488,7 +5104,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
4488
5104
  if (picked.value === null) return showFailure("No workspace SSH key is available.");
4489
5105
  const registered = await api2("POST", "/workspace-ssh/keys", {
4490
5106
  publicKey: picked.value.publicKey,
4491
- name: os5.hostname()
5107
+ name: os6.hostname()
4492
5108
  });
4493
5109
  const sourceLabel = {
4494
5110
  explicit: "from --key",
@@ -4496,22 +5112,22 @@ var setup = async (api2, cliHome, serverUrl, args) => {
4496
5112
  existing: "existing selected key",
4497
5113
  generated: "generated dedicated key"
4498
5114
  }[picked.value.source];
4499
- say3(`key ${green3("\u25CF")} ${registered.fingerprint} ${dim3(`\xB7 ${sourceLabel}`)}`);
5115
+ say4(`key ${green4("\u25CF")} ${registered.fingerprint} ${dim4(`\xB7 ${sourceLabel}`)}`);
4500
5116
  const written = writeWorkspaceSshConfig(
4501
5117
  sshConfigPath(),
4502
5118
  parsedTarget.value,
4503
5119
  picked.value.identityFile
4504
5120
  );
4505
5121
  if (written.ok === false) return showFailure(written.error.message);
4506
- say3(
4507
- `ssh config ${green3("\u25CF")} Host ${parsedTarget.value.alias} ${dim3(`\xB7 ${sshConfigPath()}`)}`
5122
+ say4(
5123
+ `ssh config ${green4("\u25CF")} Host ${parsedTarget.value.alias} ${dim4(`\xB7 ${sshConfigPath()}`)}`
4508
5124
  );
4509
- say3(
5125
+ say4(
4510
5126
  "host trust not checked \xB7 SSH verifies the gateway when you connect; setup does not replace known_hosts entries"
4511
5127
  );
4512
- say3("");
4513
- say3(
4514
- `connect with ssh ${view.gateway.usernamePrefix}-<workspace-id>@${parsedTarget.value.alias} ${dim3("\xB7 the VS Code extension uses this automatically")}`
5128
+ say4("");
5129
+ say4(
5130
+ `connect with ssh ${view.gateway.usernamePrefix}-<workspace-id>@${parsedTarget.value.alias} ${dim4("\xB7 the VS Code extension uses this automatically")}`
4515
5131
  );
4516
5132
  };
4517
5133
  var sshCommand = async (args, api2, cliHome, serverUrl) => {
@@ -4530,8 +5146,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
4530
5146
  };
4531
5147
 
4532
5148
  // src/uninstall.ts
4533
- import * as fs8 from "node:fs";
4534
- import * as path9 from "node:path";
5149
+ import * as fs10 from "node:fs";
5150
+ import * as path11 from "node:path";
4535
5151
  var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
4536
5152
  var parseUninstallArgs = (args) => {
4537
5153
  let scope = null;
@@ -4553,14 +5169,14 @@ var parseUninstallArgs = (args) => {
4553
5169
  var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
4554
5170
  var countEntries = (directory) => {
4555
5171
  try {
4556
- return fs8.readdirSync(directory).length;
5172
+ return fs10.readdirSync(directory).length;
4557
5173
  } catch {
4558
5174
  return 0;
4559
5175
  }
4560
5176
  };
4561
5177
  var exists = (file) => {
4562
5178
  try {
4563
- fs8.lstatSync(file);
5179
+ fs10.lstatSync(file);
4564
5180
  return true;
4565
5181
  } catch {
4566
5182
  return false;
@@ -4569,7 +5185,7 @@ var exists = (file) => {
4569
5185
  var managedBlockCount = (sshConfigFile) => {
4570
5186
  let config;
4571
5187
  try {
4572
- config = fs8.readFileSync(sshConfigFile, "utf8");
5188
+ config = fs10.readFileSync(sshConfigFile, "utf8");
4573
5189
  } catch {
4574
5190
  return 0;
4575
5191
  }
@@ -4593,27 +5209,27 @@ var describeUninstall = async (runtime, scope) => {
4593
5209
  if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
4594
5210
  server = "none";
4595
5211
  } else if (read.value === null) {
4596
- server = exists(path9.join(configDir, "identity.env")) ? {
5212
+ server = exists(path11.join(configDir, "identity.env")) ? {
4597
5213
  version: "(no active generation)",
4598
5214
  appUrl: "",
4599
5215
  dockerContext: "",
4600
- generations: countEntries(path9.join(configDir, "generations")),
4601
- backups: countEntries(path9.join(configDir, "backups"))
5216
+ generations: countEntries(path11.join(configDir, "generations")),
5217
+ backups: countEntries(path11.join(configDir, "backups"))
4602
5218
  } : "none";
4603
5219
  } else {
4604
5220
  server = {
4605
5221
  version: read.value.config.serverVersion,
4606
5222
  appUrl: read.value.config.appUrl,
4607
5223
  dockerContext: read.value.config.dockerContext,
4608
- generations: countEntries(path9.join(configDir, "generations")),
4609
- backups: countEntries(path9.join(configDir, "backups"))
5224
+ generations: countEntries(path11.join(configDir, "generations")),
5225
+ backups: countEntries(path11.join(configDir, "backups"))
4610
5226
  };
4611
5227
  }
4612
5228
  }
4613
5229
  let home = null;
4614
5230
  if (scope !== "server") {
4615
- const cliConfig = path9.join(runtime.cliHome, "cli.json");
4616
- const sshDirectory = path9.join(runtime.cliHome, "ssh");
5231
+ const cliConfig = path11.join(runtime.cliHome, "cli.json");
5232
+ const sshDirectory = path11.join(runtime.cliHome, "ssh");
4617
5233
  home = {
4618
5234
  cliConfig: exists(cliConfig) ? cliConfig : null,
4619
5235
  sshDirectory: exists(sshDirectory) ? sshDirectory : null,
@@ -4745,7 +5361,7 @@ var removeServer = async (runtime, failures, leftovers) => {
4745
5361
  }
4746
5362
  }
4747
5363
  for (const name of SERVER_FILES) {
4748
- fs8.rmSync(path9.join(configDir, name), { recursive: true, force: true });
5364
+ fs10.rmSync(path11.join(configDir, name), { recursive: true, force: true });
4749
5365
  }
4750
5366
  server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
4751
5367
  },
@@ -4756,20 +5372,20 @@ var removeServer = async (runtime, failures, leftovers) => {
4756
5372
  var removeHome = async (runtime, plan, failures, leftovers) => {
4757
5373
  const { server } = runtime;
4758
5374
  if (plan.cliConfig !== null) {
4759
- fs8.rmSync(plan.cliConfig, { force: true });
5375
+ fs10.rmSync(plan.cliConfig, { force: true });
4760
5376
  server.writeLine(`removed ${plan.cliConfig}`);
4761
5377
  }
4762
5378
  if (plan.sshDirectory !== null) {
4763
- fs8.rmSync(plan.sshDirectory, { recursive: true, force: true });
5379
+ fs10.rmSync(plan.sshDirectory, { recursive: true, force: true });
4764
5380
  server.writeLine(`removed ${plan.sshDirectory}`);
4765
5381
  }
4766
5382
  if (plan.managedSshBlocks > 0) {
4767
5383
  try {
4768
- const before = fs8.readFileSync(runtime.sshConfigFile, "utf8");
5384
+ const before = fs10.readFileSync(runtime.sshConfigFile, "utf8");
4769
5385
  const stripped = stripManagedWorkspaceSshBlocks(before);
4770
5386
  if (!stripped.ok) throw stripped.error;
4771
5387
  if (stripped.value.removed > 0) {
4772
- fs8.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
5388
+ fs10.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
4773
5389
  server.writeLine(
4774
5390
  `removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
4775
5391
  );
@@ -4782,12 +5398,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
4782
5398
  }
4783
5399
  let remaining = [];
4784
5400
  try {
4785
- remaining = fs8.readdirSync(runtime.cliHome);
5401
+ remaining = fs10.readdirSync(runtime.cliHome);
4786
5402
  } catch {
4787
5403
  return;
4788
5404
  }
4789
5405
  if (remaining.length === 0) {
4790
- fs8.rmdirSync(runtime.cliHome);
5406
+ fs10.rmdirSync(runtime.cliHome);
4791
5407
  server.writeLine(`removed ${runtime.cliHome}`);
4792
5408
  } else {
4793
5409
  leftovers.push(
@@ -4815,24 +5431,57 @@ var executeUninstall = async (runtime, plan) => {
4815
5431
  return { failures, leftovers };
4816
5432
  };
4817
5433
 
5434
+ // src/upgrade-url.ts
5435
+ var PATHS = {
5436
+ tty: "/api/tty",
5437
+ "service-tunnel": "/api/service-tunnel",
5438
+ "keys-bridge": "/api/keys/bridge/ws"
5439
+ };
5440
+ var upgradeBaseUrl = (serverUrl, target, params, extra = {}) => {
5441
+ const url = new URL(`${serverUrl.replace(/\/$/, "")}${PATHS[target]}`);
5442
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
5443
+ for (const [key, value] of Object.entries({ ...params, ...extra })) {
5444
+ if (typeof value === "string" && value !== "") url.searchParams.set(key, value);
5445
+ }
5446
+ return url;
5447
+ };
5448
+ var mintRefusedInTransit = async (serverUrl, send = fetch) => {
5449
+ try {
5450
+ const response = await send(`${serverUrl.replace(/\/$/, "")}/api/health`);
5451
+ if (!response.ok) return false;
5452
+ const health = await response.json();
5453
+ return typeof health === "object" && health !== null && "upgradeTickets" in health && health.upgradeTickets === true;
5454
+ } catch {
5455
+ return false;
5456
+ }
5457
+ };
5458
+ var MINT_REFUSED_IN_TRANSIT = "POST /api/upgrade-tickets answered 404, but this server mints upgrade tickets: something between this client and Mend is refusing it. The saved token was not put in a URL.";
5459
+ var upgradeUrl = async (input) => {
5460
+ const url = upgradeBaseUrl(input.serverUrl, input.target, input.params, input.extra);
5461
+ const minted = await input.mint(input.target, input.params);
5462
+ if (minted.kind === "ticket") url.searchParams.set("ticket", minted.ticket);
5463
+ else if (input.legacyToken !== null) url.searchParams.set("token", input.legacyToken);
5464
+ return url;
5465
+ };
5466
+
4818
5467
  // src/main.ts
4819
5468
  var mendCliHome = () => {
4820
5469
  const xdg = process.env["XDG_CONFIG_HOME"];
4821
- const preferred = path10.join(
4822
- xdg === void 0 || xdg === "" ? path10.join(os6.homedir(), ".config") : xdg,
5470
+ const preferred = path12.join(
5471
+ xdg === void 0 || xdg === "" ? path12.join(os7.homedir(), ".config") : xdg,
4823
5472
  "mend"
4824
5473
  );
4825
- const legacy = path10.join(os6.homedir(), ".mend");
4826
- return !fs9.existsSync(preferred) && fs9.existsSync(legacy) ? legacy : preferred;
5474
+ const legacy = path12.join(os7.homedir(), ".mend");
5475
+ return !fs11.existsSync(preferred) && fs11.existsSync(legacy) ? legacy : preferred;
4827
5476
  };
4828
- var CONFIG_PATH = path10.join(mendCliHome(), "cli.json");
5477
+ var CONFIG_PATH = path12.join(mendCliHome(), "cli.json");
4829
5478
  var loadConfig = () => {
4830
5479
  let fileConfig = {};
4831
- if (fs9.existsSync(CONFIG_PATH)) {
5480
+ if (fs11.existsSync(CONFIG_PATH)) {
4832
5481
  try {
4833
- fileConfig = JSON.parse(fs9.readFileSync(CONFIG_PATH, "utf8"));
5482
+ fileConfig = JSON.parse(fs11.readFileSync(CONFIG_PATH, "utf8"));
4834
5483
  } catch {
4835
- fail4(`could not parse ${CONFIG_PATH}`);
5484
+ fail5(`could not parse ${CONFIG_PATH}`);
4836
5485
  }
4837
5486
  }
4838
5487
  const configuredUrl = process.env["MEND_URL"] ?? fileConfig.url ?? null;
@@ -4843,7 +5492,7 @@ var loadConfig = () => {
4843
5492
  deviceId: fileConfig.deviceId ?? null
4844
5493
  };
4845
5494
  };
4846
- var fail4 = (message) => {
5495
+ var fail5 = (message) => {
4847
5496
  process.stderr.write(`mend: ${message}
4848
5497
  `);
4849
5498
  process.exit(1);
@@ -4852,18 +5501,18 @@ var parseMendUrl = (value) => {
4852
5501
  try {
4853
5502
  return new URL(value);
4854
5503
  } catch {
4855
- return fail4(`"${value}" is not a valid Mend URL`);
5504
+ return fail5(`"${value}" is not a valid Mend URL`);
4856
5505
  }
4857
5506
  };
4858
- var paint3 = (code) => (text) => process.stdout.isTTY ? `\x1B[${code}m${text}\x1B[0m` : text;
4859
- var dim4 = paint3("2");
4860
- var green4 = paint3("32");
4861
- var amber = paint3("33");
4862
- var cobalt2 = paint3("34");
4863
- var say4 = (line) => process.stdout.write(`${line}
5507
+ var paint4 = (code) => (text) => process.stdout.isTTY ? `\x1B[${code}m${text}\x1B[0m` : text;
5508
+ var dim5 = paint4("2");
5509
+ var green5 = paint4("32");
5510
+ var amber = paint4("33");
5511
+ var cobalt2 = paint4("34");
5512
+ var say5 = (line) => process.stdout.write(`${line}
4864
5513
  `);
4865
5514
  var detachKeyEnabled = process.env["MEND_DETACH_KEY"] !== "none";
4866
- var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${dim4("Ctrl+]")}` : "";
5515
+ var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${dim5("Ctrl+]")}` : "";
4867
5516
  var herdrAgentOf = (harness) => {
4868
5517
  if (harness === "codex" || harness === "claude" || harness === "opencode") return harness;
4869
5518
  return null;
@@ -4931,9 +5580,42 @@ var api = async (config, method, route, body) => {
4931
5580
  try {
4932
5581
  return await request(config, method, route, body);
4933
5582
  } catch (error) {
4934
- return fail4(error instanceof Error ? error.message : String(error));
5583
+ return fail5(error instanceof Error ? error.message : String(error));
4935
5584
  }
4936
5585
  };
5586
+ var warnedLegacyBearer = false;
5587
+ var mintTicket = (config) => async (target, params) => {
5588
+ const headers = { "content-type": "application/json" };
5589
+ if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
5590
+ const response = await fetch(`${config.url}/api/upgrade-tickets`, {
5591
+ method: "POST",
5592
+ headers,
5593
+ body: JSON.stringify({ target, ...params })
5594
+ });
5595
+ if (response.status === 404) {
5596
+ if (await mintRefusedInTransit(config.url)) throw new Error(MINT_REFUSED_IN_TRANSIT);
5597
+ if (config.token !== null && !warnedLegacyBearer) {
5598
+ warnedLegacyBearer = true;
5599
+ console.error(
5600
+ "mend: this server predates upgrade tickets, so the saved token rides the socket's URL. Upgrade the server to stop that."
5601
+ );
5602
+ }
5603
+ return { kind: "unsupported" };
5604
+ }
5605
+ if (!response.ok) throw new Error(`upgrade ticket refused (${response.status})`);
5606
+ const minted = await response.json();
5607
+ if (typeof minted.ticket !== "string")
5608
+ throw new Error("upgrade ticket missing from the answer");
5609
+ return { kind: "ticket", ticket: minted.ticket };
5610
+ };
5611
+ var socketUrl = (config, target, params, extra) => upgradeUrl({
5612
+ serverUrl: parseMendUrl(config.url).toString(),
5613
+ target,
5614
+ params,
5615
+ ...extra === void 0 ? {} : { extra },
5616
+ mint: mintTicket(config),
5617
+ legacyToken: config.token
5618
+ });
4937
5619
  var boundApi = (config) => (method, route, body) => api(config, method, route, body);
4938
5620
  var withSpinner = async (label, work) => {
4939
5621
  if (process.stdout.isTTY !== true) return work;
@@ -4942,7 +5624,7 @@ var withSpinner = async (label, work) => {
4942
5624
  let frame = 0;
4943
5625
  const timer = setInterval(() => {
4944
5626
  const seconds = Math.round((Date.now() - started) / 1e3);
4945
- process.stdout.write(`\r ${frames[frame % frames.length]} ${label} ${dim4(`${seconds}s`)} `);
5627
+ process.stdout.write(`\r ${frames[frame % frames.length]} ${label} ${dim5(`${seconds}s`)} `);
4946
5628
  frame += 1;
4947
5629
  }, 120);
4948
5630
  try {
@@ -4956,66 +5638,74 @@ var adopt = async (config, args) => {
4956
5638
  const nameFlag = args.indexOf("--name");
4957
5639
  const name = nameFlag !== -1 && args[nameFlag + 1] !== void 0 ? String(args[nameFlag + 1]) : null;
4958
5640
  const authFlagIndex = args.indexOf("--auth");
5641
+ if (args.includes("--private") && args.includes("--shared")) {
5642
+ return fail5("--private and --shared pick one visibility; pass one of them");
5643
+ }
5644
+ const visibility = args.includes("--shared") ? "shared" : "private";
4959
5645
  const positional = args.filter(
4960
5646
  (a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1) && (authFlagIndex === -1 || i !== authFlagIndex + 1)
4961
5647
  );
4962
5648
  const source = positional[0] ?? gitOriginUrl(process.cwd());
4963
5649
  if (source === null) {
4964
- return fail4(
5650
+ return fail5(
4965
5651
  "No Git URL was given and this repository has no origin. Run mend adopt <git-url>."
4966
5652
  );
4967
5653
  }
4968
5654
  const sourceIssue = repositoryCloneUrlIssue(source);
4969
- if (sourceIssue !== null) return fail4(sourceIssue);
4970
- const projectName = name ?? normalizeProjectName(path10.basename(source, ".git"));
5655
+ if (sourceIssue !== null) return fail5(sourceIssue);
5656
+ const projectName = name ?? normalizeProjectName(path12.basename(source, ".git"));
4971
5657
  const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
4972
5658
  if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
4973
- return fail4(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
5659
+ return fail5(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
4974
5660
  }
4975
5661
  const project2 = await api(config, "POST", "/projects", {
4976
5662
  name: projectName,
4977
5663
  source,
5664
+ visibility,
4978
5665
  ...auth === null ? {} : { gitAuthMode: auth }
4979
5666
  });
4980
- say4(`${green4("\u2713")} adopted \xB7 ${project2.name} \xB7 ${dim4(project2.storePath)}`);
4981
- say4(`${dim4(" default branch")} ${project2.defaultBranch}`);
5667
+ say5(`${green5("\u2713")} adopted \xB7 ${project2.name} \xB7 ${dim5(project2.storePath)}`);
5668
+ say5(`${dim5(" default branch")} ${project2.defaultBranch}`);
5669
+ say5(
5670
+ `${dim5(" visible to")} ${visibility === "private" ? "only you" : "everyone in the organization"}`
5671
+ );
4982
5672
  if (project2.gitAuthMode === "mend-key") {
4983
- say4(`${dim4(" git auth")} mend key ${dim4("(your Mend key signed this clone)")}`);
5673
+ say5(`${dim5(" git auth")} mend key ${dim5("(your Mend key signed this clone)")}`);
4984
5674
  } else if (project2.gitAuthMode === "bridge") {
4985
- say4(`${dim4(" git auth")} bridge ${dim4("(signed through the connected `mend keys share`)")}`);
5675
+ say5(`${dim5(" git auth")} bridge ${dim5("(signed through the connected `mend keys share`)")}`);
4986
5676
  }
4987
- say4(
4988
- `${dim4(" sessions start with:")} mend codex ${dim4("(from anywhere \u2014")} --project ${project2.name}${dim4(")")}`
5677
+ say5(
5678
+ `${dim5(" sessions start with:")} mend codex ${dim5("(from anywhere \u2014")} --project ${project2.name}${dim5(")")}`
4989
5679
  );
4990
5680
  };
4991
5681
  var findProject = async (config, explicit, adoptCwd = false) => {
4992
5682
  const projects = await api(config, "GET", "/projects");
4993
5683
  if (explicit !== null) {
4994
5684
  const named = projects.find((p) => p.name === explicit);
4995
- if (named === void 0) return fail4(`no adopted project named "${explicit}"`);
5685
+ if (named === void 0) return fail5(`no adopted project named "${explicit}"`);
4996
5686
  return named;
4997
5687
  }
4998
5688
  const cwd = process.cwd();
4999
5689
  const project2 = matchProjectByCwd(projects, cwdFacts(cwd));
5000
5690
  if (project2 !== void 0) return project2;
5001
5691
  if (!adoptCwd) {
5002
- return fail4(
5692
+ return fail5(
5003
5693
  `no adopted project matches ${cwd} \u2014 run "mend adopt" here first, or name one with --project`
5004
5694
  );
5005
5695
  }
5006
5696
  const facts = cwdFacts(cwd);
5007
5697
  if (facts.repoRoot === null || facts.originUrl === null) {
5008
- return fail4(
5698
+ return fail5(
5009
5699
  `${cwd} does not match an adopted project with a Git origin \u2014 run "mend adopt <git-url>" first`
5010
5700
  );
5011
5701
  }
5012
5702
  const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
5013
- if (sourceIssue !== null) return fail4(sourceIssue);
5703
+ if (sourceIssue !== null) return fail5(sourceIssue);
5014
5704
  const adopted = await api(config, "POST", "/projects", {
5015
- name: normalizeProjectName(path10.basename(facts.repoRoot)),
5705
+ name: normalizeProjectName(path12.basename(facts.repoRoot)),
5016
5706
  source: facts.originUrl
5017
5707
  });
5018
- say4(`${green4("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim4(adopted.storePath)}`);
5708
+ say5(`${green5("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim5(adopted.storePath)}`);
5019
5709
  return adopted;
5020
5710
  };
5021
5711
  var resolvedBackgroundSessions = async (config, project2) => {
@@ -5037,7 +5727,7 @@ var askWorktreeName = async () => {
5037
5727
  const readline = await import("node:readline/promises");
5038
5728
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5039
5729
  try {
5040
- const answer = (await rl.question(` worktree name ${dim4("(enter for auto)")}: `)).trim();
5730
+ const answer = (await rl.question(` worktree name ${dim5("(enter for auto)")}: `)).trim();
5041
5731
  return answer === "" ? null : normalizeProjectName(answer);
5042
5732
  } finally {
5043
5733
  rl.close();
@@ -5045,35 +5735,35 @@ var askWorktreeName = async () => {
5045
5735
  };
5046
5736
  var launch = async (config, harness, args) => {
5047
5737
  const parsed = parseLaunchArgs(args);
5048
- if (parsed.error !== null) return fail4(parsed.error);
5738
+ if (parsed.error !== null) return fail5(parsed.error);
5049
5739
  const structured = parsed.prompt !== null || parsed.model !== null || parsed.effort !== null || parsed.ask || parsed.fast;
5050
5740
  if (harness === "run" && structured) {
5051
- return fail4(`mend run takes no prompt or harness flags \xB7 ${usageOf("run")}`);
5741
+ return fail5(`mend run takes no prompt or harness flags \xB7 ${usageOf("run")}`);
5052
5742
  }
5053
5743
  if (harness === "run" && (parsed.detach || parsed.foreground)) {
5054
- return fail4(
5744
+ return fail5(
5055
5745
  "mend run takes no lifecycle flags \u2014 it tails the record; Ctrl+C stops watching, not the command"
5056
5746
  );
5057
5747
  }
5058
5748
  const argv = harness === "run" ? parsed.custom : HARNESS_COMMANDS[harness] ?? [];
5059
5749
  if (argv.length === 0) {
5060
- return fail4(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
5750
+ return fail5(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
5061
5751
  }
5062
5752
  const project2 = await findProject(config, parsed.project, true);
5063
- say4(
5064
- `${green4("\u2713")} project ${project2.name} ${dim4(`\xB7 ${project2.defaultBranch}${parsed.project === null ? " \xB7 from cwd" : ""}`)}`
5753
+ say5(
5754
+ `${green5("\u2713")} project ${project2.name} ${dim5(`\xB7 ${project2.defaultBranch}${parsed.project === null ? " \xB7 from cwd" : ""}`)}`
5065
5755
  );
5066
5756
  let worktreeName;
5067
5757
  if (parsed.worktree !== null) {
5068
5758
  const detail = await api(config, "GET", `/projects/${project2.id}`);
5069
5759
  const existing = detail.worktrees ?? [];
5070
5760
  if (detail.worktrees === void 0) {
5071
- return fail4("this server predates shared worktrees \u2014 use --name instead");
5761
+ return fail5("this server predates shared worktrees \u2014 use --name instead");
5072
5762
  }
5073
5763
  const match = existing.find((worktree) => worktree.name === parsed.worktree) ?? (existing.filter((worktree) => worktree.name.startsWith(parsed.worktree ?? "")).length === 1 ? existing.find((worktree) => worktree.name.startsWith(parsed.worktree ?? "")) : void 0);
5074
5764
  if (match === void 0) {
5075
5765
  const names = existing.map((worktree) => worktree.name).join(", ");
5076
- return fail4(
5766
+ return fail5(
5077
5767
  `no worktree matches "${parsed.worktree}" in ${project2.name}${names === "" ? "" : ` \u2014 have: ${names}`}`
5078
5768
  );
5079
5769
  }
@@ -5090,8 +5780,8 @@ var launch = async (config, harness, args) => {
5090
5780
  const members = (detail?.sessions ?? []).filter(
5091
5781
  (candidate) => candidate.worktreeId === joined.id
5092
5782
  ).length;
5093
- say4(
5094
- `${green4("\u2713")} joins worktree ${joined.name} ${dim4(`\xB7 ${members} session${members === 1 ? "" : "s"} \xB7 branch ${joined.branch}`)}`
5783
+ say5(
5784
+ `${green5("\u2713")} joins worktree ${joined.name} ${dim5(`\xB7 ${members} session${members === 1 ? "" : "s"} \xB7 branch ${joined.branch}`)}`
5095
5785
  );
5096
5786
  }
5097
5787
  }
@@ -5116,12 +5806,12 @@ var launch = async (config, harness, args) => {
5116
5806
  base: parsed.base
5117
5807
  });
5118
5808
  createdSessionId = session.id;
5119
- say4(`${green4("\u2713")} worktree ${session.worktree} ${dim4(`\xB7 branch ${session.branch}`)}`);
5809
+ say5(`${green5("\u2713")} worktree ${session.worktree} ${dim5(`\xB7 branch ${session.branch}`)}`);
5120
5810
  const baseWord = session.baseRef === null ? "" : `${session.baseRef} `;
5121
- say4(
5122
- `${green4("\u2713")} base ${baseWord}${dim4(session.baseSha.slice(0, 12))} \xB7 session ${dim4(session.id.slice(0, 8))}`
5811
+ say5(
5812
+ `${green5("\u2713")} base ${baseWord}${dim5(session.baseSha.slice(0, 12))} \xB7 session ${dim5(session.id.slice(0, 8))}`
5123
5813
  );
5124
- say4(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
5814
+ say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
5125
5815
  if (harness === "run") {
5126
5816
  return supervisedRun(config, session, argv);
5127
5817
  }
@@ -5137,23 +5827,28 @@ var launch = async (config, harness, args) => {
5137
5827
  api(config, "POST", `/sessions/${session.id}/launch`, launchBody)
5138
5828
  );
5139
5829
  if (lifecycle === "detach") {
5140
- say4(`${green4("\u2713 recording")} \xB7 running detached`);
5141
- say4(`${cobalt2(" attach")} \xB7 mend attach ${session.id.slice(0, 8)}`);
5830
+ say5(`${green5("\u2713 recording")} \xB7 running detached`);
5831
+ say5(`${cobalt2(" attach")} \xB7 mend attach ${session.id.slice(0, 8)}`);
5142
5832
  return;
5143
5833
  }
5144
- say4(`${green4("\u2713 recording")} \xB7 workspace mounts the worktree${detachHint()}`);
5145
- say4("");
5834
+ say5(`${green5("\u2713 recording")} \xB7 workspace mounts the worktree${detachHint()}`);
5835
+ say5("");
5146
5836
  attachOwnsSignals = true;
5147
5837
  await attachOrExit(config, session.id, session.harness, lifecycle);
5148
5838
  exitAfterSessionEnd(config, session.id);
5149
5839
  };
5150
5840
  var attachTty = async (config, sessionId, harness, from, processId, options) => {
5151
- const url = parseMendUrl(`${config.url}/api/tty`);
5152
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
5153
- if (processId === void 0) url.searchParams.set("session", sessionId);
5154
- else url.searchParams.set("process", processId);
5155
- url.searchParams.set("from", from.toString());
5156
- if (config.token !== null) url.searchParams.set("token", config.token);
5841
+ let url;
5842
+ try {
5843
+ url = await socketUrl(
5844
+ config,
5845
+ "tty",
5846
+ processId === void 0 ? { session: sessionId } : { process: processId },
5847
+ { from: from.toString() }
5848
+ );
5849
+ } catch {
5850
+ return "unavailable";
5851
+ }
5157
5852
  const ws = new WebSocket(url);
5158
5853
  ws.binaryType = "arraybuffer";
5159
5854
  try {
@@ -5297,44 +5992,44 @@ var stopSessionQuickly = async (config, sessionId) => {
5297
5992
  var stopAndExit = async (config, sessionId) => {
5298
5993
  const id8 = sessionId.slice(0, 8);
5299
5994
  if (await stopSessionQuickly(config, sessionId)) {
5300
- say4(`${green4("\u2713")} stopped \xB7 ${id8}`);
5301
- say4(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
5995
+ say5(`${green5("\u2713")} stopped \xB7 ${id8}`);
5996
+ say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
5302
5997
  process.exit(0);
5303
5998
  }
5304
- say4(`${amber("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
5999
+ say5(`${amber("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
5305
6000
  process.exit(1);
5306
6001
  };
5307
6002
  var finishAttach = async (config, sessionId, outcome, mode = "background") => {
5308
6003
  const id8 = sessionId.slice(0, 8);
5309
6004
  if (outcome === "unavailable") {
5310
- return fail4(`tty attach unavailable: could not connect to ${config.url}`);
6005
+ return fail5(`tty attach unavailable: could not connect to ${config.url}`);
5311
6006
  }
5312
6007
  if (outcome === "detached") {
5313
- say4("");
5314
- say4(`${amber("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
6008
+ say5("");
6009
+ say5(`${amber("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
5315
6010
  process.exit(0);
5316
6011
  }
5317
6012
  if (outcome === "interrupted") {
5318
- say4("");
6013
+ say5("");
5319
6014
  if (mode === "foreground") return stopAndExit(config, sessionId);
5320
- say4(`${amber("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
6015
+ say5(`${amber("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
5321
6016
  process.exit(0);
5322
6017
  }
5323
6018
  if (outcome === "dropped") {
5324
- say4("");
6019
+ say5("");
5325
6020
  if (mode === "foreground") {
5326
6021
  let live;
5327
6022
  try {
5328
6023
  const detail = await request(config, "GET", `/sessions/${sessionId}`);
5329
6024
  live = agentIsLive(detail.session, detail.currentAgent);
5330
6025
  } catch {
5331
- say4(`${amber("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
6026
+ say5(`${amber("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
5332
6027
  process.exit(1);
5333
6028
  }
5334
6029
  if (!live) return;
5335
6030
  return stopAndExit(config, sessionId);
5336
6031
  }
5337
- say4(`${amber("disconnected")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
6032
+ say5(`${amber("disconnected")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
5338
6033
  process.exit(0);
5339
6034
  }
5340
6035
  };
@@ -5345,9 +6040,9 @@ var attachOrExit = async (config, sessionId, harness, mode = "background") => fi
5345
6040
  mode
5346
6041
  );
5347
6042
  var exitAfterSessionEnd = (config, sessionId) => {
5348
- say4("");
5349
- say4(`${green4("\u2713")} session ended`);
5350
- say4(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
6043
+ say5("");
6044
+ say5(`${green5("\u2713")} session ended`);
6045
+ say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
5351
6046
  process.exit(0);
5352
6047
  };
5353
6048
  var attach = async (config, args) => {
@@ -5358,26 +6053,26 @@ var attach = async (config, args) => {
5358
6053
  }
5359
6054
  const sessions = await api(config, "GET", "/sessions");
5360
6055
  const match = sessions.find((s) => s.id.startsWith(prefix));
5361
- if (match === void 0) return fail4(`no active session matches "${prefix}"`);
6056
+ if (match === void 0) return fail5(`no active session matches "${prefix}"`);
5362
6057
  return attachPicked(config, match);
5363
6058
  };
5364
6059
  var attachPicked = async (config, session) => {
5365
- say4(
5366
- `${green4("\u2713")} attaching to ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim4(session.id.slice(0, 8))}${detachHint()}`
6060
+ say5(
6061
+ `${green5("\u2713")} attaching to ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim5(session.id.slice(0, 8))}${detachHint()}`
5367
6062
  );
5368
- say4("");
6063
+ say5("");
5369
6064
  const outcome = await attachTty(config, session.id, session.harness, 0n, void 0, {
5370
6065
  handleSignals: true
5371
6066
  });
5372
6067
  if (outcome === "unavailable") {
5373
6068
  const detail = await api(config, "GET", `/sessions/${session.id}`);
5374
6069
  if (detail.currentAgent?.kind === "agent-protocol" && agentIsLive(detail.session, detail.currentAgent)) {
5375
- say4(`${amber("taking over")} from the protocol session`);
6070
+ say5(`${amber("taking over")} from the protocol session`);
5376
6071
  await withSpinner(
5377
6072
  "reopening as a terminal \u2014 same conversation\u2026",
5378
6073
  api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
5379
6074
  );
5380
- say4("");
6075
+ say5("");
5381
6076
  await attachOrExit(config, session.id, session.harness);
5382
6077
  return exitAfterSessionEnd(config, session.id);
5383
6078
  }
@@ -5396,7 +6091,7 @@ var stopCommand = async (config, args) => {
5396
6091
  return;
5397
6092
  }
5398
6093
  if (!all && prefix === void 0) {
5399
- return fail4(usageOf("stop"));
6094
+ return fail5(usageOf("stop"));
5400
6095
  }
5401
6096
  const [sessions, projects] = await Promise.all([
5402
6097
  api(config, "GET", "/sessions"),
@@ -5405,21 +6100,21 @@ var stopCommand = async (config, args) => {
5405
6100
  let scoped = sessions;
5406
6101
  if (projectName !== null) {
5407
6102
  const project2 = projects.find((p) => p.name === projectName);
5408
- if (project2 === void 0) return fail4(`no project named "${projectName}"`);
6103
+ if (project2 === void 0) return fail5(`no project named "${projectName}"`);
5409
6104
  scoped = sessions.filter((s) => s.projectId === project2.id);
5410
6105
  }
5411
6106
  let targets;
5412
6107
  if (all) {
5413
6108
  targets = scoped;
5414
6109
  if (targets.length === 0) {
5415
- say4("no active sessions");
6110
+ say5("no active sessions");
5416
6111
  return;
5417
6112
  }
5418
6113
  } else {
5419
6114
  const matches = scoped.filter((s) => s.id.startsWith(prefix ?? ""));
5420
- if (matches.length === 0) return fail4(`no active session matches "${prefix}"`);
6115
+ if (matches.length === 0) return fail5(`no active session matches "${prefix}"`);
5421
6116
  if (matches.length > 1) {
5422
- return fail4(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
6117
+ return fail5(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
5423
6118
  }
5424
6119
  targets = matches;
5425
6120
  }
@@ -5428,20 +6123,20 @@ var stopCommand = async (config, args) => {
5428
6123
  var stopSessions = async (config, targets, summarise) => {
5429
6124
  for (const session of targets) {
5430
6125
  await api(config, "POST", `/sessions/${session.id}/stop`);
5431
- say4(
5432
- `${green4("\u2713")} stopped \xB7 ${session.harness} \xB7 ${dim4(session.id.slice(0, 8))} \xB7 ${session.branch}`
6126
+ say5(
6127
+ `${green5("\u2713")} stopped \xB7 ${session.harness} \xB7 ${dim5(session.id.slice(0, 8))} \xB7 ${session.branch}`
5433
6128
  );
5434
- say4(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
6129
+ say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
5435
6130
  }
5436
6131
  if (summarise) {
5437
- say4(`${green4("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
6132
+ say5(`${green5("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
5438
6133
  }
5439
6134
  };
5440
6135
  var pickSessionInteractively = async (rows) => {
5441
- say4(dim4("more than one live session \u2014 pick one:"));
6136
+ say5(dim5("more than one live session \u2014 pick one:"));
5442
6137
  rows.forEach((row, index) => {
5443
- say4(
5444
- ` ${index + 1}. ${row.session.harness.padEnd(8)} ${dim4(row.session.id.slice(0, 8))} ${row.projectName} ${dim4(row.session.branch)} ${dim4(row.session.status)}`
6138
+ say5(
6139
+ ` ${index + 1}. ${row.session.harness.padEnd(8)} ${dim5(row.session.id.slice(0, 8))} ${row.projectName} ${dim5(row.session.branch)} ${dim5(row.session.status)}`
5445
6140
  );
5446
6141
  });
5447
6142
  const readline = await import("node:readline/promises");
@@ -5449,17 +6144,17 @@ var pickSessionInteractively = async (rows) => {
5449
6144
  const answer = await rl.question(" session #: ");
5450
6145
  rl.close();
5451
6146
  const chosen = rows[Number(answer.trim()) - 1];
5452
- if (chosen === void 0) return fail4(`"${answer.trim()}" is not one of the choices`);
6147
+ if (chosen === void 0) return fail5(`"${answer.trim()}" is not one of the choices`);
5453
6148
  return chosen.session;
5454
6149
  };
5455
6150
  var resolveLiveSession = async (config, prefix, command) => {
5456
6151
  const sessions = await api(config, "GET", "/sessions?retained=1");
5457
6152
  if (prefix !== void 0) {
5458
6153
  const matches = sessions.filter((s) => s.id.startsWith(prefix));
5459
- if (matches.length === 0) return fail4(`no live or retained session matches "${prefix}"`);
6154
+ if (matches.length === 0) return fail5(`no live or retained session matches "${prefix}"`);
5460
6155
  const exact = matches[0];
5461
6156
  if (matches.length > 1 || exact === void 0) {
5462
- return fail4(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
6157
+ return fail5(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
5463
6158
  }
5464
6159
  return exact;
5465
6160
  }
@@ -5468,11 +6163,11 @@ var resolveLiveSession = async (config, prefix, command) => {
5468
6163
  const candidates = project2 === void 0 ? sessions : sessions.filter((s) => s.projectId === project2.id);
5469
6164
  const only = candidates[0];
5470
6165
  if (only === void 0) {
5471
- return fail4("no live or retained session \u2014 start one with mend codex|claude|opencode");
6166
+ return fail5("no live or retained session \u2014 start one with mend codex|claude|opencode");
5472
6167
  }
5473
6168
  if (candidates.length === 1) return only;
5474
6169
  if (process.stdin.isTTY !== true) {
5475
- return fail4(`several live sessions \u2014 name one: mend ${command} <session-id-prefix>`);
6170
+ return fail5(`several live sessions \u2014 name one: mend ${command} <session-id-prefix>`);
5476
6171
  }
5477
6172
  const nameById = new Map(projects.map((p) => [p.id, p.name]));
5478
6173
  return pickSessionInteractively(
@@ -5485,26 +6180,26 @@ var resolveLiveSession = async (config, prefix, command) => {
5485
6180
  var shellCommand = async (config, args) => {
5486
6181
  const prefix = args.find((a) => !a.startsWith("--"));
5487
6182
  const session = await resolveLiveSession(config, prefix, "shell");
5488
- say4(
5489
- `${green4("\u2713")} shell in ${session.harness} session ${dim4(session.id.slice(0, 8))} \xB7 ${dim4(session.branch)}${detachHint()}`
6183
+ say5(
6184
+ `${green5("\u2713")} shell in ${session.harness} session ${dim5(session.id.slice(0, 8))} \xB7 ${dim5(session.branch)}${detachHint()}`
5490
6185
  );
5491
6186
  const shellProcess = await withSpinner(
5492
6187
  "opening a shell in the session workspace\u2026",
5493
6188
  api(config, "POST", `/sessions/${session.id}/shell`)
5494
6189
  );
5495
- say4("");
6190
+ say5("");
5496
6191
  const outcome = await attachTty(config, session.id, "shell", 0n, shellProcess.id, {
5497
6192
  handleSignals: true
5498
6193
  });
5499
6194
  if (outcome === "unavailable") {
5500
- return fail4(`tty attach unavailable: could not connect to ${config.url}`);
6195
+ return fail5(`tty attach unavailable: could not connect to ${config.url}`);
5501
6196
  }
5502
- say4("");
6197
+ say5("");
5503
6198
  if (outcome === "detached" || outcome === "interrupted" || outcome === "dropped") {
5504
- say4(`${amber("detached")} \u2014 the shell keeps running and holds the workspace open`);
6199
+ say5(`${amber("detached")} \u2014 the shell keeps running and holds the workspace open`);
5505
6200
  process.exit(0);
5506
6201
  }
5507
- say4(`${green4("\u2713")} shell ended`);
6202
+ say5(`${green5("\u2713")} shell ended`);
5508
6203
  process.exit(0);
5509
6204
  };
5510
6205
  var flattenService = (view) => {
@@ -5545,30 +6240,30 @@ var serviceUrl = (service) => service.browserUrl ?? (service.authority === null
5545
6240
  var printServiceAccess = (config, service, tunneling = false) => {
5546
6241
  const gate = "no Mend sign-in on this port \u2014 network reach is the only gate";
5547
6242
  if (serverIsLocal(config)) {
5548
- if (service.authority !== null) say4(` ${cobalt2(serviceUrl(service))} ${dim4(gate)}`);
6243
+ if (service.authority !== null) say5(` ${cobalt2(serviceUrl(service))} ${dim5(gate)}`);
5549
6244
  return;
5550
6245
  }
5551
6246
  if (service.protocol === "udp") {
5552
6247
  if (service.authority !== null) {
5553
- say4(` ${cobalt2(serviceUrl(service))} ${dim4(`on the server's network \xB7 ${gate}`)}`);
6248
+ say5(` ${cobalt2(serviceUrl(service))} ${dim5(`on the server's network \xB7 ${gate}`)}`);
5554
6249
  }
5555
6250
  return;
5556
6251
  }
5557
6252
  if (!tunneling) {
5558
6253
  const local = service.hostPort ?? service.workspacePort;
5559
- say4(
5560
- ` ${cobalt2(`mend service connect ${service.label}`)} ${dim4(`\u2192 127.0.0.1:${local} on this machine, authenticated as you`)}`
6254
+ say5(
6255
+ ` ${cobalt2(`mend service connect ${service.label}`)} ${dim5(`\u2192 127.0.0.1:${local} on this machine, authenticated as you`)}`
5561
6256
  );
5562
6257
  }
5563
6258
  if (service.authority !== null) {
5564
- say4(` ${dim4(`server-side listener ${service.authority} \xB7 ${gate}`)}`);
6259
+ say5(` ${dim5(`server-side listener ${service.authority} \xB7 ${gate}`)}`);
5565
6260
  }
5566
6261
  };
5567
6262
  var printWorkspaceTtlFailure = (service) => {
5568
6263
  if (service.workspaceTtlRenewalError === null) return;
5569
- say4(amber(` workspace TTL renewal failed \xB7 ${service.workspaceTtlRenewalError}`));
5570
- say4(
5571
- dim4(
6264
+ say5(amber(` workspace TTL renewal failed \xB7 ${service.workspaceTtlRenewalError}`));
6265
+ say5(
6266
+ dim5(
5572
6267
  ` last renewed ${service.workspaceTtlRenewedAt ?? "unknown"} \xB7 known expiry ${service.workspaceExpiresAt ?? "unknown"} \xB7 failed ${service.workspaceTtlRenewalFailedAt ?? "unknown"}`
5573
6268
  )
5574
6269
  );
@@ -5578,11 +6273,11 @@ var printServiceEndpoint = (config, service, tunneling = false) => {
5578
6273
  printWorkspaceTtlFailure(service);
5579
6274
  };
5580
6275
  var printService = (config, service) => {
5581
- const status = service.status === "reachable" ? green4(service.status) : amber(service.status);
6276
+ const status = service.status === "reachable" ? green5(service.status) : amber(service.status);
5582
6277
  const port = `:${service.workspacePort ?? "?"}${service.protocol === "udp" ? "/udp" : ""}`;
5583
6278
  const statusPad = " ".repeat(Math.max(1, 12 - service.status.length));
5584
- say4(
5585
- `${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${dim4(port.padEnd(7))} ${dim4(service.id.slice(0, 8))}`
6279
+ say5(
6280
+ `${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${dim5(port.padEnd(7))} ${dim5(service.id.slice(0, 8))}`
5586
6281
  );
5587
6282
  printServiceAccess(config, service);
5588
6283
  printWorkspaceTtlFailure(service);
@@ -5593,21 +6288,21 @@ var serviceAdd = async (config, args) => {
5593
6288
  const protocol = args.includes("--udp") ? "udp" : "tcp";
5594
6289
  const http = args.includes("--http");
5595
6290
  const https = args.includes("--https");
5596
- if (http && https) return fail4("Choose either --http or --https, not both.");
6291
+ if (http && https) return fail5("Choose either --http or --https, not both.");
5597
6292
  const browserScheme = https ? "https" : http ? "http" : null;
5598
6293
  if (protocol === "udp" && browserScheme !== null) {
5599
- return fail4("UDP Services cannot use --http or --https");
6294
+ return fail5("UDP Services cannot use --http or --https");
5600
6295
  }
5601
6296
  const positional = args.filter(
5602
6297
  (a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1)
5603
6298
  );
5604
6299
  const portRaw = positional.find((a) => /^\d+$/.test(a));
5605
6300
  if (portRaw === void 0) {
5606
- return fail4(usageOf("service add"));
6301
+ return fail5(usageOf("service add"));
5607
6302
  }
5608
6303
  const port = Number(portRaw);
5609
6304
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
5610
- return fail4(`"${portRaw}" is not a port`);
6305
+ return fail5(`"${portRaw}" is not a port`);
5611
6306
  }
5612
6307
  const prefix = positional.find((a) => a !== portRaw);
5613
6308
  const session = await resolveLiveSession(config, prefix, "service add");
@@ -5617,52 +6312,52 @@ var serviceAdd = async (config, args) => {
5617
6312
  protocol,
5618
6313
  browserScheme
5619
6314
  });
5620
- say4(`${green4("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
6315
+ say5(`${green5("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
5621
6316
  printServiceEndpoint(config, service);
5622
6317
  if (protocol === "udp") {
5623
- say4(dim4(` udp \u2014 a reply is the only reachability signal; silence just relays`));
6318
+ say5(dim5(` udp \u2014 a reply is the only reachability signal; silence just relays`));
5624
6319
  } else if (service.status !== "reachable") {
5625
- say4(dim4(` nothing answered on :${port} yet \u2014 the URL goes live when something listens`));
6320
+ say5(dim5(` nothing answered on :${port} yet \u2014 the URL goes live when something listens`));
5626
6321
  }
5627
6322
  };
5628
6323
  var serviceList = async (config) => {
5629
6324
  const services = await fetchServices(config);
5630
6325
  if (services.length === 0) {
5631
- say4(dim4("no live services \u2014 mend service add <port> adopts a listening one"));
6326
+ say5(dim5("no live services \u2014 mend service add <port> adopts a listening one"));
5632
6327
  return;
5633
6328
  }
5634
6329
  for (const service of services) printService(config, service);
5635
6330
  };
5636
6331
  var serviceStop = async (config, args) => {
5637
6332
  const needle = args.find((a) => !a.startsWith("--"));
5638
- if (needle === void 0) return fail4(usageOf("service stop"));
6333
+ if (needle === void 0) return fail5(usageOf("service stop"));
5639
6334
  const services = await fetchServices(config);
5640
6335
  const matches = services.filter(
5641
6336
  (service) => service.label === needle || service.id.startsWith(needle)
5642
6337
  );
5643
- if (matches.length === 0) return fail4(`no live service matches "${needle}"`);
6338
+ if (matches.length === 0) return fail5(`no live service matches "${needle}"`);
5644
6339
  const match = matches[0];
5645
6340
  if (matches.length > 1 || match === void 0) {
5646
- return fail4(`"${needle}" is ambiguous \u2014 use more of the id`);
6341
+ return fail5(`"${needle}" is ambiguous \u2014 use more of the id`);
5647
6342
  }
5648
6343
  const stoppedService = await mutateService(config, "POST", `/services/${match.id}/stop`);
5649
- say4(`${green4("\u2713")} stopped \xB7 ${stoppedService.label ?? stoppedService.id.slice(0, 8)}`);
6344
+ say5(`${green5("\u2713")} stopped \xB7 ${stoppedService.label ?? stoppedService.id.slice(0, 8)}`);
5650
6345
  };
5651
6346
  var findLiveService = async (config, needle) => {
5652
6347
  const services = await fetchServices(config);
5653
6348
  const matches = services.filter(
5654
6349
  (service) => service.label === needle || service.id.startsWith(needle)
5655
6350
  );
5656
- if (matches.length === 0) return fail4(`no live service matches "${needle}"`);
6351
+ if (matches.length === 0) return fail5(`no live service matches "${needle}"`);
5657
6352
  const match = matches[0];
5658
6353
  if (matches.length > 1 || match === void 0) {
5659
- return fail4(`"${needle}" is ambiguous \u2014 use more of the id`);
6354
+ return fail5(`"${needle}" is ambiguous \u2014 use more of the id`);
5660
6355
  }
5661
6356
  return match;
5662
6357
  };
5663
6358
  var willAutoConnect = (config, service, optOut) => !optOut && !serverIsLocal(config) && service.protocol !== "udp";
5664
6359
  var autoConnect = async (config, service) => {
5665
- say4(dim4(" remote server \u2014 tunneling the port here \xB7 Ctrl-C stops the tunnel, not the Service"));
6360
+ say5(dim5(" remote server \u2014 tunneling the port here \xB7 Ctrl-C stops the tunnel, not the Service"));
5666
6361
  await tunnelServices(config, [service], null);
5667
6362
  };
5668
6363
  var serviceRun = async (config, args) => {
@@ -5671,7 +6366,7 @@ var serviceRun = async (config, args) => {
5671
6366
  if (dashdash === -1) {
5672
6367
  const positionals = args.filter((a) => !a.startsWith("--"));
5673
6368
  const name2 = positionals.at(-1);
5674
- if (name2 === void 0) return fail4(usage);
6369
+ if (name2 === void 0) return fail5(usage);
5675
6370
  const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
5676
6371
  const session2 = await resolveLiveSession(config, prefix2, "service run");
5677
6372
  const recipes = await api(
@@ -5682,7 +6377,7 @@ var serviceRun = async (config, args) => {
5682
6377
  const recipe = recipes.find((entry) => entry.name === name2);
5683
6378
  if (recipe === void 0) {
5684
6379
  const known = recipes.map((entry) => entry.name).join(", ");
5685
- return fail4(
6380
+ return fail5(
5686
6381
  recipes.length === 0 ? `no mend.toml recipes in this worktree \u2014 declare [service.${name2}] first` : `no recipe named "${name2}" \u2014 declared: ${known}`
5687
6382
  );
5688
6383
  }
@@ -5693,26 +6388,26 @@ var serviceRun = async (config, args) => {
5693
6388
  })
5694
6389
  );
5695
6390
  const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect"));
5696
- say4(`${green4("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
6391
+ say5(`${green5("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
5697
6392
  printServiceEndpoint(config, service2, tunneling2);
5698
- say4(dim4(` logs: mend service logs ${service2.label ?? service2.id.slice(0, 8)}`));
6393
+ say5(dim5(` logs: mend service logs ${service2.label ?? service2.id.slice(0, 8)}`));
5699
6394
  if (tunneling2) await autoConnect(config, service2);
5700
6395
  return;
5701
6396
  }
5702
6397
  const argv = args.slice(dashdash + 1);
5703
- if (argv.length === 0) return fail4(usage);
6398
+ if (argv.length === 0) return fail5(usage);
5704
6399
  const head = args.slice(0, dashdash);
5705
6400
  const portFlag = head.indexOf("--port");
5706
6401
  const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
5707
- if (!Number.isInteger(port) || port < 1 || port > 65535) return fail4(usage);
6402
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return fail5(usage);
5708
6403
  const nameFlag = head.indexOf("--name");
5709
6404
  const name = nameFlag !== -1 && head[nameFlag + 1] !== void 0 ? String(head[nameFlag + 1]) : null;
5710
6405
  const protocol = head.includes("--udp") ? "udp" : "tcp";
5711
6406
  const http = head.includes("--http");
5712
6407
  const https = head.includes("--https");
5713
- if (http && https) return fail4(usage);
6408
+ if (http && https) return fail5(usage);
5714
6409
  const browserScheme = https ? "https" : http ? "http" : null;
5715
- if (protocol === "udp" && browserScheme !== null) return fail4(usage);
6410
+ if (protocol === "udp" && browserScheme !== null) return fail5(usage);
5716
6411
  const prefix = head.find(
5717
6412
  (a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
5718
6413
  );
@@ -5728,9 +6423,9 @@ var serviceRun = async (config, args) => {
5728
6423
  })
5729
6424
  );
5730
6425
  const tunneling = willAutoConnect(config, service, head.includes("--no-connect"));
5731
- say4(`${green4("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
6426
+ say5(`${green5("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
5732
6427
  printServiceEndpoint(config, service, tunneling);
5733
- say4(dim4(` logs: mend service logs ${service.label ?? service.id.slice(0, 8)}`));
6428
+ say5(dim5(` logs: mend service logs ${service.label ?? service.id.slice(0, 8)}`));
5734
6429
  if (tunneling) await autoConnect(config, service);
5735
6430
  };
5736
6431
  var serviceLogs = async (config, args) => {
@@ -5740,32 +6435,32 @@ var serviceLogs = async (config, args) => {
5740
6435
  (argument, index) => !argument.startsWith("--") && index !== fromFlag + 1
5741
6436
  );
5742
6437
  if (needle === void 0 || from === void 0 || !/^(0|[1-9]\d*)$/.test(from)) {
5743
- return fail4(usageOf("service logs"));
6438
+ return fail5(usageOf("service logs"));
5744
6439
  }
5745
6440
  const everything = await fetchServiceViews(config, true);
5746
6441
  const matches = everything.filter(
5747
6442
  (view2) => view2.service.name === needle || view2.service.id.startsWith(needle) || view2.attempts.some((attempt3) => attempt3.id.startsWith(needle))
5748
6443
  );
5749
- if (matches.length === 0) return fail4(`no service matches "${needle}"`);
6444
+ if (matches.length === 0) return fail5(`no service matches "${needle}"`);
5750
6445
  const view = matches.find((candidate) => {
5751
6446
  const current = candidate.service.currentAttemptId === null ? null : candidate.attempts.find((attempt3) => attempt3.id === candidate.service.currentAttemptId);
5752
6447
  return current !== null && current !== void 0 && current.exitedAt === null;
5753
6448
  }) ?? matches[0];
5754
- if (view === void 0) return fail4(`no service matches "${needle}"`);
6449
+ if (view === void 0) return fail5(`no service matches "${needle}"`);
5755
6450
  const currentAttempt = view.service.currentAttemptId === null ? null : view.attempts.find((candidate) => candidate.id === view.service.currentAttemptId) ?? null;
5756
6451
  const attempt2 = view.attempts.find((candidate) => candidate.id.startsWith(needle)) ?? currentAttempt ?? view.attempts.findLast((candidate) => candidate.sealantSessionId !== null) ?? null;
5757
6452
  if (attempt2?.sealantSessionId === null || attempt2 === null) {
5758
- return fail4(
6453
+ return fail5(
5759
6454
  `"${needle}" is an adopted port \u2014 no process of Mend's, no logs. mend service run supervises.`
5760
6455
  );
5761
6456
  }
5762
6457
  const label = view.service.name;
5763
- say4(
5764
- dim4(
6458
+ say5(
6459
+ dim5(
5765
6460
  attempt2.exitedAt === null ? `following ${label} from sequence ${from} \u2014 Ctrl+C detaches, the Service keeps running` : `${label} \xB7 ${attempt2.status} \u2014 recorded output from sequence ${from}`
5766
6461
  )
5767
6462
  );
5768
- say4("");
6463
+ say5("");
5769
6464
  let cursor = from;
5770
6465
  let telemetryReported = false;
5771
6466
  for (; ; ) {
@@ -5775,7 +6470,7 @@ var serviceLogs = async (config, args) => {
5775
6470
  `/processes/${attempt2.id}/logs?from=${encodeURIComponent(cursor)}&limit=256`
5776
6471
  );
5777
6472
  if (!telemetryReported) {
5778
- say4(dim4(`telemetry loss: ${page.telemetryLoss} \xB7 ${page.telemetryNote}`));
6473
+ say5(dim5(`telemetry loss: ${page.telemetryLoss} \xB7 ${page.telemetryNote}`));
5779
6474
  telemetryReported = true;
5780
6475
  }
5781
6476
  for (const chunk of page.chunks) {
@@ -5787,29 +6482,29 @@ var serviceLogs = async (config, args) => {
5787
6482
  if (ended && !advanced) break;
5788
6483
  if (!advanced) await new Promise((resolve4) => setTimeout(resolve4, 250));
5789
6484
  }
5790
- say4("");
5791
- say4(dim4(`stream ended \xB7 next sequence ${cursor}`));
6485
+ say5("");
6486
+ say5(dim5(`stream ended \xB7 next sequence ${cursor}`));
5792
6487
  };
5793
6488
  var serviceRestart = async (config, args) => {
5794
6489
  const needle = args.find((a) => !a.startsWith("--"));
5795
- if (needle === void 0) return fail4(usageOf("service restart"));
6490
+ if (needle === void 0) return fail5(usageOf("service restart"));
5796
6491
  const service = await findLiveService(config, needle);
5797
6492
  const restarted = await withSpinner(
5798
6493
  `restarting ${service.label ?? service.id.slice(0, 8)}\u2026`,
5799
6494
  mutateService(config, "POST", `/services/${service.id}/restart`)
5800
6495
  );
5801
- say4(`${green4("\u2713")} restarted \xB7 ${restarted.status}`);
6496
+ say5(`${green5("\u2713")} restarted \xB7 ${restarted.status}`);
5802
6497
  printServiceEndpoint(config, restarted);
5803
6498
  };
5804
6499
  var serviceInit = async (args) => {
5805
6500
  const root = gitTopLevel(process.cwd()) ?? process.cwd();
5806
- const target = path10.join(root, "mend.toml");
5807
- if (fs9.existsSync(target)) {
5808
- return fail4(`${target} already exists \u2014 edit it directly (init never merges)`);
6501
+ const target = path12.join(root, "mend.toml");
6502
+ if (fs11.existsSync(target)) {
6503
+ return fail5(`${target} already exists \u2014 edit it directly (init never merges)`);
5809
6504
  }
5810
- const rootFiles = fs9.readdirSync(root);
6505
+ const rootFiles = fs11.readdirSync(root);
5811
6506
  const proposals = [];
5812
- const readRoot = (name) => fs9.readFileSync(path10.join(root, name), "utf8");
6507
+ const readRoot = (name) => fs11.readFileSync(path12.join(root, name), "utf8");
5813
6508
  if (rootFiles.includes("package.json")) {
5814
6509
  proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
5815
6510
  }
@@ -5819,13 +6514,13 @@ var serviceInit = async (args) => {
5819
6514
  );
5820
6515
  for (const glob of globs) {
5821
6516
  const parent = glob.replace(/\/?\*+$/, "");
5822
- const dirs = glob.endsWith("*") ? fs9.existsSync(path10.join(root, parent)) ? fs9.readdirSync(path10.join(root, parent)).map((dir) => path10.join(parent, dir)) : [] : [glob];
6517
+ const dirs = glob.endsWith("*") ? fs11.existsSync(path12.join(root, parent)) ? fs11.readdirSync(path12.join(root, parent)).map((dir) => path12.join(parent, dir)) : [] : [glob];
5823
6518
  for (const dir of dirs) {
5824
- const manifest = path10.join(root, dir, "package.json");
5825
- if (!fs9.existsSync(manifest)) continue;
6519
+ const manifest = path12.join(root, dir, "package.json");
6520
+ if (!fs11.existsSync(manifest)) continue;
5826
6521
  for (const proposal of proposeFromWorkspacePackage(
5827
- path10.basename(dir),
5828
- fs9.readFileSync(manifest, "utf8"),
6522
+ path12.basename(dir),
6523
+ fs11.readFileSync(manifest, "utf8"),
5829
6524
  rootFiles
5830
6525
  )) {
5831
6526
  if (!proposals.some((existing) => existing.name === proposal.name)) {
@@ -5847,58 +6542,60 @@ var serviceInit = async (args) => {
5847
6542
  }
5848
6543
  }
5849
6544
  if (proposals.length === 0) {
5850
- return fail4(
6545
+ return fail5(
5851
6546
  "nothing to propose \u2014 no server-ish package.json script with a nameable port, no compose ports"
5852
6547
  );
5853
6548
  }
5854
6549
  const toml = renderMendToml(proposals);
5855
- say4(dim4(`proposed ${target}:`));
5856
- say4("");
6550
+ say5(dim5(`proposed ${target}:`));
6551
+ say5("");
5857
6552
  process.stdout.write(toml);
5858
- say4("");
6553
+ say5("");
5859
6554
  if (!args.includes("--yes")) {
5860
6555
  if (process.stdin.isTTY !== true) {
5861
- return fail4("non-interactive \u2014 pass --yes to write the file");
6556
+ return fail5("non-interactive \u2014 pass --yes to write the file");
5862
6557
  }
5863
6558
  const readline = await import("node:readline/promises");
5864
6559
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5865
6560
  const answer = await rl.question("write it? [y/N] ");
5866
6561
  rl.close();
5867
6562
  if (answer.trim().toLowerCase() !== "y") {
5868
- say4(dim4("nothing written"));
6563
+ say5(dim5("nothing written"));
5869
6564
  return;
5870
6565
  }
5871
6566
  }
5872
- fs9.writeFileSync(target, toml);
5873
- say4(`${green4("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
6567
+ fs11.writeFileSync(target, toml);
6568
+ say5(`${green5("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
5874
6569
  };
5875
6570
  var tunnelServices = async (config, services, portOverride) => {
5876
- const tunnelUrl = (serviceId) => {
5877
- const url = parseMendUrl(`${config.url}/api/service-tunnel`);
5878
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
5879
- url.searchParams.set("service", serviceId);
5880
- if (config.token !== null) url.searchParams.set("token", config.token);
5881
- return url;
5882
- };
6571
+ const tunnelUrl = (serviceId) => socketUrl(config, "service-tunnel", { service: serviceId });
5883
6572
  for (const service of services) {
5884
6573
  const port = portOverride ?? service.hostPort ?? service.workspacePort;
5885
6574
  const server = net3.createServer((socket) => {
5886
6575
  socket.pause();
5887
- const ws = new WebSocket(tunnelUrl(service.id));
5888
- ws.binaryType = "arraybuffer";
5889
- ws.addEventListener("open", () => socket.resume(), { once: true });
5890
- ws.addEventListener("message", (event) => {
5891
- if (typeof event.data === "string") return;
5892
- socket.write(Buffer.from(event.data));
5893
- });
5894
- ws.addEventListener("close", () => socket.end(), { once: true });
5895
- ws.addEventListener("error", () => socket.destroy(), { once: true });
5896
- socket.on("data", (chunk) => ws.send(new Uint8Array(chunk)));
5897
- socket.on("end", () => {
5898
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ t: "eof" }));
5899
- });
5900
- socket.on("close", () => ws.close());
5901
- socket.on("error", () => ws.close());
6576
+ void tunnelUrl(service.id).then(
6577
+ (url) => {
6578
+ if (socket.destroyed) return null;
6579
+ const ws = new WebSocket(url);
6580
+ ws.binaryType = "arraybuffer";
6581
+ ws.addEventListener("open", () => socket.resume(), { once: true });
6582
+ ws.addEventListener("message", (event) => {
6583
+ if (typeof event.data === "string") return;
6584
+ socket.write(Buffer.from(event.data));
6585
+ });
6586
+ ws.addEventListener("close", () => socket.end(), { once: true });
6587
+ ws.addEventListener("error", () => socket.destroy(), { once: true });
6588
+ socket.on("data", (chunk) => ws.send(new Uint8Array(chunk)));
6589
+ socket.on("end", () => {
6590
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ t: "eof" }));
6591
+ });
6592
+ socket.on("close", () => ws.close());
6593
+ socket.on("error", () => ws.close());
6594
+ return null;
6595
+ },
6596
+ // No ticket, no tunnel: the local connection ends the way a refused one would.
6597
+ () => socket.destroy()
6598
+ );
5902
6599
  });
5903
6600
  await new Promise((resolve4, reject) => {
5904
6601
  server.once("error", (error) => {
@@ -5909,12 +6606,12 @@ var tunnelServices = async (config, services, portOverride) => {
5909
6606
  );
5910
6607
  });
5911
6608
  server.listen(port, "127.0.0.1", () => resolve4());
5912
- }).catch((error) => fail4(error.message));
5913
- say4(
5914
- `${green4("\u25CF")} ${service.label ?? service.id.slice(0, 8)} \u2192 127.0.0.1:${port} ${dim4(`(tunnel to ${config.url})`)}`
6609
+ }).catch((error) => fail5(error.message));
6610
+ say5(
6611
+ `${green5("\u25CF")} ${service.label ?? service.id.slice(0, 8)} \u2192 127.0.0.1:${port} ${dim5(`(tunnel to ${config.url})`)}`
5915
6612
  );
5916
6613
  }
5917
- say4(dim4(" connections are authenticated as you \xB7 Ctrl-C stops"));
6614
+ say5(dim5(" connections are authenticated as you \xB7 Ctrl-C stops"));
5918
6615
  await new Promise(() => {
5919
6616
  });
5920
6617
  };
@@ -5922,18 +6619,18 @@ var serviceConnect = async (config, args) => {
5922
6619
  const portFlag = args.indexOf("--port");
5923
6620
  const portOverride = portFlag === -1 ? null : Number(args[portFlag + 1]);
5924
6621
  if (portOverride !== null && !Number.isInteger(portOverride)) {
5925
- return fail4("--port takes a port number");
6622
+ return fail5("--port takes a port number");
5926
6623
  }
5927
6624
  const names = args.filter((a, i) => !a.startsWith("--") && i !== portFlag + 1);
5928
6625
  const live = (await fetchServices(config)).filter((s) => s.protocol === "tcp");
5929
6626
  const picked = names.length === 0 ? live : live.filter((s) => names.some((n) => s.label === n || s.id.startsWith(n)));
5930
6627
  if (picked.length === 0) {
5931
- return fail4(
6628
+ return fail5(
5932
6629
  names.length === 0 ? "no live TCP services \u2014 mend service run starts one" : `no live TCP service matches "${names.join('", "')}"`
5933
6630
  );
5934
6631
  }
5935
6632
  if (portOverride !== null && picked.length !== 1) {
5936
- return fail4("--port applies to exactly one service \u2014 name it");
6633
+ return fail5("--port applies to exactly one service \u2014 name it");
5937
6634
  }
5938
6635
  await tunnelServices(config, picked, portOverride);
5939
6636
  };
@@ -5962,23 +6659,23 @@ var serviceCommand = async (config, args) => {
5962
6659
  }
5963
6660
  };
5964
6661
  var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
5965
- var readIfExists = (file) => fs9.existsSync(file) ? fs9.readFileSync(file, "utf8") : null;
6662
+ var readIfExists = (file) => fs11.existsSync(file) ? fs11.readFileSync(file, "utf8") : null;
5966
6663
  var localCredential = (provider) => {
5967
- const home = os6.homedir();
6664
+ const home = os7.homedir();
5968
6665
  switch (provider) {
5969
6666
  case "codex":
5970
6667
  return readIfExists(
5971
- path10.join(process.env["CODEX_HOME"] ?? path10.join(home, ".codex"), "auth.json")
6668
+ path12.join(process.env["CODEX_HOME"] ?? path12.join(home, ".codex"), "auth.json")
5972
6669
  );
5973
6670
  case "claude":
5974
6671
  return readIfExists(
5975
- path10.join(
5976
- process.env["CLAUDE_CONFIG_DIR"] ?? path10.join(home, ".claude"),
6672
+ path12.join(
6673
+ process.env["CLAUDE_CONFIG_DIR"] ?? path12.join(home, ".claude"),
5977
6674
  ".credentials.json"
5978
6675
  )
5979
6676
  );
5980
6677
  case "github": {
5981
- const result = spawnSync2("gh", ["auth", "token"], { encoding: "utf8" });
6678
+ const result = spawnSync3("gh", ["auth", "token"], { encoding: "utf8" });
5982
6679
  const token = result.status === 0 ? result.stdout.trim() : "";
5983
6680
  return token === "" ? null : token;
5984
6681
  }
@@ -6010,16 +6707,94 @@ var accountsCommand = async (config) => {
6010
6707
  );
6011
6708
  }
6012
6709
  };
6710
+ var minuteOrUnknown = (at) => at === null ? "unknown" : at.toISOString().slice(0, 16);
6711
+ var staleGrantReason = (secret) => {
6712
+ const facts = claudeGrantFacts(secret);
6713
+ if (facts === null) return null;
6714
+ if (!facts.hasRefreshToken) return "signed out";
6715
+ return facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now() ? `expired ${facts.refreshExpiresAt.toISOString().slice(0, 10)}` : null;
6716
+ };
6717
+ var claudeGrant = async () => {
6718
+ const cli = claudeCli();
6719
+ const dir = claudeGrantDir(mendCliHome());
6720
+ const personal = personalClaudeDir();
6721
+ const personalBefore = grantStatus(cli, personal);
6722
+ let status = grantStatus(cli, dir);
6723
+ if (status === null) {
6724
+ fail5(
6725
+ "claude: could not run `claude auth status` \u2014 install Claude Code, set MEND_CLAUDE_BIN, or paste a credential: mend connect claude --from-stdin"
6726
+ );
6727
+ return null;
6728
+ }
6729
+ if (!status.loggedIn) {
6730
+ say5(` Mend needs its own Claude login, kept in ${dir}`);
6731
+ say5(dim5(" your own Claude login stays as it is"));
6732
+ if (!runClaudeLogin(cli, dir)) {
6733
+ fail5("claude: the login did not complete");
6734
+ return null;
6735
+ }
6736
+ status = grantStatus(cli, dir) ?? status;
6737
+ if (!status.loggedIn) {
6738
+ fail5("claude: the login completed but Claude still reports no grant in Mend's directory");
6739
+ return null;
6740
+ }
6741
+ }
6742
+ if (status.configDirectory !== null && path12.resolve(status.configDirectory) !== path12.resolve(dir)) {
6743
+ fail5(
6744
+ `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`
6745
+ );
6746
+ return null;
6747
+ }
6748
+ let read = readGrant(dir);
6749
+ if (read.kind !== "missing") {
6750
+ const why = staleGrantReason(read.secret);
6751
+ if (why !== null) {
6752
+ say5(` Mend's Claude grant is ${why}; logging in again`);
6753
+ if (!runClaudeLogin(cli, dir)) {
6754
+ fail5("claude: the login did not complete");
6755
+ return null;
6756
+ }
6757
+ read = readGrant(dir);
6758
+ const still = read.kind === "missing" ? "missing" : staleGrantReason(read.secret);
6759
+ if (still !== null) {
6760
+ fail5(`claude: the grant is still ${still} after logging in`);
6761
+ return null;
6762
+ }
6763
+ }
6764
+ }
6765
+ if (read.kind === "missing") {
6766
+ const where = read.triedService === null ? read.triedPath : `${read.triedPath} or the Keychain item ${read.triedService}`;
6767
+ fail5(`claude: logged in, but no credential to read \u2014 looked in ${where}`);
6768
+ return null;
6769
+ }
6770
+ const mine = localCredential("claude");
6771
+ if (mine !== null && sameGrant(read.secret, mine)) {
6772
+ fail5(
6773
+ "claude: that is the same grant this machine's Claude holds, so both sides would race on refresh \u2014 remove Mend's directory and log in again, or accept the race with --use-my-login"
6774
+ );
6775
+ return null;
6776
+ }
6777
+ const personalAfter = grantStatus(cli, personal);
6778
+ if (personalBefore?.loggedIn === true && personalAfter?.loggedIn === false) {
6779
+ say5(
6780
+ ` your own Claude login was signed out by this one \u2014 this account allows one grant at a time. Run \`claude auth login\` to get it back, and connect the shared grant with --use-my-login instead.`
6781
+ );
6782
+ } else if (personalAfter?.loggedIn === true) {
6783
+ say5(dim5(" your own Claude login still works \xB7 verified"));
6784
+ }
6785
+ say5(dim5(` grant ${read.kind === "file" ? read.path : `keychain ${read.service}`}`));
6786
+ return read.secret;
6787
+ };
6013
6788
  var connectCommand = async (config, args) => {
6014
6789
  const [providerArg, ...flags] = args;
6015
6790
  if (!isProvider(providerArg)) {
6016
- return fail4(usageOf("connect"));
6791
+ return fail5(usageOf("connect"));
6017
6792
  }
6018
6793
  const provider = providerArg;
6019
6794
  if (flags.includes("--remove")) {
6020
6795
  const identity = await api(config, "GET", "/me/sealant");
6021
6796
  const account2 = identity.accounts.find((row) => row.provider === provider);
6022
- if (account2 === void 0) return fail4(`${provider}: nothing connected`);
6797
+ if (account2 === void 0) return fail5(`${provider}: nothing connected`);
6023
6798
  await api(config, "DELETE", `/me/sealant/accounts/${account2.id}`);
6024
6799
  process.stdout.write(`${provider}: disconnected
6025
6800
  `);
@@ -6027,35 +6802,60 @@ var connectCommand = async (config, args) => {
6027
6802
  }
6028
6803
  let secret;
6029
6804
  if (flags.includes("--from-stdin")) {
6030
- secret = fs9.readFileSync(0, "utf8").trim();
6031
- if (secret === "") return fail4("nothing on stdin");
6805
+ secret = fs11.readFileSync(0, "utf8").trim();
6806
+ if (secret === "") return fail5("nothing on stdin");
6807
+ } else if (provider === "claude" && !flags.includes("--use-my-login")) {
6808
+ secret = await claudeGrant();
6809
+ if (secret === null) return;
6032
6810
  } else {
6033
6811
  secret = localCredential(provider);
6034
6812
  if (secret === null) {
6035
- const where = provider === "github" ? "`gh auth login` first, or pipe a token: gh auth token | mend connect github --from-stdin" : provider === "codex" ? "`codex login` first, or: mend connect codex --from-stdin < auth.json" : "`claude setup-token` then: mend connect claude --from-stdin";
6036
- return fail4(`${provider}: no credential on this machine \u2014 ${where}`);
6813
+ const where = provider === "github" ? "`gh auth login` first, or pipe a token: gh auth token | mend connect github --from-stdin" : provider === "codex" ? "`codex login` first, or: mend connect codex --from-stdin < auth.json" : "`claude auth login` first, or: mend connect claude --from-stdin";
6814
+ return fail5(`${provider}: no credential on this machine \u2014 ${where}`);
6815
+ }
6816
+ if (provider === "claude") {
6817
+ say5(
6818
+ dim5(
6819
+ " --use-my-login: Mend and this machine will share one grant, and whichever refreshes second is signed out"
6820
+ )
6821
+ );
6037
6822
  }
6038
6823
  }
6824
+ const narrowed = narrowCredential(provider, secret);
6825
+ if (narrowed.kind === "narrowed" && narrowed.dropped.length > 0) {
6826
+ say5(dim5(` keeping ${narrowed.dropped.join(", ")} on this machine`));
6827
+ }
6039
6828
  const account = await withSpinner(
6040
6829
  `connecting ${provider}`,
6041
- api(config, "POST", "/me/sealant/accounts", { provider, secret })
6830
+ api(config, "POST", "/me/sealant/accounts", {
6831
+ provider,
6832
+ secret: narrowed.secret
6833
+ })
6042
6834
  );
6043
6835
  process.stdout.write(`${accountLine(account)}
6044
6836
  `);
6837
+ const facts = claudeGrantFacts(narrowed.secret);
6838
+ if (facts !== null) {
6839
+ say5(
6840
+ dim5(
6841
+ ` access expires ${minuteOrUnknown(facts.accessExpiresAt)} \xB7 grant expires ${minuteOrUnknown(facts.refreshExpiresAt)}`
6842
+ )
6843
+ );
6844
+ }
6045
6845
  };
6046
- var takeFlagValue3 = (args, flag) => {
6846
+ var takeFlagValue4 = (args, flag) => {
6047
6847
  const at = args.indexOf(flag);
6048
6848
  return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
6049
6849
  };
6050
6850
  var saveCliConfig = (next) => {
6051
- fs9.mkdirSync(path10.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
6052
- fs9.writeFileSync(
6851
+ fs11.mkdirSync(path12.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
6852
+ fs11.writeFileSync(
6053
6853
  CONFIG_PATH,
6054
6854
  `${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
6055
6855
  `,
6056
6856
  { mode: 384 }
6057
6857
  );
6058
- fs9.chmodSync(CONFIG_PATH, 384);
6858
+ fs11.chmodSync(CONFIG_PATH, 384);
6059
6859
  };
6060
6860
  var login = async (config, args) => {
6061
6861
  await loginCommand(args, {
@@ -6063,25 +6863,25 @@ var login = async (config, args) => {
6063
6863
  defaultUrl: config.url,
6064
6864
  save: (next) => {
6065
6865
  saveCliConfig({ url: next.url, token: next.token, deviceId: next.deviceId });
6066
- say4(dim4(` token saved to ${CONFIG_PATH} (0600)`));
6866
+ say5(dim5(` token saved to ${CONFIG_PATH} (0600)`));
6067
6867
  }
6068
6868
  });
6069
6869
  };
6070
6870
  var logout = async (config) => {
6071
- if (!fs9.existsSync(CONFIG_PATH) && config.token === null) {
6072
- say4(dim4("nothing saved \u2014 already signed out"));
6871
+ if (!fs11.existsSync(CONFIG_PATH) && config.token === null) {
6872
+ say5(dim5("nothing saved \u2014 already signed out"));
6073
6873
  return;
6074
6874
  }
6075
6875
  if (config.token !== null && config.deviceId !== null) {
6076
6876
  try {
6077
6877
  await request(config, "DELETE", `/me/devices/${config.deviceId}`);
6078
- say4(`${green4("\u2713")} device revoked on ${config.url}`);
6878
+ say5(`${green5("\u2713")} device revoked on ${config.url}`);
6079
6879
  } catch {
6080
- say4(dim4(" could not revoke on the server \u2014 end it under Settings \u2192 Devices"));
6880
+ say5(dim5(" could not revoke on the server \u2014 end it under Settings \u2192 Devices"));
6081
6881
  }
6082
6882
  }
6083
6883
  saveCliConfig({ ...config, token: null, deviceId: null });
6084
- say4(`${green4("\u2713")} signed out \xB7 ${dim4(`token removed from ${CONFIG_PATH}`)}`);
6884
+ say5(`${green5("\u2713")} signed out \xB7 ${dim5(`token removed from ${CONFIG_PATH}`)}`);
6085
6885
  };
6086
6886
  var UNINSTALL_CHOICES = [
6087
6887
  { scope: "all", text: "everything \xB7 the server on this machine and this machine's Mend files" },
@@ -6093,20 +6893,20 @@ var UNINSTALL_CHOICES = [
6093
6893
  ];
6094
6894
  var askUninstallScope = async () => {
6095
6895
  if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
6096
- return fail4(`${UNINSTALL_USAGE} \xB7 no terminal to ask on`);
6896
+ return fail5(`${UNINSTALL_USAGE} \xB7 no terminal to ask on`);
6097
6897
  }
6098
- say4("what should go?");
6099
- UNINSTALL_CHOICES.forEach((choice, index) => say4(` ${index + 1}. ${choice.text}`));
6898
+ say5("what should go?");
6899
+ UNINSTALL_CHOICES.forEach((choice, index) => say5(` ${index + 1}. ${choice.text}`));
6100
6900
  const readline = await import("node:readline/promises");
6101
6901
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6102
6902
  const answer = (await rl.question(" 1, 2 or 3: ")).trim();
6103
6903
  rl.close();
6104
6904
  const chosen = UNINSTALL_CHOICES[Number(answer) - 1];
6105
- if (chosen === void 0) return fail4(`"${answer}" is not one of the choices`);
6905
+ if (chosen === void 0) return fail5(`"${answer}" is not one of the choices`);
6106
6906
  return chosen.scope;
6107
6907
  };
6108
6908
  var confirmUninstall = async (word) => {
6109
- if (process.stdin.isTTY !== true) return fail4("non-interactive \u2014 pass --yes to remove");
6909
+ if (process.stdin.isTTY !== true) return fail5("non-interactive \u2014 pass --yes to remove");
6110
6910
  const readline = await import("node:readline/promises");
6111
6911
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6112
6912
  const answer = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
@@ -6115,14 +6915,14 @@ var confirmUninstall = async (word) => {
6115
6915
  };
6116
6916
  var uninstallCommand = async (config, args) => {
6117
6917
  const parsed = parseUninstallArgs(args);
6118
- if ("error" in parsed) return fail4(parsed.error);
6918
+ if ("error" in parsed) return fail5(parsed.error);
6119
6919
  const scope = parsed.scope ?? await askUninstallScope();
6120
6920
  const server = nodeServerRuntime();
6121
6921
  const runtime = {
6122
6922
  server,
6123
6923
  cliHome: mendCliHome(),
6124
- sshConfigFile: path10.join(os6.homedir(), ".ssh", "config"),
6125
- signedIn: config.token === null || !fs9.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
6924
+ sshConfigFile: path12.join(os7.homedir(), ".ssh", "config"),
6925
+ signedIn: config.token === null || !fs11.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
6126
6926
  revokeDevice: async () => {
6127
6927
  if (config.deviceId === null) return "no device id saved";
6128
6928
  try {
@@ -6134,42 +6934,42 @@ var uninstallCommand = async (config, args) => {
6134
6934
  }
6135
6935
  };
6136
6936
  const plan = await describeUninstall(runtime, scope);
6137
- say4(dim4(`mend uninstall \xB7 ${scope === "all" ? "everything" : scope}`));
6138
- for (const line of planLines(plan, server.configDir)) say4(` ${line}`);
6937
+ say5(dim5(`mend uninstall \xB7 ${scope === "all" ? "everything" : scope}`));
6938
+ for (const line of planLines(plan, server.configDir)) say5(` ${line}`);
6139
6939
  const nothing = (plan.server === null || plan.server === "none") && (plan.home === null || plan.home.cliConfig === null && plan.home.sshDirectory === null && plan.home.managedSshBlocks === 0);
6140
6940
  if (nothing) {
6141
- say4(dim4("nothing to remove"));
6941
+ say5(dim5("nothing to remove"));
6142
6942
  return;
6143
6943
  }
6144
6944
  if (planDeletesData(plan)) {
6145
- say4("repositories, worktrees, the database and its backups are deleted with the server");
6945
+ say5("repositories, worktrees, the database and its backups are deleted with the server");
6146
6946
  }
6147
6947
  if (!parsed.yes && !await confirmUninstall(planDeletesData(plan) ? "delete" : "y")) {
6148
- say4(dim4("nothing removed"));
6948
+ say5(dim5("nothing removed"));
6149
6949
  return;
6150
6950
  }
6151
6951
  const outcome = await executeUninstall(runtime, plan);
6152
- for (const line of outcome.leftovers) say4(dim4(` kept \xB7 ${line}`));
6952
+ for (const line of outcome.leftovers) say5(dim5(` kept \xB7 ${line}`));
6153
6953
  if (outcome.failures.length > 0) {
6154
- return fail4(outcome.failures.join("\n"));
6954
+ return fail5(outcome.failures.join("\n"));
6155
6955
  }
6156
- say4(
6157
- `${green4("\u2713")} ${scope === "all" ? "Mend is gone from this machine" : scope === "server" ? "the server is gone from this machine" : "this machine no longer holds Mend files"}`
6956
+ say5(
6957
+ `${green5("\u2713")} ${scope === "all" ? "Mend is gone from this machine" : scope === "server" ? "the server is gone from this machine" : "this machine no longer holds Mend files"}`
6158
6958
  );
6159
6959
  };
6160
6960
  var printGitKey = (key) => {
6161
6961
  if (key.publicKey === null) return;
6162
- say4(key.publicKey);
6163
- if (key.fingerprint !== null) say4(dim4(` ${key.fingerprint}`));
6164
- say4(dim4(" add this to your git account's SSH keys (GitHub: settings \u2192 SSH keys) so"));
6165
- say4(dim4(" every repository you can reach works; for one repository only, add it"));
6166
- say4(dim4(" as that repository's deploy key instead (grant write if sessions push)"));
6962
+ say5(key.publicKey);
6963
+ if (key.fingerprint !== null) say5(dim5(` ${key.fingerprint}`));
6964
+ say5(dim5(" add this to your git account's SSH keys (GitHub: settings \u2192 SSH keys) so"));
6965
+ say5(dim5(" every repository you can reach works; for one repository only, add it"));
6966
+ say5(dim5(" as that repository's deploy key instead (grant write if sessions push)"));
6167
6967
  };
6168
6968
  var keysShow = async (config) => {
6169
6969
  const key = await api(config, "GET", "/keys/git");
6170
6970
  if (!key.exists) {
6171
- say4(
6172
- `no Mend key yet ${dim4("\u2014 mend keys init generates one (ed25519, stays on the server host)")}`
6971
+ say5(
6972
+ `no Mend key yet ${dim5("\u2014 mend keys init generates one (ed25519, stays on the server host)")}`
6173
6973
  );
6174
6974
  return;
6175
6975
  }
@@ -6177,46 +6977,41 @@ var keysShow = async (config) => {
6177
6977
  };
6178
6978
  var keysInit = async (config) => {
6179
6979
  const key = await api(config, "POST", "/keys/git");
6180
- say4(`${green4("\u2713")} Mend key ready ${dim4("(private half stays on the server host)")}`);
6980
+ say5(`${green5("\u2713")} Mend key ready ${dim5("(private half stays on the server host)")}`);
6181
6981
  printGitKey(key);
6182
6982
  };
6183
6983
  var keysShare = async (config) => {
6184
6984
  const agentSock = process.env["SSH_AUTH_SOCK"];
6185
6985
  if (agentSock === void 0 || agentSock === "") {
6186
- return fail4("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
6986
+ return fail5("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
6187
6987
  }
6188
- const url = bridgeUrlOf(
6189
- parseMendUrl(config.url).toString().replace(/\/$/, ""),
6190
- config.token,
6191
- os6.hostname()
6192
- );
6193
6988
  await shareAgent({
6194
- url,
6989
+ url: () => socketUrl(config, "keys-bridge", { host: os7.hostname() }),
6195
6990
  agentSock,
6196
6991
  signal: new AbortController().signal,
6197
6992
  onEvent: (event) => {
6198
6993
  switch (event.kind) {
6199
6994
  case "connected":
6200
- say4(`${green4("\u25CF")} sharing this machine's ssh-agent with ${config.url}`);
6201
- say4(dim4(` agent: ${agentSock} \xB7 signature requests print here \xB7 Ctrl-C stops sharing`));
6995
+ say5(`${green5("\u25CF")} sharing this machine's ssh-agent with ${config.url}`);
6996
+ say5(dim5(` agent: ${agentSock} \xB7 signature requests print here \xB7 Ctrl-C stops sharing`));
6202
6997
  return;
6203
6998
  case "disconnected":
6204
- say4(
6205
- dim4(`not connected \u2014 retrying in ${Math.round(event.retryMs / 1e3)}s (Ctrl-C stops)`)
6999
+ say5(
7000
+ dim5(`not connected \u2014 retrying in ${Math.round(event.retryMs / 1e3)}s (Ctrl-C stops)`)
6206
7001
  );
6207
7002
  return;
6208
7003
  case "sign-requested":
6209
- say4(`${amber("\u270E")} signature requested by mend ${dim4(`(${event.context})`)}`);
6210
- say4(dim4(" waiting \u2014 touch your key if it blinks (up to 60s)\u2026"));
7004
+ say5(`${amber("\u270E")} signature requested by mend ${dim5(`(${event.context})`)}`);
7005
+ say5(dim5(" waiting \u2014 touch your key if it blinks (up to 60s)\u2026"));
6211
7006
  return;
6212
7007
  case "identities-requested":
6213
- say4(dim4(` identities requested (${event.context})`));
7008
+ say5(dim5(` identities requested (${event.context})`));
6214
7009
  return;
6215
7010
  case "signed":
6216
- say4(`${green4("\u2713")} signed ${dim4(`(${event.seconds.toFixed(1)}s)`)}`);
7011
+ say5(`${green5("\u2713")} signed ${dim5(`(${event.seconds.toFixed(1)}s)`)}`);
6217
7012
  return;
6218
7013
  case "not-signed":
6219
- say4(`${amber("\u2717")} not signed \u2014 ${event.message}`);
7014
+ say5(`${amber("\u2717")} not signed \u2014 ${event.message}`);
6220
7015
  return;
6221
7016
  }
6222
7017
  }
@@ -6225,19 +7020,19 @@ var keysShare = async (config) => {
6225
7020
  var keysMode = async (config, args) => {
6226
7021
  const [value] = args;
6227
7022
  if (value !== void 0 && value !== "mend-key" && value !== "bridge") {
6228
- return fail4(`keys mode takes mend-key or bridge, not "${value}"`);
7023
+ return fail5(`keys mode takes mend-key or bridge, not "${value}"`);
6229
7024
  }
6230
7025
  const access = value === void 0 ? await api(config, "GET", "/me/git-access") : await api(config, "PUT", "/me/git-access", { mode: value });
6231
- if (value !== void 0) say4(`${green4("\u2713")} git access \xB7 ${value}`);
7026
+ if (value !== void 0) say5(`${green5("\u2713")} git access \xB7 ${value}`);
6232
7027
  if (access.mode === "mend-key") {
6233
- say4(`mend-key ${dim4("\u2014 your Mend key on the server signs; works whenever the server is up")}`);
7028
+ say5(`mend-key ${dim5("\u2014 your Mend key on the server signs; works whenever the server is up")}`);
6234
7029
  if (access.key.exists) printGitKey(access.key);
6235
- else say4(dim4(" no key yet \u2014 mend keys init creates it"));
7030
+ else say5(dim5(" no key yet \u2014 mend keys init creates it"));
6236
7031
  return;
6237
7032
  }
6238
- say4(`bridge ${dim4("\u2014 this machine's ssh-agent signs; only while a mend command runs here")}`);
6239
- say4(
6240
- access.bridge.connected ? `${green4("\u25CF")} signer connected \xB7 ${access.bridge.clientName ?? "unknown machine"}` : dim4(" no signer connected \u2014 mend shares the agent whenever it runs (or: mend keys share)")
7033
+ say5(`bridge ${dim5("\u2014 this machine's ssh-agent signs; only while a mend command runs here")}`);
7034
+ say5(
7035
+ access.bridge.connected ? `${green5("\u25CF")} signer connected \xB7 ${access.bridge.clientName ?? "unknown machine"}` : dim5(" no signer connected \u2014 mend shares the agent whenever it runs (or: mend keys share)")
6241
7036
  );
6242
7037
  };
6243
7038
  var keysCommand = async (config, args) => {
@@ -6253,75 +7048,75 @@ var keysCommand = async (config, args) => {
6253
7048
  case void 0:
6254
7049
  return keysShow(config);
6255
7050
  default:
6256
- return fail4(`unknown keys command "${verb}" \xB7 mend help keys lists them`);
7051
+ return fail5(`unknown keys command "${verb}" \xB7 mend help keys lists them`);
6257
7052
  }
6258
7053
  };
6259
7054
  var formatDotfileBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
6260
7055
  var dotfilesShow = async (config) => {
6261
7056
  const dotfiles = await api(config, "GET", "/dotfiles");
6262
7057
  if (dotfiles.repository === null) {
6263
- say4(`repo ${dim4("none")}`);
7058
+ say5(`repo ${dim5("none")}`);
6264
7059
  } else {
6265
7060
  const branch = dotfiles.repository.ref ?? "default branch";
6266
7061
  const subdir = dotfiles.repository.subdirectory === null ? [] : [`${dotfiles.repository.subdirectory}/`];
6267
7062
  const bootstrap = dotfiles.repository.bootstrap ? "install.sh on" : "install.sh off";
6268
- say4(
6269
- `repo ${dotfiles.repository.url} ${dim4(`(${[branch, ...subdir, bootstrap].join(" \xB7 ")})`)}`
7063
+ say5(
7064
+ `repo ${dotfiles.repository.url} ${dim5(`(${[branch, ...subdir, bootstrap].join(" \xB7 ")})`)}`
6270
7065
  );
6271
7066
  }
6272
7067
  if (dotfiles.snapshot === null) {
6273
- say4(`snapshot ${dim4("none \u2014 sync from this machine: mend dotfiles sync --all")}`);
7068
+ say5(`snapshot ${dim5("none \u2014 sync from this machine: mend dotfiles sync --all")}`);
6274
7069
  return;
6275
7070
  }
6276
7071
  const snapshot = dotfiles.snapshot;
6277
- say4(
6278
- `snapshot ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} \xB7 from ${snapshot.source} \xB7 ${dim4(snapshot.sha.slice(0, 7))}`
7072
+ say5(
7073
+ `snapshot ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} \xB7 from ${snapshot.source} \xB7 ${dim5(snapshot.sha.slice(0, 7))}`
6279
7074
  );
6280
7075
  for (const file of snapshot.files) {
6281
- say4(` ${file.path.padEnd(36)} ${dim4(formatDotfileBytes(file.bytes))}`);
7076
+ say5(` ${file.path.padEnd(36)} ${dim5(formatDotfileBytes(file.bytes))}`);
6282
7077
  }
6283
7078
  };
6284
7079
  var dotfilesSync = async (config, args) => {
6285
7080
  const all = args.includes("--all");
6286
7081
  const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
6287
- const home = os6.homedir();
7082
+ const home = os7.homedir();
6288
7083
  if (!all && requestedPaths.length === 0) {
6289
7084
  const found = scanDotfileCandidates(home);
6290
7085
  if (found.length === 0) {
6291
- say4(dim4("no known config files found under ~"));
7086
+ say5(dim5("no known config files found under ~"));
6292
7087
  return;
6293
7088
  }
6294
7089
  let group = "";
6295
7090
  for (const entry of found) {
6296
7091
  if (entry.group !== group) {
6297
7092
  group = entry.group;
6298
- say4(dim4(group));
7093
+ say5(dim5(group));
6299
7094
  }
6300
- say4(` ${entry.path.padEnd(36)} ${dim4(formatDotfileBytes(entry.bytes))}`);
7095
+ say5(` ${entry.path.padEnd(36)} ${dim5(formatDotfileBytes(entry.bytes))}`);
6301
7096
  }
6302
- say4("");
6303
- say4(
7097
+ say5("");
7098
+ say5(
6304
7099
  `sync everything with ${cobalt2("mend dotfiles sync --all")}, or pick: ${cobalt2("mend dotfiles sync .zshrc .gitconfig")}`
6305
7100
  );
6306
7101
  return;
6307
7102
  }
6308
7103
  const selected = all ? scanDotfileCandidates(home).map((entry) => entry.path) : requestedPaths;
6309
- if (selected.length === 0) return fail4("nothing to sync \u2014 no known config files found under ~");
7104
+ if (selected.length === 0) return fail5("nothing to sync \u2014 no known config files found under ~");
6310
7105
  const read = readSyncFiles(home, selected);
6311
- if ("error" in read) return fail4(read.error);
7106
+ if ("error" in read) return fail5(read.error);
6312
7107
  const result = await api(config, "POST", "/dotfiles/snapshot", {
6313
7108
  files: read.files,
6314
- source: os6.hostname(),
7109
+ source: os7.hostname(),
6315
7110
  merge: false
6316
7111
  });
6317
7112
  const snapshot = result.snapshot;
6318
- if (snapshot === null) return fail4("the server accepted the sync but reports no snapshot");
6319
- say4(
6320
- `${green4("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os6.hostname()} \xB7 ${dim4(snapshot.sha.slice(0, 7))} ${dim4("\u2014 applies from the next session launch")}`
7113
+ if (snapshot === null) return fail5("the server accepted the sync but reports no snapshot");
7114
+ say5(
7115
+ `${green5("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os7.hostname()} \xB7 ${dim5(snapshot.sha.slice(0, 7))} ${dim5("\u2014 applies from the next session launch")}`
6321
7116
  );
6322
7117
  };
6323
7118
  var envLoad = async (config, args) => {
6324
- const explicitProject = takeFlagValue3(args, "--project");
7119
+ const explicitProject = takeFlagValue4(args, "--project");
6325
7120
  const secretFlag = args.indexOf("--secret");
6326
7121
  const secretArg = secretFlag === -1 ? void 0 : args[secretFlag + 1];
6327
7122
  const secretNames = secretArg !== void 0 && !secretArg.startsWith("--") && !secretArg.includes("/") && !secretArg.includes(".") ? secretArg.split(",").map((s) => s.trim()).filter((s) => s !== "") : [];
@@ -6331,15 +7126,15 @@ var envLoad = async (config, args) => {
6331
7126
  const positional = args.filter(
6332
7127
  (arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
6333
7128
  );
6334
- const file = path10.resolve(positional[0] ?? ".env");
7129
+ const file = path12.resolve(positional[0] ?? ".env");
6335
7130
  let contents;
6336
7131
  try {
6337
- contents = fs9.readFileSync(file, "utf8");
7132
+ contents = fs11.readFileSync(file, "utf8");
6338
7133
  } catch {
6339
- return fail4(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
7134
+ return fail5(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
6340
7135
  }
6341
7136
  if (contents.trim() === "") {
6342
- say4(dim4(`${file} is empty`));
7137
+ say5(dim5(`${file} is empty`));
6343
7138
  return;
6344
7139
  }
6345
7140
  const project2 = await findProject(config, explicitProject);
@@ -6350,46 +7145,46 @@ var envLoad = async (config, args) => {
6350
7145
  { contents, allSecret, secretNames }
6351
7146
  );
6352
7147
  if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
6353
- say4(dim4(`${path10.basename(file)} has no variables`));
7148
+ say5(dim5(`${path12.basename(file)} has no variables`));
6354
7149
  return;
6355
7150
  }
6356
- say4(`${green4("\u2713")} loaded ${path10.basename(file)} into ${project2.name}`);
6357
- for (const line of formatLoadReport(report, { dim: dim4, warn: amber })) say4(line);
7151
+ say5(`${green5("\u2713")} loaded ${path12.basename(file)} into ${project2.name}`);
7152
+ for (const line of formatLoadReport(report, { dim: dim5, warn: amber })) say5(line);
6358
7153
  const plaintextUrls = report.loaded.filter(
6359
7154
  (entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
6360
7155
  );
6361
7156
  if (plaintextUrls.length > 0) {
6362
- say4(
7157
+ say5(
6363
7158
  amber(
6364
7159
  ` ${plaintextUrls.map((entry) => entry.name).join(", ")} stored as plaintext configuration \u2014 if a value embeds a password, store it as a secret: mend env load --secret ${plaintextUrls.map((entry) => entry.name).join(",")}`
6365
7160
  )
6366
7161
  );
6367
7162
  }
6368
7163
  if (report.malformedLines.length > 0) {
6369
- say4(
7164
+ say5(
6370
7165
  amber(
6371
7166
  ` skipped ${report.malformedLines.length} malformed line${report.malformedLines.length === 1 ? "" : "s"}: ${report.malformedLines.join(", ")}`
6372
7167
  )
6373
7168
  );
6374
7169
  }
6375
- say4(
6376
- dim4(
7170
+ say5(
7171
+ dim5(
6377
7172
  ` configuration r${report.environmentRevision} \xB7 secrets r${report.secretRevision} \u2014 applies from the next workspace launch, including resume; running workspaces keep what they started with`
6378
7173
  )
6379
7174
  );
6380
7175
  };
6381
7176
  var envShow = async (config, args) => {
6382
- const project2 = await findProject(config, takeFlagValue3(args, "--project"));
7177
+ const project2 = await findProject(config, takeFlagValue4(args, "--project"));
6383
7178
  const [environment, secrets, cluster] = await Promise.all([
6384
7179
  api(config, "GET", `/projects/${project2.id}/environment`),
6385
7180
  api(config, "GET", `/projects/${project2.id}/secrets`),
6386
7181
  api(config, "GET", `/projects/${project2.id}/cluster-bindings`)
6387
7182
  ]);
6388
- say4(
6389
- `${project2.name} ${dim4(`\xB7 configuration r${environment.revision} \xB7 secrets r${secrets.revision} \xB7 cluster r${cluster.revision}`)}`
7183
+ say5(
7184
+ `${project2.name} ${dim5(`\xB7 configuration r${environment.revision} \xB7 secrets r${secrets.revision} \xB7 cluster r${cluster.revision}`)}`
6390
7185
  );
6391
7186
  if (environment.variables.length === 0 && secrets.secrets.length === 0 && cluster.bindings.length === 0 && cluster.serviceAccount === null) {
6392
- say4(dim4(` nothing stored \u2014 load a file: ${cobalt2("mend env load")}`));
7187
+ say5(dim5(` nothing stored \u2014 load a file: ${cobalt2("mend env load")}`));
6393
7188
  return;
6394
7189
  }
6395
7190
  const bindingNames = cluster.bindings.map((b) => `${b.kind}/${b.objectName}`);
@@ -6400,23 +7195,23 @@ var envShow = async (config, args) => {
6400
7195
  ...bindingNames.map((name) => name.length)
6401
7196
  );
6402
7197
  for (const variable of environment.variables) {
6403
- say4(` ${variable.name.padEnd(width)} configuration ${dim4("\xB7 plaintext")}`);
7198
+ say5(` ${variable.name.padEnd(width)} configuration ${dim5("\xB7 plaintext")}`);
6404
7199
  }
6405
7200
  for (const secret of secrets.secrets) {
6406
- say4(` ${secret.name.padEnd(width)} secret ${dim4("\xB7 value set, never shown")}`);
7201
+ say5(` ${secret.name.padEnd(width)} secret ${dim5("\xB7 value set, never shown")}`);
6407
7202
  }
6408
7203
  for (const name of bindingNames) {
6409
- say4(
6410
- ` ${name.padEnd(width)} cluster binding ${dim4("\xB7 resolved by the platform at launch \xB7 contents unknown to Mend")}`
7204
+ say5(
7205
+ ` ${name.padEnd(width)} cluster binding ${dim5("\xB7 resolved by the platform at launch \xB7 contents unknown to Mend")}`
6411
7206
  );
6412
7207
  }
6413
7208
  if (cluster.serviceAccount !== null) {
6414
- say4(
6415
- ` ${cluster.serviceAccount.padEnd(width)} service account ${dim4("\xB7 workspace pod identity \xB7 allowlisted by the operator")}`
7209
+ say5(
7210
+ ` ${cluster.serviceAccount.padEnd(width)} service account ${dim5("\xB7 workspace pod identity \xB7 allowlisted by the operator")}`
6416
7211
  );
6417
7212
  }
6418
7213
  if (!cluster.clusterCapable && (cluster.bindings.length > 0 || cluster.serviceAccount !== null)) {
6419
- say4(
7214
+ say5(
6420
7215
  amber(
6421
7216
  ` ${cluster.bindings.length} cluster binding${cluster.bindings.length === 1 ? "" : "s"}${cluster.serviceAccount === null ? "" : " \xB7 service account set"} \xB7 local runner \u2014 cluster bindings do not resolve here`
6422
7217
  )
@@ -6424,54 +7219,54 @@ var envShow = async (config, args) => {
6424
7219
  }
6425
7220
  };
6426
7221
  var envCluster = async (config, args) => {
6427
- const explicitProject = takeFlagValue3(args, "--project");
7222
+ const explicitProject = takeFlagValue4(args, "--project");
6428
7223
  const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
6429
7224
  const [verb, ...rest] = positional;
6430
7225
  const usage = usageOf("env cluster");
6431
7226
  const project2 = await findProject(config, explicitProject);
6432
7227
  const route = `/projects/${project2.id}/cluster-bindings`;
6433
- const appliesLine = () => say4(
6434
- dim4(
7228
+ const appliesLine = () => say5(
7229
+ dim5(
6435
7230
  " applies from the next workspace launch; running workspaces keep what they started with"
6436
7231
  )
6437
7232
  );
6438
7233
  if (verb === "add") {
6439
7234
  const [kind, objectName] = rest;
6440
7235
  if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
6441
- return fail4(`env cluster add takes a kind and an object name
7236
+ return fail5(`env cluster add takes a kind and an object name
6442
7237
  ${usage}`);
6443
7238
  }
6444
7239
  const result = await api(config, "POST", route, {
6445
7240
  kind,
6446
7241
  objectName
6447
7242
  });
6448
- say4(`${green4("\u2713")} bound ${kind}/${objectName} ${dim4(`\xB7 cluster r${result.revision}`)}`);
6449
- say4(dim4(" resolved by the platform at launch \xB7 contents unknown to Mend"));
7243
+ say5(`${green5("\u2713")} bound ${kind}/${objectName} ${dim5(`\xB7 cluster r${result.revision}`)}`);
7244
+ say5(dim5(" resolved by the platform at launch \xB7 contents unknown to Mend"));
6450
7245
  return appliesLine();
6451
7246
  }
6452
7247
  if (verb === "remove") {
6453
7248
  const [ref] = rest;
6454
7249
  const [kind, objectName] = ref?.split("/", 2) ?? [];
6455
7250
  if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
6456
- return fail4(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
7251
+ return fail5(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
6457
7252
  ${usage}`);
6458
7253
  }
6459
7254
  const snapshot = await api(config, "GET", route);
6460
7255
  const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
6461
- if (binding === void 0) return fail4(`${kind}/${objectName} is not bound on ${project2.name}`);
7256
+ if (binding === void 0) return fail5(`${kind}/${objectName} is not bound on ${project2.name}`);
6462
7257
  const result = await api(
6463
7258
  config,
6464
7259
  "DELETE",
6465
7260
  `${route}/${binding.id}`
6466
7261
  );
6467
- say4(`${green4("\u2713")} removed ${kind}/${objectName} ${dim4(`\xB7 cluster r${result.revision}`)}`);
7262
+ say5(`${green5("\u2713")} removed ${kind}/${objectName} ${dim5(`\xB7 cluster r${result.revision}`)}`);
6468
7263
  return appliesLine();
6469
7264
  }
6470
7265
  if (verb === "sa") {
6471
7266
  const clear = args.includes("--clear");
6472
7267
  const [name] = rest;
6473
7268
  if (!clear && name === void 0)
6474
- return fail4(`env cluster sa takes a name or --clear
7269
+ return fail5(`env cluster sa takes a name or --clear
6475
7270
  ${usage}`);
6476
7271
  const result = await api(
6477
7272
  config,
@@ -6479,11 +7274,11 @@ ${usage}`);
6479
7274
  `${route}/service-account`,
6480
7275
  { serviceAccount: clear ? null : name }
6481
7276
  );
6482
- say4(
6483
- result.serviceAccount === null ? `${green4("\u2713")} cleared the workspace service account ${dim4(`\xB7 cluster r${result.revision}`)}` : `${green4("\u2713")} service account ${result.serviceAccount} ${dim4(`\xB7 cluster r${result.revision}`)}`
7277
+ say5(
7278
+ result.serviceAccount === null ? `${green5("\u2713")} cleared the workspace service account ${dim5(`\xB7 cluster r${result.revision}`)}` : `${green5("\u2713")} service account ${result.serviceAccount} ${dim5(`\xB7 cluster r${result.revision}`)}`
6484
7279
  );
6485
7280
  if (result.serviceAccount !== null) {
6486
- say4(
7281
+ say5(
6487
7282
  amber(
6488
7283
  " the session agent holds this role's full permissions for the whole session \u2014 bind a least-privilege role intended for untrusted code; names outside the platform allowlist fail the launch"
6489
7284
  )
@@ -6491,7 +7286,7 @@ ${usage}`);
6491
7286
  }
6492
7287
  return appliesLine();
6493
7288
  }
6494
- return fail4(`unknown env cluster command "${verb ?? ""}"
7289
+ return fail5(`unknown env cluster command "${verb ?? ""}"
6495
7290
  ${usage}`);
6496
7291
  };
6497
7292
  var envCommand = async (config, args) => {
@@ -6505,7 +7300,7 @@ var envCommand = async (config, args) => {
6505
7300
  case void 0:
6506
7301
  return envShow(config, rest);
6507
7302
  default:
6508
- return fail4(`unknown env command "${verb}" \xB7 mend help env lists them`);
7303
+ return fail5(`unknown env command "${verb}" \xB7 mend help env lists them`);
6509
7304
  }
6510
7305
  };
6511
7306
  var dotfilesCommand = async (config, args) => {
@@ -6517,12 +7312,12 @@ var dotfilesCommand = async (config, args) => {
6517
7312
  case void 0:
6518
7313
  return dotfilesShow(config);
6519
7314
  default:
6520
- return fail4(`unknown dotfiles command "${verb}" \xB7 mend help dotfiles lists them`);
7315
+ return fail5(`unknown dotfiles command "${verb}" \xB7 mend help dotfiles lists them`);
6521
7316
  }
6522
7317
  };
6523
7318
  var skillsList = async (config, args) => {
6524
7319
  const forProject = args.includes("--project");
6525
- const project2 = forProject ? await findProject(config, takeFlagValue3(args, "--project")) : null;
7320
+ const project2 = forProject ? await findProject(config, takeFlagValue4(args, "--project")) : null;
6526
7321
  const skills = await api(
6527
7322
  config,
6528
7323
  "GET",
@@ -6530,25 +7325,25 @@ var skillsList = async (config, args) => {
6530
7325
  );
6531
7326
  const library = project2 === null ? "your library" : project2.name;
6532
7327
  if (skills.length === 0) {
6533
- say4(dim4(`no skills in ${library} \u2014 push a local one: mend skills push`));
7328
+ say5(dim5(`no skills in ${library} \u2014 push a local one: mend skills push`));
6534
7329
  return;
6535
7330
  }
6536
7331
  for (const skill of skills) {
6537
- say4(
6538
- ` ${skill.name.padEnd(28)} ${dim4(`${skill.fileCount} file${skill.fileCount === 1 ? "" : "s"} \xB7 ${formatDotfileBytes(skill.bytes)}`)}`
7332
+ say5(
7333
+ ` ${skill.name.padEnd(28)} ${dim5(`${skill.fileCount} file${skill.fileCount === 1 ? "" : "s"} \xB7 ${formatDotfileBytes(skill.bytes)}`)}`
6539
7334
  );
6540
- if (skill.description !== "") say4(` ${dim4(skill.description)}`);
7335
+ if (skill.description !== "") say5(` ${dim5(skill.description)}`);
6541
7336
  }
6542
7337
  };
6543
7338
  var skillsPush = async (config, args) => {
6544
- const dirFlag = takeFlagValue3(args, "--dir");
6545
- const root = dirFlag === null ? path10.join(os6.homedir(), DEFAULT_SKILLS_DIR) : path10.resolve(dirFlag);
7339
+ const dirFlag = takeFlagValue4(args, "--dir");
7340
+ const root = dirFlag === null ? path12.join(os7.homedir(), DEFAULT_SKILLS_DIR) : path12.resolve(dirFlag);
6546
7341
  const scanned = scanSkillLibrary(root);
6547
- if ("error" in scanned) return fail4(scanned.error);
6548
- for (const note of scanned.notes) say4(dim4(` ${note.skill}: ${note.message}`));
6549
- if (scanned.skills.length === 0) return fail4(`no skills found under ${root}`);
7342
+ if ("error" in scanned) return fail5(scanned.error);
7343
+ for (const note of scanned.notes) say5(dim5(` ${note.skill}: ${note.message}`));
7344
+ if (scanned.skills.length === 0) return fail5(`no skills found under ${root}`);
6550
7345
  const forProject = args.includes("--project");
6551
- const project2 = forProject ? await findProject(config, takeFlagValue3(args, "--project")) : null;
7346
+ const project2 = forProject ? await findProject(config, takeFlagValue4(args, "--project")) : null;
6552
7347
  const report = await api(config, "POST", "/skills/sync", {
6553
7348
  scope: project2 === null ? "user" : "project",
6554
7349
  projectId: project2?.id ?? null,
@@ -6566,10 +7361,10 @@ var skillsPush = async (config, args) => {
6566
7361
  `${report.unchanged.length} unchanged`,
6567
7362
  ...report.removed.length > 0 ? [`${report.removed.length} removed`] : []
6568
7363
  ].join(" \xB7 ");
6569
- say4(
6570
- `${green4("pushed")} ${scanned.skills.length} skill${scanned.skills.length === 1 ? "" : "s"} from ${root} to ${library} \xB7 ${dim4(counts)}`
7364
+ say5(
7365
+ `${green5("pushed")} ${scanned.skills.length} skill${scanned.skills.length === 1 ? "" : "s"} from ${root} to ${library} \xB7 ${dim5(counts)}`
6571
7366
  );
6572
- say4(dim4("sessions receive them from the next launch"));
7367
+ say5(dim5("sessions receive them from the next launch"));
6573
7368
  };
6574
7369
  var skillsCommand = async (config, args) => {
6575
7370
  const [verb, ...rest] = args;
@@ -6582,7 +7377,7 @@ var skillsCommand = async (config, args) => {
6582
7377
  return skillsList(config, args);
6583
7378
  default:
6584
7379
  if (verb.startsWith("--")) return skillsList(config, args);
6585
- return fail4(`unknown skills command "${verb}" \xB7 mend help skills lists them`);
7380
+ return fail5(`unknown skills command "${verb}" \xB7 mend help skills lists them`);
6586
7381
  }
6587
7382
  };
6588
7383
  var completeCommand = async (config, args) => {
@@ -6686,7 +7481,7 @@ var completionsCommand = (args) => {
6686
7481
  process.stdout.write(BASH_COMPLETIONS);
6687
7482
  return;
6688
7483
  default:
6689
- return fail4(`${usageOf("completions")} \xB7 e.g. mend completions zsh > "$fpath[1]/_mend"`);
7484
+ return fail5(`${usageOf("completions")} \xB7 e.g. mend completions zsh > "$fpath[1]/_mend"`);
6690
7485
  }
6691
7486
  };
6692
7487
  var supervisedRun = async (config, session, argv) => {
@@ -6694,15 +7489,15 @@ var supervisedRun = async (config, session, argv) => {
6694
7489
  "provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
6695
7490
  api(config, "POST", `/sessions/${session.id}/launch`, { argv })
6696
7491
  );
6697
- say4(
6698
- `${green4("\u2713 recording")} \xB7 run ${dim4(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
7492
+ say5(
7493
+ `${green5("\u2713 recording")} \xB7 run ${dim5(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
6699
7494
  );
6700
- say4(dim4(` ${argv.join(" ")} \u2014 live record:`));
6701
- say4("");
7495
+ say5(dim5(` ${argv.join(" ")} \u2014 live record:`));
7496
+ say5("");
6702
7497
  const headers = {};
6703
7498
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
6704
7499
  const response = await fetch(`${config.url}/api/events`, { headers });
6705
- if (response.body === null) return fail4("event stream unavailable");
7500
+ if (response.body === null) return fail5("event stream unavailable");
6706
7501
  const reader = response.body.getReader();
6707
7502
  const decoder = new TextDecoder();
6708
7503
  let buffer = "";
@@ -6724,7 +7519,7 @@ var supervisedRun = async (config, session, argv) => {
6724
7519
  }
6725
7520
  if (event.sessionId !== session.id) continue;
6726
7521
  if (event.type === "session-progress" && event.line !== void 0) {
6727
- say4(dim4(` ${event.line}`));
7522
+ say5(dim5(` ${event.line}`));
6728
7523
  }
6729
7524
  if (event.type === "session") {
6730
7525
  const detail = await api(config, "GET", `/sessions/${session.id}`);
@@ -6734,9 +7529,9 @@ var supervisedRun = async (config, session, argv) => {
6734
7529
  }
6735
7530
  }
6736
7531
  await reader.cancel().catch(() => void 0);
6737
- say4("");
6738
- say4(`${green4("\u2713")} session ${settled ?? "settled"} \xB7 recorded \xB7 checkpoint taken`);
6739
- say4(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
7532
+ say5("");
7533
+ say5(`${green5("\u2713")} session ${settled ?? "settled"} \xB7 recorded \xB7 checkpoint taken`);
7534
+ say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
6740
7535
  process.exit(settled === "failed" ? 1 : 0);
6741
7536
  };
6742
7537
  var continueSession = async (config, args) => {
@@ -6759,20 +7554,20 @@ var continueSession = async (config, args) => {
6759
7554
  }
6760
7555
  }
6761
7556
  if (sessionId === null || followUp === null) {
6762
- return fail4("no session with a pending follow-up \u2014 send one from the review first");
7557
+ return fail5("no session with a pending follow-up \u2014 send one from the review first");
6763
7558
  }
6764
7559
  } else {
6765
7560
  followUp = await api(config, "GET", `/sessions/${sessionId}/follow-up`);
6766
- if (followUp === null) return fail4(`session ${sessionId} has no pending follow-up`);
7561
+ if (followUp === null) return fail5(`session ${sessionId} has no pending follow-up`);
6767
7562
  }
6768
7563
  const detail = await api(config, "GET", `/sessions/${sessionId}`);
6769
7564
  const session = detail.session;
6770
- say4(`${green4("\u2713")} follow-up for session ${dim4(session.id.slice(0, 8))} \xB7 ${session.branch}`);
6771
- say4(dim4(" instruction:"));
6772
- for (const line of followUp.instruction.split("\n").slice(0, 6)) say4(dim4(` \u2502 ${line}`));
6773
- if (followUp.instruction.split("\n").length > 6) say4(dim4(" \u2502 \u2026"));
7565
+ say5(`${green5("\u2713")} follow-up for session ${dim5(session.id.slice(0, 8))} \xB7 ${session.branch}`);
7566
+ say5(dim5(" instruction:"));
7567
+ for (const line of followUp.instruction.split("\n").slice(0, 6)) say5(dim5(` \u2502 ${line}`));
7568
+ if (followUp.instruction.split("\n").length > 6) say5(dim5(" \u2502 \u2026"));
6774
7569
  if (followUp.reviewSliceId === null || followUp.checkpointAId === null || followUp.checkpointBId === null || followUp.diffDigest === null || followUp.idempotencyKey === null) {
6775
- return fail4("legacy follow-up \u2014 recreate it from a pinned Review before delivery");
7570
+ return fail5("legacy follow-up \u2014 recreate it from a pinned Review before delivery");
6776
7571
  }
6777
7572
  const delivered = await withSpinner(
6778
7573
  "delivering persisted Review bundle \u2014 retries reconcile one process\u2026",
@@ -6787,16 +7582,16 @@ var continueSession = async (config, args) => {
6787
7582
  })
6788
7583
  );
6789
7584
  if (delivered.status === "pending") {
6790
- return fail4("the session is active \u2014 the follow-up remains pending");
7585
+ return fail5("the session is active \u2014 the follow-up remains pending");
6791
7586
  }
6792
7587
  if (delivered.status === "delivery_failed") {
6793
- return fail4(delivered.deliveryError ?? "delivery failed before process membership finalized");
7588
+ return fail5(delivered.deliveryError ?? "delivery failed before process membership finalized");
6794
7589
  }
6795
- say4(
6796
- `${green4("\u2713 recording")} \xB7 delivered to run ${delivered.deliverySealantRunId ?? "unknown"}${detachHint()}`
7590
+ say5(
7591
+ `${green5("\u2713 recording")} \xB7 delivered to run ${delivered.deliverySealantRunId ?? "unknown"}${detachHint()}`
6797
7592
  );
6798
- say4(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
6799
- say4("");
7593
+ say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
7594
+ say5("");
6800
7595
  await attachOrExit(config, session.id, session.harness);
6801
7596
  exitAfterSessionEnd(config, session.id);
6802
7597
  };
@@ -6813,27 +7608,27 @@ var resumeCommand = async (config, args) => {
6813
7608
  const detail = await api(config, "GET", `/projects/${project2.id}`);
6814
7609
  const match = prefix === void 0 ? detail.sessions.find((s) => !agentLiveIn(detail, s)) : detail.sessions.find((s) => s.id.startsWith(prefix));
6815
7610
  if (match === void 0) {
6816
- return fail4(
7611
+ return fail5(
6817
7612
  prefix === void 0 ? "no settled session to resume \u2014 mend status lists sessions" : `no session matches "${prefix}"`
6818
7613
  );
6819
7614
  }
6820
7615
  if (agentLiveIn(detail, match)) {
6821
- return fail4(
7616
+ return fail5(
6822
7617
  `session ${match.id.slice(0, 8)} is live \u2014 attach: mend attach ${match.id.slice(0, 8)}`
6823
7618
  );
6824
7619
  }
6825
- say4(
6826
- `${green4("\u2713")} resuming ${match.harness} \xB7 ${dim4(match.id.slice(0, 8))}${withHarness === null ? "" : ` ${dim4("as")} ${withHarness}`}`
7620
+ say5(
7621
+ `${green5("\u2713")} resuming ${match.harness} \xB7 ${dim5(match.id.slice(0, 8))}${withHarness === null ? "" : ` ${dim5("as")} ${withHarness}`}`
6827
7622
  );
6828
- say4(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
7623
+ say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
6829
7624
  const priorAgent = detail.annotations.find((annotation) => annotation.sessionId === match.id)?.currentAgent ?? null;
6830
7625
  const protocolPrior = priorAgent?.kind === "agent-protocol" && withHarness === null;
6831
7626
  await withSpinner(
6832
7627
  protocolPrior ? "reopening as a terminal \u2014 same conversation\u2026" : "resuming \u2014 a fresh workspace restores the saved session state\u2026",
6833
7628
  protocolPrior ? api(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : api(config, "POST", `/sessions/${match.id}/resume`, { harness: withHarness })
6834
7629
  );
6835
- say4(`${green4("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
6836
- say4("");
7630
+ say5(`${green5("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
7631
+ say5("");
6837
7632
  await attachOrExit(config, match.id, withHarness ?? match.harness);
6838
7633
  exitAfterSessionEnd(config, match.id);
6839
7634
  };
@@ -6847,7 +7642,7 @@ var resumeForRejoin = async (config, sessionId, label) => {
6847
7642
  } catch (error) {
6848
7643
  const refreshed = await api(config, "GET", `/sessions/${sessionId}`);
6849
7644
  if (agentIsLive(refreshed.session, refreshed.currentAgent)) return false;
6850
- return fail4(error instanceof Error ? error.message : String(error));
7645
+ return fail5(error instanceof Error ? error.message : String(error));
6851
7646
  }
6852
7647
  };
6853
7648
  var rejoinCommand = async (config, args) => {
@@ -6861,20 +7656,20 @@ var rejoinCommand = async (config, args) => {
6861
7656
  const matches = prefix === void 0 ? newest === void 0 ? [] : [newest] : eligible.filter((session2) => session2.id.startsWith(prefix));
6862
7657
  if (matches.length === 0) {
6863
7658
  const harnessDescription = harness === null ? "" : ` ${harness}`;
6864
- return fail4(
7659
+ return fail5(
6865
7660
  prefix === void 0 ? `no${harnessDescription} session to rejoin in project ${project2.name}` : `no${harnessDescription} session matches "${prefix}" in project ${project2.name}`
6866
7661
  );
6867
7662
  }
6868
7663
  if (matches.length > 1) {
6869
- return fail4(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
7664
+ return fail5(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
6870
7665
  }
6871
7666
  const session = matches[0];
6872
- if (session === void 0) return fail4("session selection failed");
7667
+ if (session === void 0) return fail5("session selection failed");
6873
7668
  const alreadyLive = agentLiveIn(detail, session);
6874
- say4(
6875
- `${green4("\u2713")} rejoining ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim4(session.id.slice(0, 8))} \xB7 ${alreadyLive ? "already live" : "restoring"}`
7669
+ say5(
7670
+ `${green5("\u2713")} rejoining ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim5(session.id.slice(0, 8))} \xB7 ${alreadyLive ? "already live" : "restoring"}`
6876
7671
  );
6877
- say4(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
7672
+ say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
6878
7673
  let restored = false;
6879
7674
  const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ?? null;
6880
7675
  if (currentAgent?.kind === "agent-protocol") {
@@ -6890,10 +7685,10 @@ var rejoinCommand = async (config, args) => {
6890
7685
  "resuming \u2014 a fresh workspace restores the saved session state\u2026"
6891
7686
  );
6892
7687
  }
6893
- say4(
6894
- restored ? `${green4("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}` : `${green4("\u2713 recording")} \xB7 attached to the live session${detachHint()}`
7688
+ say5(
7689
+ restored ? `${green5("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}` : `${green5("\u2713 recording")} \xB7 attached to the live session${detachHint()}`
6895
7690
  );
6896
- say4("");
7691
+ say5("");
6897
7692
  let outcome = await attachTty(config, session.id, session.harness, 0n, void 0, {
6898
7693
  handleSignals: true
6899
7694
  });
@@ -6919,7 +7714,7 @@ var projectsCommand = async (config) => {
6919
7714
  api(config, "GET", "/sessions")
6920
7715
  ]);
6921
7716
  if (projects.length === 0) {
6922
- say4(dim4("no adopted projects \u2014 mend adopt brings one in"));
7717
+ say5(dim5("no adopted projects \u2014 mend adopt brings one in"));
6923
7718
  return;
6924
7719
  }
6925
7720
  const liveByProject = /* @__PURE__ */ new Map();
@@ -6931,30 +7726,30 @@ var projectsCommand = async (config) => {
6931
7726
  const here = matchProjectByCwd(projects, cwdFacts(process.cwd()));
6932
7727
  for (const project2 of projects) {
6933
7728
  const live = liveByProject.get(project2.id) ?? 0;
6934
- const liveLabel = live > 0 ? green4(`${live} live`) : dim4("\u2014");
7729
+ const liveLabel = live > 0 ? green5(`${live} live`) : dim5("\u2014");
6935
7730
  const marker = project2.id === here?.id ? cobalt2("\u25B8 ") : " ";
6936
- say4(
6937
- `${marker}${project2.name.padEnd(nameWidth)} ${dim4(project2.defaultBranch.padEnd(branchWidth))} ${liveLabel} ${dim4(project2.storePath)}`
7731
+ say5(
7732
+ `${marker}${project2.name.padEnd(nameWidth)} ${dim5(project2.defaultBranch.padEnd(branchWidth))} ${liveLabel} ${dim5(project2.storePath)}`
6938
7733
  );
6939
7734
  }
6940
- if (here !== void 0) say4(dim4(` \u25B8 ${here.name} is the cwd's project`));
7735
+ if (here !== void 0) say5(dim5(` \u25B8 ${here.name} is the cwd's project`));
6941
7736
  };
6942
7737
  var refreshCommand = async (config, args) => {
6943
- const explicit = args.find((a) => !a.startsWith("--")) ?? takeFlagValue3(args, "--project");
7738
+ const explicit = args.find((a) => !a.startsWith("--")) ?? takeFlagValue4(args, "--project");
6944
7739
  const project2 = await findProject(config, explicit ?? null);
6945
7740
  const branches = await api(
6946
7741
  config,
6947
7742
  "POST",
6948
7743
  `/projects/${project2.id}/refresh`
6949
7744
  );
6950
- say4(`${green4("\u2713")} refreshed ${project2.name} ${dim4(`\xB7 ${branches.length} branches`)}`);
7745
+ say5(`${green5("\u2713")} refreshed ${project2.name} ${dim5(`\xB7 ${branches.length} branches`)}`);
6951
7746
  for (const branch of branches.slice(0, 12)) {
6952
7747
  const marker = branch.isDefault ? cobalt2("\u25B8 ") : " ";
6953
- say4(
6954
- `${marker}${branch.name} ${dim4(branch.sha.slice(0, 12))} ${dim4(branch.committedAt.slice(0, 10))}`
7748
+ say5(
7749
+ `${marker}${branch.name} ${dim5(branch.sha.slice(0, 12))} ${dim5(branch.committedAt.slice(0, 10))}`
6955
7750
  );
6956
7751
  }
6957
- if (branches.length > 12) say4(dim4(` \u2026 ${branches.length - 12} more`));
7752
+ if (branches.length > 12) say5(dim5(` \u2026 ${branches.length - 12} more`));
6958
7753
  };
6959
7754
  var printSessionRow = (row) => {
6960
7755
  const { session, annotation } = row;
@@ -6968,8 +7763,8 @@ var printSessionRow = (row) => {
6968
7763
  if (annotation !== void 0 && annotation.pendingFollowUp)
6969
7764
  facts.push(amber("follow-up pending"));
6970
7765
  const base = session.baseRef === null ? session.baseSha.slice(0, 12) : session.baseRef;
6971
- say4(
6972
- `${session.harness.padEnd(8)} ${dim4(session.id.slice(0, 8))} ${live ? green4(status) : dim4(status)} ${row.projectName} ${dim4(`${session.branch} \xB7 base ${base}`)}${facts.length > 0 ? ` ${facts.join(dim4(" \xB7 "))}` : ""}`
7766
+ say5(
7767
+ `${session.harness.padEnd(8)} ${dim5(session.id.slice(0, 8))} ${live ? green5(status) : dim5(status)} ${row.projectName} ${dim5(`${session.branch} \xB7 base ${base}`)}${facts.length > 0 ? ` ${facts.join(dim5(" \xB7 "))}` : ""}`
6973
7768
  );
6974
7769
  };
6975
7770
  var sessionsCommand = async (config, args) => {
@@ -6977,7 +7772,7 @@ var sessionsCommand = async (config, args) => {
6977
7772
  if (args.includes("--json=v2")) {
6978
7773
  const projectFlag2 = args.indexOf("--project");
6979
7774
  const projectName2 = projectFlag2 !== -1 && args[projectFlag2 + 1] !== void 0 ? String(args[projectFlag2 + 1]) : null;
6980
- say4(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
7775
+ say5(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
6981
7776
  return;
6982
7777
  }
6983
7778
  const json = args.includes("--json");
@@ -6986,7 +7781,7 @@ var sessionsCommand = async (config, args) => {
6986
7781
  const projects = await api(config, "GET", "/projects");
6987
7782
  const scope = projectName === null ? projects : projects.filter((p) => p.name === projectName);
6988
7783
  if (projectName !== null && scope.length === 0) {
6989
- return fail4(`no adopted project named "${projectName}"`);
7784
+ return fail5(`no adopted project named "${projectName}"`);
6990
7785
  }
6991
7786
  let rows;
6992
7787
  if (all || projectName !== null) {
@@ -7013,11 +7808,11 @@ var sessionsCommand = async (config, args) => {
7013
7808
  }
7014
7809
  if (rows.length === 0) {
7015
7810
  if (json) {
7016
- say4(JSON.stringify({ version: 1, sessions: [] }, null, 2));
7811
+ say5(JSON.stringify({ version: 1, sessions: [] }, null, 2));
7017
7812
  return;
7018
7813
  }
7019
- say4(
7020
- dim4(all ? "no sessions" : "no active sessions \u2014 mend sessions --all includes settled ones")
7814
+ say5(
7815
+ dim5(all ? "no sessions" : "no active sessions \u2014 mend sessions --all includes settled ones")
7021
7816
  );
7022
7817
  return;
7023
7818
  }
@@ -7051,7 +7846,7 @@ var sessionsCommand = async (config, args) => {
7051
7846
  }
7052
7847
  }))
7053
7848
  };
7054
- say4(JSON.stringify(payload, null, 2));
7849
+ say5(JSON.stringify(payload, null, 2));
7055
7850
  return;
7056
7851
  }
7057
7852
  for (const row of rows) printSessionRow(row);
@@ -7131,11 +7926,11 @@ var worktreesCommand = async (config, args) => {
7131
7926
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
7132
7927
  const payload = await buildWorktreesJson(config, projectName);
7133
7928
  if (json) {
7134
- say4(JSON.stringify(payload, null, 2));
7929
+ say5(JSON.stringify(payload, null, 2));
7135
7930
  return;
7136
7931
  }
7137
7932
  if (payload.worktrees.length === 0) {
7138
- say4(dim4("no worktrees yet \u2014 mend claude --name <worktree> starts one"));
7933
+ say5(dim5("no worktrees yet \u2014 mend claude --name <worktree> starts one"));
7139
7934
  return;
7140
7935
  }
7141
7936
  for (const worktree of payload.worktrees) {
@@ -7143,19 +7938,19 @@ var worktreesCommand = async (config, args) => {
7143
7938
  const facts = [
7144
7939
  `${worktree.sessions.length} session${worktree.sessions.length === 1 ? "" : "s"}`
7145
7940
  ];
7146
- if (live > 0) facts.push(green4(`${live} live`));
7941
+ if (live > 0) facts.push(green5(`${live} live`));
7147
7942
  if (worktree.review !== null && worktree.review.openComments > 0) {
7148
7943
  facts.push(amber(`${worktree.review.openComments} open`));
7149
7944
  }
7150
7945
  const base = worktree.baseRef ?? worktree.baseSha.slice(0, 12);
7151
- say4(
7152
- `${worktree.name.padEnd(24)} ${worktree.projectName} ${dim4(`${worktree.branch} \xB7 base ${base}`)} ${facts.join(dim4(" \xB7 "))}`
7946
+ say5(
7947
+ `${worktree.name.padEnd(24)} ${worktree.projectName} ${dim5(`${worktree.branch} \xB7 base ${base}`)} ${facts.join(dim5(" \xB7 "))}`
7153
7948
  );
7154
7949
  for (const session of worktree.sessions) {
7155
7950
  const status = session.status.padEnd(9);
7156
7951
  const name = session.label ?? `session ${session.id.slice(0, 8)}`;
7157
- say4(
7158
- ` \u2514 ${session.harness.padEnd(8)} ${dim4(session.id.slice(0, 8))} ${ACTIVE_STATUSES.has(session.status) ? green4(status) : dim4(status)} ${name}`
7952
+ say5(
7953
+ ` \u2514 ${session.harness.padEnd(8)} ${dim5(session.id.slice(0, 8))} ${ACTIVE_STATUSES.has(session.status) ? green5(status) : dim5(status)} ${name}`
7159
7954
  );
7160
7955
  }
7161
7956
  }
@@ -7170,24 +7965,24 @@ var hasNodeFfi = () => {
7170
7965
  };
7171
7966
  var dashboard = async (config) => {
7172
7967
  if (process.stdout.isTTY !== true) {
7173
- say4(renderIndex());
7968
+ say5(renderIndex());
7174
7969
  return;
7175
7970
  }
7176
7971
  if (!hasNodeFfi()) {
7177
7972
  const major = Number(process.versions.node.split(".")[0]);
7178
7973
  if (Number.isNaN(major) || major < 26) {
7179
- return fail4(
7974
+ return fail5(
7180
7975
  `the dashboard needs Node >= 26 (node:ffi) \u2014 this is ${process.version}; every other command still works`
7181
7976
  );
7182
7977
  }
7183
- const rerun = spawnSync2(
7978
+ const rerun = spawnSync3(
7184
7979
  process.execPath,
7185
7980
  ["--experimental-ffi", "--disable-warning=ExperimentalWarning", ...process.argv.slice(1)],
7186
7981
  { stdio: "inherit" }
7187
7982
  );
7188
7983
  process.exit(rerun.status ?? 0);
7189
7984
  }
7190
- const { runDashboard } = await import("./dashboard-G52NHJCZ.js");
7985
+ const { runDashboard } = await import("./dashboard-ZKCPU2QJ.js");
7191
7986
  const agentShare = await startShareIfBridge(config);
7192
7987
  try {
7193
7988
  await runDashboard({
@@ -7213,11 +8008,7 @@ var startShareIfBridge = async (config) => {
7213
8008
  }
7214
8009
  if (access.mode !== "bridge") return null;
7215
8010
  return startAgentShare({
7216
- url: bridgeUrlOf(
7217
- parseMendUrl(config.url).toString().replace(/\/$/, ""),
7218
- config.token,
7219
- os6.hostname()
7220
- ),
8011
+ url: () => socketUrl(config, "keys-bridge", { host: os7.hostname() }),
7221
8012
  agentSock
7222
8013
  });
7223
8014
  };
@@ -7231,38 +8022,38 @@ var withAgentShare = async (config, work) => {
7231
8022
  };
7232
8023
  var helpCommand = (words) => {
7233
8024
  if (words.length === 0) {
7234
- say4(renderIndex());
8025
+ say5(renderIndex());
7235
8026
  return;
7236
8027
  }
7237
8028
  const doc = findCommand(words);
7238
8029
  if (doc !== null && (doc.name.split(" ").length > 1 || words.length === 1)) {
7239
- say4(renderCommand(doc));
8030
+ say5(renderCommand(doc));
7240
8031
  return;
7241
8032
  }
7242
8033
  const group = renderGroup(words[0] ?? "");
7243
8034
  if (doc !== null) {
7244
- say4(renderCommand(doc));
8035
+ say5(renderCommand(doc));
7245
8036
  return;
7246
8037
  }
7247
8038
  if (group !== null) {
7248
- say4(group);
8039
+ say5(group);
7249
8040
  return;
7250
8041
  }
7251
- return fail4(`no command "${words.join(" ")}" \xB7 mend help lists them`);
8042
+ return fail5(`no command "${words.join(" ")}" \xB7 mend help lists them`);
7252
8043
  };
7253
8044
  var manCommand = (words) => {
7254
8045
  const doc = words.length === 0 ? null : findCommand(words);
7255
8046
  if (words.length > 0 && doc === null) {
7256
- return fail4(`no command "${words.join(" ")}" \xB7 mend help lists them`);
8047
+ return fail5(`no command "${words.join(" ")}" \xB7 mend help lists them`);
7257
8048
  }
7258
8049
  const version = cliVersion();
7259
8050
  const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
7260
- const file = path10.join(os6.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
7261
- fs9.writeFileSync(file, page);
7262
- const result = spawnSync2("man", ["-l", file], { stdio: "inherit" });
7263
- fs9.rmSync(file, { force: true });
8051
+ const file = path12.join(os7.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
8052
+ fs11.writeFileSync(file, page);
8053
+ const result = spawnSync3("man", ["-l", file], { stdio: "inherit" });
8054
+ fs11.rmSync(file, { force: true });
7264
8055
  if (result.error !== void 0 || result.status !== 0) {
7265
- say4(doc === null ? renderIndex() : renderCommand(doc));
8056
+ say5(doc === null ? renderIndex() : renderCommand(doc));
7266
8057
  }
7267
8058
  };
7268
8059
  var main = async () => {
@@ -7273,7 +8064,7 @@ var main = async () => {
7273
8064
  }
7274
8065
  if (command === "server") {
7275
8066
  const result = await serverCommand(rest);
7276
- if (result._tag === "error") return fail4(result.message);
8067
+ if (result._tag === "error") return fail5(result.message);
7277
8068
  return;
7278
8069
  }
7279
8070
  const config = loadConfig();
@@ -7311,11 +8102,30 @@ var main = async () => {
7311
8102
  return connectCommand(config, rest);
7312
8103
  case "pair":
7313
8104
  return pairCommand(rest, boundApi(config));
8105
+ case "invite":
8106
+ return inviteCommand(boundApi(config), config.url, rest);
8107
+ case "members":
8108
+ return membersCommand(boundApi(config));
8109
+ case "folder":
8110
+ return folderCommand(boundApi(config), rest);
8111
+ case "operator":
8112
+ return operatorCommand(
8113
+ boundApi(config),
8114
+ (method, route, body) => request(config, method, route, body),
8115
+ config.url,
8116
+ rest
8117
+ );
8118
+ case "session":
8119
+ if (rest[0] === "share") return sessionShareCommand(boundApi(config), rest.slice(1));
8120
+ return fail5(`unknown session command "${rest[0] ?? ""}" \xB7 mend help session share`);
7314
8121
  // Hidden in the catalog: the installer renders its own QR through this.
7315
8122
  case "qr":
7316
8123
  return qrCommand(rest);
7317
8124
  case "doctor":
7318
- return doctorCommand(config, localCredential);
8125
+ return doctorCommand(config, localCredential, () => {
8126
+ const read = readGrant(claudeGrantDir(mendCliHome()));
8127
+ return read.kind === "missing" ? null : read.secret;
8128
+ });
7319
8129
  case "env":
7320
8130
  return envCommand(config, rest);
7321
8131
  case "ssh":
@@ -7352,10 +8162,10 @@ var main = async () => {
7352
8162
  case "--version":
7353
8163
  case "-v":
7354
8164
  for (const line of versionLines(cliVersion(), await fetchServerVersion(config.url)))
7355
- say4(line);
8165
+ say5(line);
7356
8166
  return;
7357
8167
  default:
7358
- return fail4(`unknown command "${command}" \xB7 mend help lists them`);
8168
+ return fail5(`unknown command "${command}" \xB7 mend help lists them`);
7359
8169
  }
7360
8170
  };
7361
8171
  await main();