@sealant/mend 0.28.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +4 -3
  2. package/dist/{chunk-7JPOKREB.js → chunk-C6O4LUE7.js} +238 -8
  3. package/dist/{dashboard-4WQC5MZ4.js → dashboard-JJYC4SNU.js} +1 -1
  4. package/dist/main.js +1470 -662
  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 +1 -1
  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 +3 -3
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-7JPOKREB.js";
23
+ } from "./chunk-C6O4LUE7.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" },
@@ -1015,7 +1147,166 @@ var COMMANDS = [
1015
1147
  ],
1016
1148
  see: ["keys mode", "adopt"]
1017
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
+ },
1018
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
+ },
1019
1310
  {
1020
1311
  name: "server",
1021
1312
  section: "this machine",
@@ -1081,7 +1372,7 @@ var COMMANDS = [
1081
1372
  { command: "mend server setup", text: "localhost, using the current local Docker context" },
1082
1373
  {
1083
1374
  command: "mend server setup --context orbstack --bind 0.0.0.0 --url http://100.70.80.90:3105",
1084
- text: "explicit tailnet exposure"
1375
+ text: "explicit private-network exposure (a tailnet, a LAN, a VPN)"
1085
1376
  },
1086
1377
  {
1087
1378
  command: "mend server setup --version 0.25.0 --assets-dir ./release-assets --offline",
@@ -1518,7 +1809,7 @@ var manFileName = (doc) => doc === null ? "mend.1" : `mend-${doc.name.replace(/
1518
1809
 
1519
1810
  // src/login.ts
1520
1811
  import { spawn } from "node:child_process";
1521
- import * as os from "node:os";
1812
+ import * as os2 from "node:os";
1522
1813
 
1523
1814
  // src/pair.ts
1524
1815
  var paint = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
@@ -1710,7 +2001,7 @@ var resolveServerUrl = async (args, deps) => {
1710
2001
  };
1711
2002
  var loginCommand = async (args, deps) => {
1712
2003
  const base = await resolveServerUrl(args, deps);
1713
- const started = await post(`${base}/api/cli/auth`, { name: os.hostname() });
2004
+ const started = await post(`${base}/api/cli/auth`, { name: os2.hostname() });
1714
2005
  if (started.status === 429) {
1715
2006
  return fail2(
1716
2007
  `the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
@@ -1760,15 +2051,338 @@ var loginCommand = async (args, deps) => {
1760
2051
  return fail2("the authorize request expired before anyone approved \u2014 run mend login again");
1761
2052
  };
1762
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
+
1763
2377
  // src/server-setup.ts
1764
2378
  import { createHmac, randomBytes } from "node:crypto";
1765
2379
  import { readFile } from "node:fs/promises";
1766
2380
  import * as net2 from "node:net";
1767
- import * as os3 from "node:os";
1768
- import * as path5 from "node:path";
2381
+ import * as os4 from "node:os";
2382
+ import * as path7 from "node:path";
1769
2383
 
1770
2384
  // src/server-docker-volumes.ts
1771
- import { createHash } from "node:crypto";
2385
+ import { createHash as createHash2 } from "node:crypto";
1772
2386
  var MEND_DOCKER_NAMESPACE = {
1773
2387
  project: "mend",
1774
2388
  store: "mend-store",
@@ -1796,15 +2410,15 @@ var ServerVolumeOwnershipError = class extends Error {
1796
2410
  this.operation = operation;
1797
2411
  }
1798
2412
  };
1799
- var fail3 = (reason, operation) => ({ _tag: "error", error: new ServerVolumeOwnershipError(reason, operation) });
2413
+ var fail4 = (reason, operation) => ({ _tag: "error", error: new ServerVolumeOwnershipError(reason, operation) });
1800
2414
  var docker = async (runtime, context, args) => {
1801
2415
  try {
1802
2416
  const output = await runtime.run("docker", ["--context", context, ...args]);
1803
2417
  if (output.status !== 0 || output.error !== void 0)
1804
- return fail3("docker", args.slice(0, 2).join(" "));
2418
+ return fail4("docker", args.slice(0, 2).join(" "));
1805
2419
  return { _tag: "ok", value: output.stdout };
1806
2420
  } catch {
1807
- return fail3("docker", args.slice(0, 2).join(" "));
2421
+ return fail4("docker", args.slice(0, 2).join(" "));
1808
2422
  }
1809
2423
  };
1810
2424
  var listNames = async (runtime, context, kind, project2) => {
@@ -1823,11 +2437,11 @@ var listNames = async (runtime, context, kind, project2) => {
1823
2437
  for (const line of output.value.trim().split("\n").filter(Boolean)) {
1824
2438
  const value = JSON.parse(line);
1825
2439
  if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value))
1826
- return fail3("inspection", `${kind} list`);
2440
+ return fail4("inspection", `${kind} list`);
1827
2441
  names.push(value);
1828
2442
  }
1829
2443
  } catch {
1830
- return fail3("inspection", `${kind} list`);
2444
+ return fail4("inspection", `${kind} list`);
1831
2445
  }
1832
2446
  return { _tag: "ok", value: names };
1833
2447
  };
@@ -1843,16 +2457,16 @@ var inspectOwner = async (runtime, context, name, owner) => {
1843
2457
  try {
1844
2458
  const volume = JSON.parse(output.value);
1845
2459
  if (typeof volume !== "object" || volume === null || !("Name" in volume) || volume.Name !== name || !("Labels" in volume))
1846
- return fail3("inspection", "volume inspect");
2460
+ return fail4("inspection", "volume inspect");
1847
2461
  const labels = volume.Labels;
1848
- if (labels === null) return fail3("conflict", "volume owner");
2462
+ if (labels === null) return fail4("conflict", "volume owner");
1849
2463
  if (typeof labels !== "object" || Array.isArray(labels) || !Object.values(labels).every((v) => typeof v === "string"))
1850
- return fail3("inspection", "volume labels");
2464
+ return fail4("inspection", "volume labels");
1851
2465
  if (!(SERVER_VOLUME_OWNER_LABEL in labels) || labels[SERVER_VOLUME_OWNER_LABEL] !== owner)
1852
- return fail3("conflict", "volume owner");
2466
+ return fail4("conflict", "volume owner");
1853
2467
  return { _tag: "ok", value: void 0 };
1854
2468
  } catch {
1855
- return fail3("inspection", "volume inspect");
2469
+ return fail4("inspection", "volume inspect");
1856
2470
  }
1857
2471
  };
1858
2472
  var ensureVolume = async (runtime, context, name, owner) => {
@@ -1891,38 +2505,38 @@ var inspectForeignProject = async (runtime, context, kind, name, project2) => {
1891
2505
  try {
1892
2506
  const resource = JSON.parse(output.value);
1893
2507
  if (typeof resource !== "object" || resource === null || !("Name" in resource) || resource.Name !== (kind === "container" ? `/${name}` : name) || !("Labels" in resource))
1894
- return fail3("inspection", `${kind} inspect`);
2508
+ return fail4("inspection", `${kind} inspect`);
1895
2509
  const labels = resource.Labels;
1896
- if (labels === null) return fail3("unowned-data", `existing ${kind} without anchor`);
2510
+ if (labels === null) return fail4("unowned-data", `existing ${kind} without anchor`);
1897
2511
  if (typeof labels !== "object" || Array.isArray(labels) || !Object.values(labels).every((value) => typeof value === "string"))
1898
- return fail3("inspection", `${kind} labels`);
2512
+ return fail4("inspection", `${kind} labels`);
1899
2513
  if (!("com.docker.compose.project" in labels))
1900
- return fail3("unowned-data", `existing ${kind} without anchor`);
2514
+ return fail4("unowned-data", `existing ${kind} without anchor`);
1901
2515
  const foreignProject = labels["com.docker.compose.project"];
1902
2516
  if (typeof foreignProject !== "string" || !/^[a-z0-9][a-z0-9_-]*$/.test(foreignProject))
1903
- return fail3("inspection", `${kind} project label`);
2517
+ return fail4("inspection", `${kind} project label`);
1904
2518
  if (foreignProject === project2 || !inProjectNamespace(name, foreignProject))
1905
- return fail3("unowned-data", `existing ${kind} without anchor`);
2519
+ return fail4("unowned-data", `existing ${kind} without anchor`);
1906
2520
  return { _tag: "ok", value: void 0 };
1907
2521
  } catch {
1908
- return fail3("inspection", `${kind} inspect`);
2522
+ return fail4("inspection", `${kind} inspect`);
1909
2523
  }
1910
2524
  };
1911
2525
  var refuseOldData = async (runtime, context, namespace, volumes) => {
1912
2526
  if (volumes.some(
1913
2527
  (name) => secondaryVolumesOf(namespace).includes(name) || name.startsWith(`${namespace.project}_`)
1914
2528
  ))
1915
- return fail3("unowned-data", "existing volumes without anchor");
2529
+ return fail4("unowned-data", "existing volumes without anchor");
1916
2530
  for (const kind of ["volume", "container", "network"]) {
1917
2531
  const labelled = await listNames(runtime, context, kind, namespace.project);
1918
2532
  if (labelled._tag === "error") return labelled;
1919
- 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`);
1920
2534
  if (kind !== "volume") {
1921
2535
  const named = await listNames(runtime, context, kind);
1922
2536
  if (named._tag === "error") return named;
1923
2537
  for (const name of named.value) {
1924
2538
  if (reservedResourceName(name, kind, namespace))
1925
- return fail3("unowned-data", `existing ${kind} without anchor`);
2539
+ return fail4("unowned-data", `existing ${kind} without anchor`);
1926
2540
  if (name.startsWith(`${namespace.project}_`) || name.startsWith(`${namespace.project}-`)) {
1927
2541
  const foreign = await inspectForeignProject(
1928
2542
  runtime,
@@ -1943,8 +2557,8 @@ var parseOwnershipInput = (input) => {
1943
2557
  if (!input.dockerContext.trim() || input.identityBytes.byteLength === 0 || !/^[a-z0-9][a-z0-9_-]*$/.test(namespace.project) || ![namespace.store, ...secondaryVolumesOf(namespace)].every(
1944
2558
  (name) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/.test(name)
1945
2559
  ) || (/* @__PURE__ */ new Set([namespace.store, ...secondaryVolumesOf(namespace)])).size !== 1 + secondaryVolumesOf(namespace).length)
1946
- return fail3("invalid-input", "ownership inputs");
1947
- 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");
1948
2562
  return { _tag: "ok", value: { owner, namespace } };
1949
2563
  };
1950
2564
  var ownership = async (runtime, input, mode) => {
@@ -1957,7 +2571,7 @@ var ownership = async (runtime, input, mode) => {
1957
2571
  const anchor = await inspectOwner(runtime, input.dockerContext, namespace.store, owner);
1958
2572
  if (anchor._tag === "error") return anchor;
1959
2573
  } else {
1960
- if (mode === "verify") return fail3("missing", "anchor");
2574
+ if (mode === "verify") return fail4("missing", "anchor");
1961
2575
  const empty = await refuseOldData(runtime, input.dockerContext, namespace, volumes.value);
1962
2576
  if (empty._tag === "error") return empty;
1963
2577
  const anchor = await ensureVolume(runtime, input.dockerContext, namespace.store, owner);
@@ -1971,7 +2585,7 @@ var ownership = async (runtime, input, mode) => {
1971
2585
  const owned = await inspectOwner(runtime, input.dockerContext, name, owner);
1972
2586
  if (owned._tag === "error") return owned;
1973
2587
  } else {
1974
- if (mode === "verify") return fail3("missing", role);
2588
+ if (mode === "verify") return fail4("missing", role);
1975
2589
  const created = await ensureVolume(runtime, input.dockerContext, name, owner);
1976
2590
  if (created._tag === "error") return created;
1977
2591
  }
@@ -1983,8 +2597,8 @@ var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "v
1983
2597
 
1984
2598
  // src/server-runtime.ts
1985
2599
  import { spawn as spawn2 } from "node:child_process";
1986
- import * as fs3 from "node:fs";
1987
- import * as path3 from "node:path";
2600
+ import * as fs5 from "node:fs";
2601
+ import * as path5 from "node:path";
1988
2602
  var serverProcessDeadlines = {
1989
2603
  ordinary: 3e4,
1990
2604
  pull: 10 * 6e4,
@@ -2030,7 +2644,7 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
2030
2644
  return new Promise((resolve4) => {
2031
2645
  let fd;
2032
2646
  try {
2033
- 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);
2034
2648
  const child = spawn2(command, [...args], {
2035
2649
  env,
2036
2650
  detached: process.platform !== "win32",
@@ -2081,12 +2695,12 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
2081
2695
  child.once("close", (status) => {
2082
2696
  globalThis.clearTimeout(timer);
2083
2697
  try {
2084
- if (fd !== void 0) fs3.fsyncSync(fd);
2698
+ if (fd !== void 0) fs5.fsyncSync(fd);
2085
2699
  } catch {
2086
2700
  failure2 ??= "Could not durably write process output.";
2087
2701
  } finally {
2088
2702
  try {
2089
- if (fd !== void 0) fs3.closeSync(fd);
2703
+ if (fd !== void 0) fs5.closeSync(fd);
2090
2704
  } catch {
2091
2705
  failure2 ??= "Could not close process output file.";
2092
2706
  }
@@ -2100,7 +2714,7 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
2100
2714
  });
2101
2715
  } catch (cause) {
2102
2716
  try {
2103
- if (fd !== void 0) fs3.closeSync(fd);
2717
+ if (fd !== void 0) fs5.closeSync(fd);
2104
2718
  } catch {
2105
2719
  }
2106
2720
  resolve4({
@@ -2121,17 +2735,17 @@ var serverComposeArgs = (installation, command) => [
2121
2735
  "--project-directory",
2122
2736
  installation.directory,
2123
2737
  "--env-file",
2124
- path3.join(installation.directory, "server.env"),
2738
+ path5.join(installation.directory, "server.env"),
2125
2739
  "-f",
2126
- path3.join(installation.directory, "compose.yaml"),
2740
+ path5.join(installation.directory, "compose.yaml"),
2127
2741
  ...command
2128
2742
  ];
2129
2743
 
2130
2744
  // src/server-store.ts
2131
2745
  import { randomUUID } from "node:crypto";
2132
- import * as fs4 from "node:fs";
2133
- import * as os2 from "node:os";
2134
- 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";
2135
2749
  var ServerStoreError = class extends Error {
2136
2750
  _tag = "ServerStoreError";
2137
2751
  };
@@ -2152,28 +2766,28 @@ var attempt = (operation) => {
2152
2766
  var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
2153
2767
  var readOptional = (file) => {
2154
2768
  try {
2155
- return fs4.readFileSync(file, "utf8");
2769
+ return fs6.readFileSync(file, "utf8");
2156
2770
  } catch (cause) {
2157
2771
  if (hasCode(cause, "ENOENT")) return null;
2158
2772
  throw cause;
2159
2773
  }
2160
2774
  };
2161
2775
  var syncDirectory = (directory) => {
2162
- const fd = fs4.openSync(directory, "r");
2776
+ const fd = fs6.openSync(directory, "r");
2163
2777
  try {
2164
- fs4.fsyncSync(fd);
2778
+ fs6.fsyncSync(fd);
2165
2779
  } finally {
2166
- fs4.closeSync(fd);
2780
+ fs6.closeSync(fd);
2167
2781
  }
2168
2782
  };
2169
2783
  var writeDurable = (file, content, mode = 384) => {
2170
- const fd = fs4.openSync(file, "wx", mode);
2784
+ const fd = fs6.openSync(file, "wx", mode);
2171
2785
  try {
2172
- fs4.writeFileSync(fd, content, "utf8");
2173
- fs4.fchmodSync(fd, mode);
2174
- fs4.fsyncSync(fd);
2786
+ fs6.writeFileSync(fd, content, "utf8");
2787
+ fs6.fchmodSync(fd, mode);
2788
+ fs6.fsyncSync(fd);
2175
2789
  } finally {
2176
- fs4.closeSync(fd);
2790
+ fs6.closeSync(fd);
2177
2791
  }
2178
2792
  };
2179
2793
  var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
@@ -2193,11 +2807,11 @@ var parseLockOwner = (raw) => {
2193
2807
  };
2194
2808
  var describeOwner = (lockDir) => {
2195
2809
  try {
2196
- 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"));
2197
2811
  const owner = parseLockOwner(raw);
2198
2812
  if (owner === null) return "owner metadata is invalid";
2199
2813
  const label = `PID ${owner.pid} on ${owner.hostname}`;
2200
- if (owner.hostname !== os2.hostname()) return label;
2814
+ if (owner.hostname !== os3.hostname()) return label;
2201
2815
  try {
2202
2816
  process.kill(owner.pid, 0);
2203
2817
  return `${label} is still live`;
@@ -2213,28 +2827,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
2213
2827
  );
2214
2828
  var acquireLock = (configDir, create) => {
2215
2829
  if (create) {
2216
- fs4.mkdirSync(configDir, { recursive: true, mode: 448 });
2217
- fs4.chmodSync(configDir, 448);
2218
- } else if (!fs4.existsSync(configDir)) {
2830
+ fs6.mkdirSync(configDir, { recursive: true, mode: 448 });
2831
+ fs6.chmodSync(configDir, 448);
2832
+ } else if (!fs6.existsSync(configDir)) {
2219
2833
  throw new ServerStoreError(
2220
2834
  "No Mend server is configured. Run mend server setup explicitly to install one."
2221
2835
  );
2222
2836
  }
2223
- const lockDir = path4.join(configDir, "server.lock");
2837
+ const lockDir = path6.join(configDir, "server.lock");
2224
2838
  try {
2225
- fs4.mkdirSync(lockDir, { mode: 448 });
2839
+ fs6.mkdirSync(lockDir, { mode: 448 });
2226
2840
  } catch (cause) {
2227
2841
  if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
2228
2842
  throw cause;
2229
2843
  }
2230
- const owned = fs4.statSync(lockDir);
2231
- const ownerFile = path4.join(lockDir, "owner.json");
2232
- 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() })}
2233
2847
  `;
2234
2848
  let open = true;
2235
2849
  const assertOwned = () => {
2236
- const current = fs4.statSync(lockDir);
2237
- 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) {
2238
2852
  throw new ServerStoreError(
2239
2853
  "Server lock ownership was lost. Stop and check for another server command before retrying."
2240
2854
  );
@@ -2250,8 +2864,8 @@ var acquireLock = (configDir, create) => {
2250
2864
  release: () => {
2251
2865
  const result = attempt(() => {
2252
2866
  assertOwned();
2253
- fs4.unlinkSync(ownerFile);
2254
- fs4.rmdirSync(lockDir);
2867
+ fs6.unlinkSync(ownerFile);
2868
+ fs6.rmdirSync(lockDir);
2255
2869
  });
2256
2870
  open = false;
2257
2871
  return result;
@@ -2260,7 +2874,7 @@ var acquireLock = (configDir, create) => {
2260
2874
  };
2261
2875
  var readIdentity = (paths) => {
2262
2876
  const identity = readOptional(paths.identity);
2263
- 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)) {
2264
2878
  throw new ServerStoreError(
2265
2879
  "Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
2266
2880
  );
@@ -2270,7 +2884,7 @@ var readIdentity = (paths) => {
2270
2884
  var readActive = (paths) => {
2271
2885
  let target;
2272
2886
  try {
2273
- target = fs4.readlinkSync(paths.active);
2887
+ target = fs6.readlinkSync(paths.active);
2274
2888
  } catch (cause) {
2275
2889
  if (hasCode(cause, "ENOENT")) return null;
2276
2890
  throw cause;
@@ -2280,13 +2894,13 @@ var readActive = (paths) => {
2280
2894
  "Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
2281
2895
  );
2282
2896
  }
2283
- const directory = path4.join(paths.configDir, target);
2897
+ const directory = path6.join(paths.configDir, target);
2284
2898
  const files = {
2285
- identity: fs4.readFileSync(path4.join(directory, fileNames.identity), "utf8"),
2286
- config: fs4.readFileSync(path4.join(directory, fileNames.config), "utf8"),
2287
- env: fs4.readFileSync(path4.join(directory, fileNames.env), "utf8"),
2288
- compose: fs4.readFileSync(path4.join(directory, fileNames.compose), "utf8"),
2289
- 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")
2290
2904
  };
2291
2905
  if (readIdentity(paths) !== files.identity) {
2292
2906
  throw new ServerStoreError(
@@ -2296,20 +2910,20 @@ var readActive = (paths) => {
2296
2910
  return { directory, files };
2297
2911
  };
2298
2912
  var publishIdentity = (paths, identity) => {
2299
- const temporary = path4.join(paths.configDir, `.identity-${randomUUID()}`);
2913
+ const temporary = path6.join(paths.configDir, `.identity-${randomUUID()}`);
2300
2914
  writeDurable(temporary, identity);
2301
- fs4.linkSync(temporary, paths.identity);
2915
+ fs6.linkSync(temporary, paths.identity);
2302
2916
  syncDirectory(paths.configDir);
2303
- fs4.unlinkSync(temporary);
2917
+ fs6.unlinkSync(temporary);
2304
2918
  };
2305
2919
  var prepareGeneration = (paths, files) => {
2306
- fs4.mkdirSync(paths.generations, { recursive: true, mode: 448 });
2920
+ fs6.mkdirSync(paths.generations, { recursive: true, mode: 448 });
2307
2921
  const generationName = `gen-${randomUUID()}`;
2308
- const directory = path4.join(paths.generations, generationName);
2309
- fs4.mkdirSync(directory, { mode: 448 });
2922
+ const directory = path6.join(paths.generations, generationName);
2923
+ fs6.mkdirSync(directory, { mode: 448 });
2310
2924
  for (const key of fileKeys) {
2311
2925
  writeDurable(
2312
- path4.join(directory, fileNames[key]),
2926
+ path6.join(directory, fileNames[key]),
2313
2927
  files[key],
2314
2928
  key === "postgresInit" ? 493 : 384
2315
2929
  );
@@ -2320,18 +2934,18 @@ var prepareGeneration = (paths, files) => {
2320
2934
  return { directory, files };
2321
2935
  };
2322
2936
  var activateGeneration = (paths, generation) => {
2323
- const relative3 = path4.relative(paths.configDir, generation.directory);
2324
- 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")) {
2325
2939
  throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
2326
2940
  }
2327
2941
  for (const key of fileKeys) {
2328
- 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]) {
2329
2943
  throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
2330
2944
  }
2331
2945
  }
2332
- const pointer = path4.join(paths.configDir, `.active-${randomUUID()}`);
2333
- fs4.symlinkSync(relative3, pointer);
2334
- 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);
2335
2949
  syncDirectory(paths.configDir);
2336
2950
  };
2337
2951
  var prepareFiles = (paths, files) => {
@@ -2340,7 +2954,7 @@ var prepareFiles = (paths, files) => {
2340
2954
  throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
2341
2955
  }
2342
2956
  const active = readActive(paths);
2343
- 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)
2344
2958
  return active;
2345
2959
  if (identity === null) publishIdentity(paths, files.identity);
2346
2960
  return prepareGeneration(paths, files);
@@ -2353,9 +2967,9 @@ var commitGeneration = (paths, files) => {
2353
2967
  var createStore = (configDir, lock) => {
2354
2968
  const paths = {
2355
2969
  configDir,
2356
- identity: path4.join(configDir, "identity.env"),
2357
- generations: path4.join(configDir, "generations"),
2358
- active: path4.join(configDir, "active")
2970
+ identity: path6.join(configDir, "identity.env"),
2971
+ generations: path6.join(configDir, "generations"),
2972
+ active: path6.join(configDir, "active")
2359
2973
  };
2360
2974
  const whileOwned = (operation) => attempt(() => {
2361
2975
  lock.assertOwned();
@@ -2368,13 +2982,13 @@ var createStore = (configDir, lock) => {
2368
2982
  prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
2369
2983
  activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
2370
2984
  createBackup: (previous, target) => whileOwned(() => {
2371
- const backups = path4.join(configDir, "backups");
2372
- fs4.mkdirSync(backups, { recursive: true, mode: 448 });
2373
- fs4.chmodSync(backups, 448);
2374
- const directory = path4.join(backups, `upgrade-${randomUUID()}`);
2375
- 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 });
2376
2990
  writeDurable(
2377
- path4.join(directory, "recovery.json"),
2991
+ path6.join(directory, "recovery.json"),
2378
2992
  `${JSON.stringify(
2379
2993
  {
2380
2994
  previousGeneration: previous.directory,
@@ -2390,21 +3004,21 @@ var createStore = (configDir, lock) => {
2390
3004
  syncDirectory(directory);
2391
3005
  syncDirectory(backups);
2392
3006
  syncDirectory(configDir);
2393
- const partialFile = path4.join(directory, "database.sql.partial");
3007
+ const partialFile = path6.join(directory, "database.sql.partial");
2394
3008
  return {
2395
3009
  directory,
2396
3010
  partialFile,
2397
3011
  complete: () => whileOwned(() => {
2398
- if (fs4.statSync(partialFile).size === 0)
3012
+ if (fs6.statSync(partialFile).size === 0)
2399
3013
  throw new ServerStoreError("Database backup is empty; refusing target startup.");
2400
- const fd = fs4.openSync(partialFile, "r");
3014
+ const fd = fs6.openSync(partialFile, "r");
2401
3015
  try {
2402
- fs4.fchmodSync(fd, 384);
2403
- fs4.fsyncSync(fd);
3016
+ fs6.fchmodSync(fd, 384);
3017
+ fs6.fsyncSync(fd);
2404
3018
  } finally {
2405
- fs4.closeSync(fd);
3019
+ fs6.closeSync(fd);
2406
3020
  }
2407
- fs4.renameSync(partialFile, path4.join(directory, "database.sql"));
3021
+ fs6.renameSync(partialFile, path6.join(directory, "database.sql"));
2408
3022
  syncDirectory(directory);
2409
3023
  })
2410
3024
  };
@@ -2413,7 +3027,7 @@ var createStore = (configDir, lock) => {
2413
3027
  };
2414
3028
  var refuseFlatLayout = (configDir) => {
2415
3029
  if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
2416
- (name) => fs4.existsSync(path4.join(configDir, name))
3030
+ (name) => fs6.existsSync(path6.join(configDir, name))
2417
3031
  )) {
2418
3032
  throw new ServerStoreError(
2419
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."
@@ -2421,7 +3035,7 @@ var refuseFlatLayout = (configDir) => {
2421
3035
  }
2422
3036
  };
2423
3037
  var withServerStore = async (directory, operation, options = {}) => {
2424
- const configDir = path4.resolve(directory);
3038
+ const configDir = path6.resolve(directory);
2425
3039
  let lock;
2426
3040
  let result;
2427
3041
  try {
@@ -2438,9 +3052,9 @@ var withServerStore = async (directory, operation, options = {}) => {
2438
3052
  };
2439
3053
 
2440
3054
  // src/version.ts
2441
- import * as fs5 from "node:fs";
3055
+ import * as fs7 from "node:fs";
2442
3056
  var cliVersion = () => {
2443
- 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");
2444
3058
  const parsed = JSON.parse(raw);
2445
3059
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
2446
3060
  const version = parsed.version;
@@ -3119,7 +3733,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
3119
3733
  return version;
3120
3734
  };
3121
3735
  var parseDockerSocketPath = (socket) => {
3122
- if (!path5.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
3736
+ if (!path7.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
3123
3737
  throw setupError(
3124
3738
  "--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
3125
3739
  );
@@ -3145,8 +3759,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
3145
3759
  if (options.assetsDir !== void 0) {
3146
3760
  try {
3147
3761
  const [compose2, postgresInit2] = await Promise.all([
3148
- readFile(path5.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
3149
- 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")
3150
3764
  ]);
3151
3765
  validateComposeAsset(compose2);
3152
3766
  validatePostgresAsset(postgresInit2);
@@ -3659,10 +4273,10 @@ var manageServer = async (command, args, runtime, store) => {
3659
4273
  };
3660
4274
  var nodeServerRuntime = () => {
3661
4275
  const environment = { ...process.env };
3662
- const home = os3.homedir();
3663
- 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");
3664
4278
  return {
3665
- configDir: path5.join(configHome, "mend"),
4279
+ configDir: path7.join(configHome, "mend"),
3666
4280
  platform: process.platform,
3667
4281
  cliVersion: cliVersion(),
3668
4282
  run: (command, args, options) => runServerProcess(command, args, environment, options),
@@ -3888,8 +4502,8 @@ var renderMendToml = (proposals) => {
3888
4502
  };
3889
4503
 
3890
4504
  // src/skills.ts
3891
- import * as fs6 from "node:fs";
3892
- import * as path6 from "node:path";
4505
+ import * as fs8 from "node:fs";
4506
+ import * as path8 from "node:path";
3893
4507
  var DEFAULT_SKILLS_DIR = ".agents/skills";
3894
4508
  var MAX_SKILL_FILES = 64;
3895
4509
  var MAX_SKILL_FILE_BYTES = 512 * 1024;
@@ -3912,12 +4526,12 @@ var readFrontmatterDescription = (contents) => {
3912
4526
  var walkFiles = (root) => {
3913
4527
  const found = [];
3914
4528
  const visit = (dir) => {
3915
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
4529
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
3916
4530
  if (IGNORED_DIRS.has(entry.name)) continue;
3917
- const absolute = path6.join(dir, entry.name);
4531
+ const absolute = path8.join(dir, entry.name);
3918
4532
  let stat;
3919
4533
  try {
3920
- stat = fs6.statSync(absolute);
4534
+ stat = fs8.statSync(absolute);
3921
4535
  } catch {
3922
4536
  continue;
3923
4537
  }
@@ -3931,7 +4545,7 @@ var walkFiles = (root) => {
3931
4545
  var scanSkillLibrary = (root) => {
3932
4546
  let entries;
3933
4547
  try {
3934
- entries = fs6.readdirSync(root, { withFileTypes: true });
4548
+ entries = fs8.readdirSync(root, { withFileTypes: true });
3935
4549
  } catch {
3936
4550
  return { error: `${root} is not a readable directory` };
3937
4551
  }
@@ -3939,23 +4553,23 @@ var scanSkillLibrary = (root) => {
3939
4553
  const notes = [];
3940
4554
  for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
3941
4555
  if (entry.name.startsWith(".")) continue;
3942
- const skillDir = path6.join(root, entry.name);
4556
+ const skillDir = path8.join(root, entry.name);
3943
4557
  let stat;
3944
4558
  try {
3945
- stat = fs6.statSync(skillDir);
4559
+ stat = fs8.statSync(skillDir);
3946
4560
  } catch {
3947
4561
  continue;
3948
4562
  }
3949
4563
  if (!stat.isDirectory()) continue;
3950
- if (!fs6.existsSync(path6.join(skillDir, "SKILL.md"))) {
4564
+ if (!fs8.existsSync(path8.join(skillDir, "SKILL.md"))) {
3951
4565
  notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
3952
4566
  continue;
3953
4567
  }
3954
4568
  const files = [];
3955
4569
  let bytes = 0;
3956
4570
  for (const absolute of walkFiles(skillDir)) {
3957
- const relative3 = path6.relative(skillDir, absolute).split(path6.sep).join("/");
3958
- const buffer = fs6.readFileSync(absolute);
4571
+ const relative3 = path8.relative(skillDir, absolute).split(path8.sep).join("/");
4572
+ const buffer = fs8.readFileSync(absolute);
3959
4573
  if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
3960
4574
  notes.push({ skill: entry.name, message: `${relative3} is over 512KB \u2014 skipped` });
3961
4575
  continue;
@@ -3990,16 +4604,16 @@ var scanSkillLibrary = (root) => {
3990
4604
  };
3991
4605
 
3992
4606
  // src/ssh-setup.ts
3993
- import * as os5 from "node:os";
3994
- import * as path8 from "node:path";
4607
+ import * as os6 from "node:os";
4608
+ import * as path10 from "node:path";
3995
4609
 
3996
4610
  // ../../packages/workspace-ssh/src/index.ts
3997
- import { spawnSync } from "node:child_process";
3998
- import { createHash as createHash2 } from "node:crypto";
3999
- 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";
4000
4614
  import { isIP as isIP2 } from "node:net";
4001
- import * as os4 from "node:os";
4002
- import * as path7 from "node:path";
4615
+ import * as os5 from "node:os";
4616
+ import * as path9 from "node:path";
4003
4617
  var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
4004
4618
  var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
4005
4619
  var WorkspaceSshError = class extends Error {
@@ -4017,7 +4631,7 @@ var WorkspaceSshError = class extends Error {
4017
4631
  var success = (value) => ({ ok: true, value });
4018
4632
  var failure = (error) => ({ ok: false, error });
4019
4633
  var stripIpv6Brackets = (hostname6) => hostname6.startsWith("[") && hostname6.endsWith("]") ? hostname6.slice(1, -1) : hostname6;
4020
- 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);
4021
4635
  var aliasLabel = (hostname6) => {
4022
4636
  const label = hostname6.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
4023
4637
  return label === "" ? "server" : label;
@@ -4086,8 +4700,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
4086
4700
  var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
4087
4701
  var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
4088
4702
  var parseIdentityPath = (value) => {
4089
- const resolved = path7.resolve(
4090
- 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
4091
4705
  );
4092
4706
  if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
4093
4707
  return failure(
@@ -4222,7 +4836,7 @@ var workspaceSshPublicKeyFingerprint = (publicKey) => {
4222
4836
  return failure(new WorkspaceSshError("key", "SSH public key has no encoded key data."));
4223
4837
  }
4224
4838
  try {
4225
- const digest = createHash2("sha256").update(Buffer.from(encoded, "base64")).digest("base64");
4839
+ const digest = createHash3("sha256").update(Buffer.from(encoded, "base64")).digest("base64");
4226
4840
  return success(`SHA256:${digest.replace(/=+$/, "")}`);
4227
4841
  } catch (cause) {
4228
4842
  return failure(
@@ -4233,7 +4847,7 @@ var workspaceSshPublicKeyFingerprint = (publicKey) => {
4233
4847
  var publicKeyIdentity = (publicKey) => publicKey.trim().split(/\s+/).slice(0, 2).join(" ");
4234
4848
  var agentKeys = () => {
4235
4849
  if (!process.env["SSH_AUTH_SOCK"]) return [];
4236
- const listed = spawnSync("ssh-add", ["-L"], {
4850
+ const listed = spawnSync2("ssh-add", ["-L"], {
4237
4851
  encoding: "utf8",
4238
4852
  timeout: 3e3,
4239
4853
  stdio: ["ignore", "pipe", "pipe"],
@@ -4243,7 +4857,7 @@ var agentKeys = () => {
4243
4857
  };
4244
4858
  var readText = (file) => {
4245
4859
  try {
4246
- return success(fs7.readFileSync(file, "utf8"));
4860
+ return success(fs9.readFileSync(file, "utf8"));
4247
4861
  } catch (cause) {
4248
4862
  const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
4249
4863
  return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
@@ -4270,7 +4884,7 @@ var readRequiredKey = (keyPath, source) => {
4270
4884
  const privatePath = publicPath.slice(0, -".pub".length);
4271
4885
  const read = readText(publicPath);
4272
4886
  if (!read.ok) return read;
4273
- const derived = spawnSync("ssh-keygen", ["-y", "-P", "", "-f", privatePath], {
4887
+ const derived = spawnSync2("ssh-keygen", ["-y", "-P", "", "-f", privatePath], {
4274
4888
  encoding: "utf8",
4275
4889
  timeout: 3e3,
4276
4890
  stdio: ["ignore", "pipe", "pipe"],
@@ -4301,19 +4915,19 @@ var readRequiredKey = (keyPath, source) => {
4301
4915
  var readOptionalKey = (keyPath) => {
4302
4916
  const parsed = parseIdentityPath(keyPath);
4303
4917
  if (!parsed.ok) return parsed;
4304
- if (!fs7.existsSync(parsed.value) && !fs7.existsSync(publicKeyPath(parsed.value)))
4918
+ if (!fs9.existsSync(parsed.value) && !fs9.existsSync(publicKeyPath(parsed.value)))
4305
4919
  return success(null);
4306
4920
  return readRequiredKey(parsed.value, "existing");
4307
4921
  };
4308
4922
  var pinAgentKey = (configHome, publicKey) => {
4309
- const identityFile = path7.resolve(
4923
+ const identityFile = path9.resolve(
4310
4924
  configHome,
4311
4925
  "ssh",
4312
4926
  `agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
4313
4927
  );
4314
4928
  try {
4315
- fs7.mkdirSync(path7.dirname(identityFile), { recursive: true, mode: 448 });
4316
- fs7.writeFileSync(identityFile, `${publicKey}
4929
+ fs9.mkdirSync(path9.dirname(identityFile), { recursive: true, mode: 448 });
4930
+ fs9.writeFileSync(identityFile, `${publicKey}
4317
4931
  `, { mode: 384 });
4318
4932
  return success({ publicKey, identityFile, source: "agent" });
4319
4933
  } catch (cause) {
@@ -4324,15 +4938,15 @@ var pinAgentKey = (configHome, publicKey) => {
4324
4938
  };
4325
4939
  var generateDedicatedKey = (privatePath) => {
4326
4940
  try {
4327
- fs7.mkdirSync(path7.dirname(privatePath), { recursive: true, mode: 448 });
4941
+ fs9.mkdirSync(path9.dirname(privatePath), { recursive: true, mode: 448 });
4328
4942
  } catch (cause) {
4329
4943
  return failure(
4330
- new WorkspaceSshError("key", `Could not create ${path7.dirname(privatePath)}.`, cause)
4944
+ new WorkspaceSshError("key", `Could not create ${path9.dirname(privatePath)}.`, cause)
4331
4945
  );
4332
4946
  }
4333
- const generated = spawnSync(
4947
+ const generated = spawnSync2(
4334
4948
  "ssh-keygen",
4335
- ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os4.hostname()}`],
4949
+ ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os5.hostname()}`],
4336
4950
  { encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
4337
4951
  );
4338
4952
  if (generated.status !== 0) {
@@ -4356,7 +4970,7 @@ var generateDedicatedKey = (privatePath) => {
4356
4970
  return fresh;
4357
4971
  };
4358
4972
  var dedicatedKey = (configHome, create) => {
4359
- const privatePath = path7.join(configHome, "ssh", "id_ed25519");
4973
+ const privatePath = path9.join(configHome, "ssh", "id_ed25519");
4360
4974
  const existing = readOptionalKey(privatePath);
4361
4975
  if (existing.ok === false) return failure(existing.error);
4362
4976
  if (existing.value !== null || !create) return existing;
@@ -4404,10 +5018,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
4404
5018
  const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
4405
5019
  if (reconciled.ok === false) return failure(reconciled.error);
4406
5020
  try {
4407
- fs7.mkdirSync(path7.dirname(configFile), { recursive: true, mode: 448 });
4408
- fs7.chmodSync(path7.dirname(configFile), 448);
4409
- fs7.writeFileSync(configFile, reconciled.value, { mode: 384 });
4410
- 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);
4411
5025
  return success(void 0);
4412
5026
  } catch (cause) {
4413
5027
  return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
@@ -4416,23 +5030,23 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
4416
5030
 
4417
5031
  // src/ssh-setup.ts
4418
5032
  var ansi = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
4419
- var dim3 = ansi("2");
4420
- var green3 = ansi("32");
5033
+ var dim4 = ansi("2");
5034
+ var green4 = ansi("32");
4421
5035
  var warn = ansi("33");
4422
- var say3 = (line) => console.log(line);
4423
- var sshConfigPath = () => path8.join(os5.homedir(), ".ssh", "config");
5036
+ var say4 = (line) => console.log(line);
5037
+ var sshConfigPath = () => path10.join(os6.homedir(), ".ssh", "config");
4424
5038
  var flagValue = (args, flag) => {
4425
5039
  const index = args.indexOf(flag);
4426
5040
  return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
4427
5041
  };
4428
5042
  var showFailure = (message) => {
4429
- say3(`mend: ${message}`);
5043
+ say4(`mend: ${message}`);
4430
5044
  process.exitCode = 1;
4431
5045
  };
4432
5046
  var showStatus = async (api2, cliHome, serverUrl, args) => {
4433
5047
  const view = await api2("GET", "/workspace-ssh");
4434
5048
  if (view.gateway === null) {
4435
- 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")}`);
4436
5050
  return;
4437
5051
  }
4438
5052
  const parsedTarget = parseWorkspaceSshTarget({
@@ -4455,16 +5069,16 @@ var showStatus = async (api2, cliHome, serverUrl, args) => {
4455
5069
  key: picked.value,
4456
5070
  registeredFingerprints: view.keys.map((key) => key.fingerprint)
4457
5071
  });
4458
- say3(
4459
- `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}`)}`
4460
5074
  );
4461
- say3(
4462
- 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}`)}`
4463
5077
  );
4464
- say3(
4465
- 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")}`
4466
5080
  );
4467
- say3(
5081
+ say4(
4468
5082
  "host trust not checked \xB7 status checks config and client-key registration, not an SSH connection"
4469
5083
  );
4470
5084
  };
@@ -4490,7 +5104,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
4490
5104
  if (picked.value === null) return showFailure("No workspace SSH key is available.");
4491
5105
  const registered = await api2("POST", "/workspace-ssh/keys", {
4492
5106
  publicKey: picked.value.publicKey,
4493
- name: os5.hostname()
5107
+ name: os6.hostname()
4494
5108
  });
4495
5109
  const sourceLabel = {
4496
5110
  explicit: "from --key",
@@ -4498,22 +5112,22 @@ var setup = async (api2, cliHome, serverUrl, args) => {
4498
5112
  existing: "existing selected key",
4499
5113
  generated: "generated dedicated key"
4500
5114
  }[picked.value.source];
4501
- say3(`key ${green3("\u25CF")} ${registered.fingerprint} ${dim3(`\xB7 ${sourceLabel}`)}`);
5115
+ say4(`key ${green4("\u25CF")} ${registered.fingerprint} ${dim4(`\xB7 ${sourceLabel}`)}`);
4502
5116
  const written = writeWorkspaceSshConfig(
4503
5117
  sshConfigPath(),
4504
5118
  parsedTarget.value,
4505
5119
  picked.value.identityFile
4506
5120
  );
4507
5121
  if (written.ok === false) return showFailure(written.error.message);
4508
- say3(
4509
- `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()}`)}`
4510
5124
  );
4511
- say3(
5125
+ say4(
4512
5126
  "host trust not checked \xB7 SSH verifies the gateway when you connect; setup does not replace known_hosts entries"
4513
5127
  );
4514
- say3("");
4515
- say3(
4516
- `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")}`
4517
5131
  );
4518
5132
  };
4519
5133
  var sshCommand = async (args, api2, cliHome, serverUrl) => {
@@ -4532,8 +5146,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
4532
5146
  };
4533
5147
 
4534
5148
  // src/uninstall.ts
4535
- import * as fs8 from "node:fs";
4536
- import * as path9 from "node:path";
5149
+ import * as fs10 from "node:fs";
5150
+ import * as path11 from "node:path";
4537
5151
  var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
4538
5152
  var parseUninstallArgs = (args) => {
4539
5153
  let scope = null;
@@ -4555,14 +5169,14 @@ var parseUninstallArgs = (args) => {
4555
5169
  var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
4556
5170
  var countEntries = (directory) => {
4557
5171
  try {
4558
- return fs8.readdirSync(directory).length;
5172
+ return fs10.readdirSync(directory).length;
4559
5173
  } catch {
4560
5174
  return 0;
4561
5175
  }
4562
5176
  };
4563
5177
  var exists = (file) => {
4564
5178
  try {
4565
- fs8.lstatSync(file);
5179
+ fs10.lstatSync(file);
4566
5180
  return true;
4567
5181
  } catch {
4568
5182
  return false;
@@ -4571,7 +5185,7 @@ var exists = (file) => {
4571
5185
  var managedBlockCount = (sshConfigFile) => {
4572
5186
  let config;
4573
5187
  try {
4574
- config = fs8.readFileSync(sshConfigFile, "utf8");
5188
+ config = fs10.readFileSync(sshConfigFile, "utf8");
4575
5189
  } catch {
4576
5190
  return 0;
4577
5191
  }
@@ -4595,27 +5209,27 @@ var describeUninstall = async (runtime, scope) => {
4595
5209
  if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
4596
5210
  server = "none";
4597
5211
  } else if (read.value === null) {
4598
- server = exists(path9.join(configDir, "identity.env")) ? {
5212
+ server = exists(path11.join(configDir, "identity.env")) ? {
4599
5213
  version: "(no active generation)",
4600
5214
  appUrl: "",
4601
5215
  dockerContext: "",
4602
- generations: countEntries(path9.join(configDir, "generations")),
4603
- backups: countEntries(path9.join(configDir, "backups"))
5216
+ generations: countEntries(path11.join(configDir, "generations")),
5217
+ backups: countEntries(path11.join(configDir, "backups"))
4604
5218
  } : "none";
4605
5219
  } else {
4606
5220
  server = {
4607
5221
  version: read.value.config.serverVersion,
4608
5222
  appUrl: read.value.config.appUrl,
4609
5223
  dockerContext: read.value.config.dockerContext,
4610
- generations: countEntries(path9.join(configDir, "generations")),
4611
- backups: countEntries(path9.join(configDir, "backups"))
5224
+ generations: countEntries(path11.join(configDir, "generations")),
5225
+ backups: countEntries(path11.join(configDir, "backups"))
4612
5226
  };
4613
5227
  }
4614
5228
  }
4615
5229
  let home = null;
4616
5230
  if (scope !== "server") {
4617
- const cliConfig = path9.join(runtime.cliHome, "cli.json");
4618
- const sshDirectory = path9.join(runtime.cliHome, "ssh");
5231
+ const cliConfig = path11.join(runtime.cliHome, "cli.json");
5232
+ const sshDirectory = path11.join(runtime.cliHome, "ssh");
4619
5233
  home = {
4620
5234
  cliConfig: exists(cliConfig) ? cliConfig : null,
4621
5235
  sshDirectory: exists(sshDirectory) ? sshDirectory : null,
@@ -4747,7 +5361,7 @@ var removeServer = async (runtime, failures, leftovers) => {
4747
5361
  }
4748
5362
  }
4749
5363
  for (const name of SERVER_FILES) {
4750
- fs8.rmSync(path9.join(configDir, name), { recursive: true, force: true });
5364
+ fs10.rmSync(path11.join(configDir, name), { recursive: true, force: true });
4751
5365
  }
4752
5366
  server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
4753
5367
  },
@@ -4758,20 +5372,20 @@ var removeServer = async (runtime, failures, leftovers) => {
4758
5372
  var removeHome = async (runtime, plan, failures, leftovers) => {
4759
5373
  const { server } = runtime;
4760
5374
  if (plan.cliConfig !== null) {
4761
- fs8.rmSync(plan.cliConfig, { force: true });
5375
+ fs10.rmSync(plan.cliConfig, { force: true });
4762
5376
  server.writeLine(`removed ${plan.cliConfig}`);
4763
5377
  }
4764
5378
  if (plan.sshDirectory !== null) {
4765
- fs8.rmSync(plan.sshDirectory, { recursive: true, force: true });
5379
+ fs10.rmSync(plan.sshDirectory, { recursive: true, force: true });
4766
5380
  server.writeLine(`removed ${plan.sshDirectory}`);
4767
5381
  }
4768
5382
  if (plan.managedSshBlocks > 0) {
4769
5383
  try {
4770
- const before = fs8.readFileSync(runtime.sshConfigFile, "utf8");
5384
+ const before = fs10.readFileSync(runtime.sshConfigFile, "utf8");
4771
5385
  const stripped = stripManagedWorkspaceSshBlocks(before);
4772
5386
  if (!stripped.ok) throw stripped.error;
4773
5387
  if (stripped.value.removed > 0) {
4774
- fs8.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
5388
+ fs10.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
4775
5389
  server.writeLine(
4776
5390
  `removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
4777
5391
  );
@@ -4784,12 +5398,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
4784
5398
  }
4785
5399
  let remaining = [];
4786
5400
  try {
4787
- remaining = fs8.readdirSync(runtime.cliHome);
5401
+ remaining = fs10.readdirSync(runtime.cliHome);
4788
5402
  } catch {
4789
5403
  return;
4790
5404
  }
4791
5405
  if (remaining.length === 0) {
4792
- fs8.rmdirSync(runtime.cliHome);
5406
+ fs10.rmdirSync(runtime.cliHome);
4793
5407
  server.writeLine(`removed ${runtime.cliHome}`);
4794
5408
  } else {
4795
5409
  leftovers.push(
@@ -4817,24 +5431,57 @@ var executeUninstall = async (runtime, plan) => {
4817
5431
  return { failures, leftovers };
4818
5432
  };
4819
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
+
4820
5467
  // src/main.ts
4821
5468
  var mendCliHome = () => {
4822
5469
  const xdg = process.env["XDG_CONFIG_HOME"];
4823
- const preferred = path10.join(
4824
- 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,
4825
5472
  "mend"
4826
5473
  );
4827
- const legacy = path10.join(os6.homedir(), ".mend");
4828
- 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;
4829
5476
  };
4830
- var CONFIG_PATH = path10.join(mendCliHome(), "cli.json");
5477
+ var CONFIG_PATH = path12.join(mendCliHome(), "cli.json");
4831
5478
  var loadConfig = () => {
4832
5479
  let fileConfig = {};
4833
- if (fs9.existsSync(CONFIG_PATH)) {
5480
+ if (fs11.existsSync(CONFIG_PATH)) {
4834
5481
  try {
4835
- fileConfig = JSON.parse(fs9.readFileSync(CONFIG_PATH, "utf8"));
5482
+ fileConfig = JSON.parse(fs11.readFileSync(CONFIG_PATH, "utf8"));
4836
5483
  } catch {
4837
- fail4(`could not parse ${CONFIG_PATH}`);
5484
+ fail5(`could not parse ${CONFIG_PATH}`);
4838
5485
  }
4839
5486
  }
4840
5487
  const configuredUrl = process.env["MEND_URL"] ?? fileConfig.url ?? null;
@@ -4845,7 +5492,7 @@ var loadConfig = () => {
4845
5492
  deviceId: fileConfig.deviceId ?? null
4846
5493
  };
4847
5494
  };
4848
- var fail4 = (message) => {
5495
+ var fail5 = (message) => {
4849
5496
  process.stderr.write(`mend: ${message}
4850
5497
  `);
4851
5498
  process.exit(1);
@@ -4854,18 +5501,18 @@ var parseMendUrl = (value) => {
4854
5501
  try {
4855
5502
  return new URL(value);
4856
5503
  } catch {
4857
- return fail4(`"${value}" is not a valid Mend URL`);
5504
+ return fail5(`"${value}" is not a valid Mend URL`);
4858
5505
  }
4859
5506
  };
4860
- var paint3 = (code) => (text) => process.stdout.isTTY ? `\x1B[${code}m${text}\x1B[0m` : text;
4861
- var dim4 = paint3("2");
4862
- var green4 = paint3("32");
4863
- var amber = paint3("33");
4864
- var cobalt2 = paint3("34");
4865
- 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}
4866
5513
  `);
4867
5514
  var detachKeyEnabled = process.env["MEND_DETACH_KEY"] !== "none";
4868
- var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${dim4("Ctrl+]")}` : "";
5515
+ var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${dim5("Ctrl+]")}` : "";
4869
5516
  var herdrAgentOf = (harness) => {
4870
5517
  if (harness === "codex" || harness === "claude" || harness === "opencode") return harness;
4871
5518
  return null;
@@ -4933,9 +5580,42 @@ var api = async (config, method, route, body) => {
4933
5580
  try {
4934
5581
  return await request(config, method, route, body);
4935
5582
  } catch (error) {
4936
- return fail4(error instanceof Error ? error.message : String(error));
5583
+ return fail5(error instanceof Error ? error.message : String(error));
4937
5584
  }
4938
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
+ });
4939
5619
  var boundApi = (config) => (method, route, body) => api(config, method, route, body);
4940
5620
  var withSpinner = async (label, work) => {
4941
5621
  if (process.stdout.isTTY !== true) return work;
@@ -4944,7 +5624,7 @@ var withSpinner = async (label, work) => {
4944
5624
  let frame = 0;
4945
5625
  const timer = setInterval(() => {
4946
5626
  const seconds = Math.round((Date.now() - started) / 1e3);
4947
- 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`)} `);
4948
5628
  frame += 1;
4949
5629
  }, 120);
4950
5630
  try {
@@ -4958,66 +5638,74 @@ var adopt = async (config, args) => {
4958
5638
  const nameFlag = args.indexOf("--name");
4959
5639
  const name = nameFlag !== -1 && args[nameFlag + 1] !== void 0 ? String(args[nameFlag + 1]) : null;
4960
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";
4961
5645
  const positional = args.filter(
4962
5646
  (a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1) && (authFlagIndex === -1 || i !== authFlagIndex + 1)
4963
5647
  );
4964
5648
  const source = positional[0] ?? gitOriginUrl(process.cwd());
4965
5649
  if (source === null) {
4966
- return fail4(
5650
+ return fail5(
4967
5651
  "No Git URL was given and this repository has no origin. Run mend adopt <git-url>."
4968
5652
  );
4969
5653
  }
4970
5654
  const sourceIssue = repositoryCloneUrlIssue(source);
4971
- if (sourceIssue !== null) return fail4(sourceIssue);
4972
- const projectName = name ?? normalizeProjectName(path10.basename(source, ".git"));
5655
+ if (sourceIssue !== null) return fail5(sourceIssue);
5656
+ const projectName = name ?? normalizeProjectName(path12.basename(source, ".git"));
4973
5657
  const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
4974
5658
  if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
4975
- return fail4(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
5659
+ return fail5(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
4976
5660
  }
4977
5661
  const project2 = await api(config, "POST", "/projects", {
4978
5662
  name: projectName,
4979
5663
  source,
5664
+ visibility,
4980
5665
  ...auth === null ? {} : { gitAuthMode: auth }
4981
5666
  });
4982
- say4(`${green4("\u2713")} adopted \xB7 ${project2.name} \xB7 ${dim4(project2.storePath)}`);
4983
- 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
+ );
4984
5672
  if (project2.gitAuthMode === "mend-key") {
4985
- 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)")}`);
4986
5674
  } else if (project2.gitAuthMode === "bridge") {
4987
- 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`)")}`);
4988
5676
  }
4989
- say4(
4990
- `${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(")")}`
4991
5679
  );
4992
5680
  };
4993
5681
  var findProject = async (config, explicit, adoptCwd = false) => {
4994
5682
  const projects = await api(config, "GET", "/projects");
4995
5683
  if (explicit !== null) {
4996
5684
  const named = projects.find((p) => p.name === explicit);
4997
- if (named === void 0) return fail4(`no adopted project named "${explicit}"`);
5685
+ if (named === void 0) return fail5(`no adopted project named "${explicit}"`);
4998
5686
  return named;
4999
5687
  }
5000
5688
  const cwd = process.cwd();
5001
5689
  const project2 = matchProjectByCwd(projects, cwdFacts(cwd));
5002
5690
  if (project2 !== void 0) return project2;
5003
5691
  if (!adoptCwd) {
5004
- return fail4(
5692
+ return fail5(
5005
5693
  `no adopted project matches ${cwd} \u2014 run "mend adopt" here first, or name one with --project`
5006
5694
  );
5007
5695
  }
5008
5696
  const facts = cwdFacts(cwd);
5009
5697
  if (facts.repoRoot === null || facts.originUrl === null) {
5010
- return fail4(
5698
+ return fail5(
5011
5699
  `${cwd} does not match an adopted project with a Git origin \u2014 run "mend adopt <git-url>" first`
5012
5700
  );
5013
5701
  }
5014
5702
  const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
5015
- if (sourceIssue !== null) return fail4(sourceIssue);
5703
+ if (sourceIssue !== null) return fail5(sourceIssue);
5016
5704
  const adopted = await api(config, "POST", "/projects", {
5017
- name: normalizeProjectName(path10.basename(facts.repoRoot)),
5705
+ name: normalizeProjectName(path12.basename(facts.repoRoot)),
5018
5706
  source: facts.originUrl
5019
5707
  });
5020
- say4(`${green4("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim4(adopted.storePath)}`);
5708
+ say5(`${green5("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim5(adopted.storePath)}`);
5021
5709
  return adopted;
5022
5710
  };
5023
5711
  var resolvedBackgroundSessions = async (config, project2) => {
@@ -5039,7 +5727,7 @@ var askWorktreeName = async () => {
5039
5727
  const readline = await import("node:readline/promises");
5040
5728
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5041
5729
  try {
5042
- 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();
5043
5731
  return answer === "" ? null : normalizeProjectName(answer);
5044
5732
  } finally {
5045
5733
  rl.close();
@@ -5047,35 +5735,35 @@ var askWorktreeName = async () => {
5047
5735
  };
5048
5736
  var launch = async (config, harness, args) => {
5049
5737
  const parsed = parseLaunchArgs(args);
5050
- if (parsed.error !== null) return fail4(parsed.error);
5738
+ if (parsed.error !== null) return fail5(parsed.error);
5051
5739
  const structured = parsed.prompt !== null || parsed.model !== null || parsed.effort !== null || parsed.ask || parsed.fast;
5052
5740
  if (harness === "run" && structured) {
5053
- 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")}`);
5054
5742
  }
5055
5743
  if (harness === "run" && (parsed.detach || parsed.foreground)) {
5056
- return fail4(
5744
+ return fail5(
5057
5745
  "mend run takes no lifecycle flags \u2014 it tails the record; Ctrl+C stops watching, not the command"
5058
5746
  );
5059
5747
  }
5060
5748
  const argv = harness === "run" ? parsed.custom : HARNESS_COMMANDS[harness] ?? [];
5061
5749
  if (argv.length === 0) {
5062
- return fail4(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
5750
+ return fail5(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
5063
5751
  }
5064
5752
  const project2 = await findProject(config, parsed.project, true);
5065
- say4(
5066
- `${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" : ""}`)}`
5067
5755
  );
5068
5756
  let worktreeName;
5069
5757
  if (parsed.worktree !== null) {
5070
5758
  const detail = await api(config, "GET", `/projects/${project2.id}`);
5071
5759
  const existing = detail.worktrees ?? [];
5072
5760
  if (detail.worktrees === void 0) {
5073
- return fail4("this server predates shared worktrees \u2014 use --name instead");
5761
+ return fail5("this server predates shared worktrees \u2014 use --name instead");
5074
5762
  }
5075
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);
5076
5764
  if (match === void 0) {
5077
5765
  const names = existing.map((worktree) => worktree.name).join(", ");
5078
- return fail4(
5766
+ return fail5(
5079
5767
  `no worktree matches "${parsed.worktree}" in ${project2.name}${names === "" ? "" : ` \u2014 have: ${names}`}`
5080
5768
  );
5081
5769
  }
@@ -5092,8 +5780,8 @@ var launch = async (config, harness, args) => {
5092
5780
  const members = (detail?.sessions ?? []).filter(
5093
5781
  (candidate) => candidate.worktreeId === joined.id
5094
5782
  ).length;
5095
- say4(
5096
- `${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}`)}`
5097
5785
  );
5098
5786
  }
5099
5787
  }
@@ -5118,12 +5806,12 @@ var launch = async (config, harness, args) => {
5118
5806
  base: parsed.base
5119
5807
  });
5120
5808
  createdSessionId = session.id;
5121
- say4(`${green4("\u2713")} worktree ${session.worktree} ${dim4(`\xB7 branch ${session.branch}`)}`);
5809
+ say5(`${green5("\u2713")} worktree ${session.worktree} ${dim5(`\xB7 branch ${session.branch}`)}`);
5122
5810
  const baseWord = session.baseRef === null ? "" : `${session.baseRef} `;
5123
- say4(
5124
- `${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))}`
5125
5813
  );
5126
- say4(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
5814
+ say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
5127
5815
  if (harness === "run") {
5128
5816
  return supervisedRun(config, session, argv);
5129
5817
  }
@@ -5139,23 +5827,28 @@ var launch = async (config, harness, args) => {
5139
5827
  api(config, "POST", `/sessions/${session.id}/launch`, launchBody)
5140
5828
  );
5141
5829
  if (lifecycle === "detach") {
5142
- say4(`${green4("\u2713 recording")} \xB7 running detached`);
5143
- 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)}`);
5144
5832
  return;
5145
5833
  }
5146
- say4(`${green4("\u2713 recording")} \xB7 workspace mounts the worktree${detachHint()}`);
5147
- say4("");
5834
+ say5(`${green5("\u2713 recording")} \xB7 workspace mounts the worktree${detachHint()}`);
5835
+ say5("");
5148
5836
  attachOwnsSignals = true;
5149
5837
  await attachOrExit(config, session.id, session.harness, lifecycle);
5150
5838
  exitAfterSessionEnd(config, session.id);
5151
5839
  };
5152
5840
  var attachTty = async (config, sessionId, harness, from, processId, options) => {
5153
- const url = parseMendUrl(`${config.url}/api/tty`);
5154
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
5155
- if (processId === void 0) url.searchParams.set("session", sessionId);
5156
- else url.searchParams.set("process", processId);
5157
- url.searchParams.set("from", from.toString());
5158
- 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
+ }
5159
5852
  const ws = new WebSocket(url);
5160
5853
  ws.binaryType = "arraybuffer";
5161
5854
  try {
@@ -5299,44 +5992,44 @@ var stopSessionQuickly = async (config, sessionId) => {
5299
5992
  var stopAndExit = async (config, sessionId) => {
5300
5993
  const id8 = sessionId.slice(0, 8);
5301
5994
  if (await stopSessionQuickly(config, sessionId)) {
5302
- say4(`${green4("\u2713")} stopped \xB7 ${id8}`);
5303
- say4(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
5995
+ say5(`${green5("\u2713")} stopped \xB7 ${id8}`);
5996
+ say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
5304
5997
  process.exit(0);
5305
5998
  }
5306
- 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}`);
5307
6000
  process.exit(1);
5308
6001
  };
5309
6002
  var finishAttach = async (config, sessionId, outcome, mode = "background") => {
5310
6003
  const id8 = sessionId.slice(0, 8);
5311
6004
  if (outcome === "unavailable") {
5312
- return fail4(`tty attach unavailable: could not connect to ${config.url}`);
6005
+ return fail5(`tty attach unavailable: could not connect to ${config.url}`);
5313
6006
  }
5314
6007
  if (outcome === "detached") {
5315
- say4("");
5316
- 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}`);
5317
6010
  process.exit(0);
5318
6011
  }
5319
6012
  if (outcome === "interrupted") {
5320
- say4("");
6013
+ say5("");
5321
6014
  if (mode === "foreground") return stopAndExit(config, sessionId);
5322
- 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}`);
5323
6016
  process.exit(0);
5324
6017
  }
5325
6018
  if (outcome === "dropped") {
5326
- say4("");
6019
+ say5("");
5327
6020
  if (mode === "foreground") {
5328
6021
  let live;
5329
6022
  try {
5330
6023
  const detail = await request(config, "GET", `/sessions/${sessionId}`);
5331
6024
  live = agentIsLive(detail.session, detail.currentAgent);
5332
6025
  } catch {
5333
- 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}`);
5334
6027
  process.exit(1);
5335
6028
  }
5336
6029
  if (!live) return;
5337
6030
  return stopAndExit(config, sessionId);
5338
6031
  }
5339
- 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}`);
5340
6033
  process.exit(0);
5341
6034
  }
5342
6035
  };
@@ -5347,9 +6040,9 @@ var attachOrExit = async (config, sessionId, harness, mode = "background") => fi
5347
6040
  mode
5348
6041
  );
5349
6042
  var exitAfterSessionEnd = (config, sessionId) => {
5350
- say4("");
5351
- say4(`${green4("\u2713")} session ended`);
5352
- 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}`);
5353
6046
  process.exit(0);
5354
6047
  };
5355
6048
  var attach = async (config, args) => {
@@ -5360,26 +6053,26 @@ var attach = async (config, args) => {
5360
6053
  }
5361
6054
  const sessions = await api(config, "GET", "/sessions");
5362
6055
  const match = sessions.find((s) => s.id.startsWith(prefix));
5363
- if (match === void 0) return fail4(`no active session matches "${prefix}"`);
6056
+ if (match === void 0) return fail5(`no active session matches "${prefix}"`);
5364
6057
  return attachPicked(config, match);
5365
6058
  };
5366
6059
  var attachPicked = async (config, session) => {
5367
- say4(
5368
- `${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()}`
5369
6062
  );
5370
- say4("");
6063
+ say5("");
5371
6064
  const outcome = await attachTty(config, session.id, session.harness, 0n, void 0, {
5372
6065
  handleSignals: true
5373
6066
  });
5374
6067
  if (outcome === "unavailable") {
5375
6068
  const detail = await api(config, "GET", `/sessions/${session.id}`);
5376
6069
  if (detail.currentAgent?.kind === "agent-protocol" && agentIsLive(detail.session, detail.currentAgent)) {
5377
- say4(`${amber("taking over")} from the protocol session`);
6070
+ say5(`${amber("taking over")} from the protocol session`);
5378
6071
  await withSpinner(
5379
6072
  "reopening as a terminal \u2014 same conversation\u2026",
5380
6073
  api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
5381
6074
  );
5382
- say4("");
6075
+ say5("");
5383
6076
  await attachOrExit(config, session.id, session.harness);
5384
6077
  return exitAfterSessionEnd(config, session.id);
5385
6078
  }
@@ -5398,7 +6091,7 @@ var stopCommand = async (config, args) => {
5398
6091
  return;
5399
6092
  }
5400
6093
  if (!all && prefix === void 0) {
5401
- return fail4(usageOf("stop"));
6094
+ return fail5(usageOf("stop"));
5402
6095
  }
5403
6096
  const [sessions, projects] = await Promise.all([
5404
6097
  api(config, "GET", "/sessions"),
@@ -5407,21 +6100,21 @@ var stopCommand = async (config, args) => {
5407
6100
  let scoped = sessions;
5408
6101
  if (projectName !== null) {
5409
6102
  const project2 = projects.find((p) => p.name === projectName);
5410
- if (project2 === void 0) return fail4(`no project named "${projectName}"`);
6103
+ if (project2 === void 0) return fail5(`no project named "${projectName}"`);
5411
6104
  scoped = sessions.filter((s) => s.projectId === project2.id);
5412
6105
  }
5413
6106
  let targets;
5414
6107
  if (all) {
5415
6108
  targets = scoped;
5416
6109
  if (targets.length === 0) {
5417
- say4("no active sessions");
6110
+ say5("no active sessions");
5418
6111
  return;
5419
6112
  }
5420
6113
  } else {
5421
6114
  const matches = scoped.filter((s) => s.id.startsWith(prefix ?? ""));
5422
- if (matches.length === 0) return fail4(`no active session matches "${prefix}"`);
6115
+ if (matches.length === 0) return fail5(`no active session matches "${prefix}"`);
5423
6116
  if (matches.length > 1) {
5424
- 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`);
5425
6118
  }
5426
6119
  targets = matches;
5427
6120
  }
@@ -5430,20 +6123,20 @@ var stopCommand = async (config, args) => {
5430
6123
  var stopSessions = async (config, targets, summarise) => {
5431
6124
  for (const session of targets) {
5432
6125
  await api(config, "POST", `/sessions/${session.id}/stop`);
5433
- say4(
5434
- `${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}`
5435
6128
  );
5436
- say4(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
6129
+ say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
5437
6130
  }
5438
6131
  if (summarise) {
5439
- say4(`${green4("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
6132
+ say5(`${green5("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
5440
6133
  }
5441
6134
  };
5442
6135
  var pickSessionInteractively = async (rows) => {
5443
- say4(dim4("more than one live session \u2014 pick one:"));
6136
+ say5(dim5("more than one live session \u2014 pick one:"));
5444
6137
  rows.forEach((row, index) => {
5445
- say4(
5446
- ` ${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)}`
5447
6140
  );
5448
6141
  });
5449
6142
  const readline = await import("node:readline/promises");
@@ -5451,17 +6144,17 @@ var pickSessionInteractively = async (rows) => {
5451
6144
  const answer = await rl.question(" session #: ");
5452
6145
  rl.close();
5453
6146
  const chosen = rows[Number(answer.trim()) - 1];
5454
- 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`);
5455
6148
  return chosen.session;
5456
6149
  };
5457
6150
  var resolveLiveSession = async (config, prefix, command) => {
5458
6151
  const sessions = await api(config, "GET", "/sessions?retained=1");
5459
6152
  if (prefix !== void 0) {
5460
6153
  const matches = sessions.filter((s) => s.id.startsWith(prefix));
5461
- 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}"`);
5462
6155
  const exact = matches[0];
5463
6156
  if (matches.length > 1 || exact === void 0) {
5464
- 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`);
5465
6158
  }
5466
6159
  return exact;
5467
6160
  }
@@ -5470,11 +6163,11 @@ var resolveLiveSession = async (config, prefix, command) => {
5470
6163
  const candidates = project2 === void 0 ? sessions : sessions.filter((s) => s.projectId === project2.id);
5471
6164
  const only = candidates[0];
5472
6165
  if (only === void 0) {
5473
- 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");
5474
6167
  }
5475
6168
  if (candidates.length === 1) return only;
5476
6169
  if (process.stdin.isTTY !== true) {
5477
- 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>`);
5478
6171
  }
5479
6172
  const nameById = new Map(projects.map((p) => [p.id, p.name]));
5480
6173
  return pickSessionInteractively(
@@ -5487,26 +6180,26 @@ var resolveLiveSession = async (config, prefix, command) => {
5487
6180
  var shellCommand = async (config, args) => {
5488
6181
  const prefix = args.find((a) => !a.startsWith("--"));
5489
6182
  const session = await resolveLiveSession(config, prefix, "shell");
5490
- say4(
5491
- `${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()}`
5492
6185
  );
5493
6186
  const shellProcess = await withSpinner(
5494
6187
  "opening a shell in the session workspace\u2026",
5495
6188
  api(config, "POST", `/sessions/${session.id}/shell`)
5496
6189
  );
5497
- say4("");
6190
+ say5("");
5498
6191
  const outcome = await attachTty(config, session.id, "shell", 0n, shellProcess.id, {
5499
6192
  handleSignals: true
5500
6193
  });
5501
6194
  if (outcome === "unavailable") {
5502
- return fail4(`tty attach unavailable: could not connect to ${config.url}`);
6195
+ return fail5(`tty attach unavailable: could not connect to ${config.url}`);
5503
6196
  }
5504
- say4("");
6197
+ say5("");
5505
6198
  if (outcome === "detached" || outcome === "interrupted" || outcome === "dropped") {
5506
- 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`);
5507
6200
  process.exit(0);
5508
6201
  }
5509
- say4(`${green4("\u2713")} shell ended`);
6202
+ say5(`${green5("\u2713")} shell ended`);
5510
6203
  process.exit(0);
5511
6204
  };
5512
6205
  var flattenService = (view) => {
@@ -5547,30 +6240,30 @@ var serviceUrl = (service) => service.browserUrl ?? (service.authority === null
5547
6240
  var printServiceAccess = (config, service, tunneling = false) => {
5548
6241
  const gate = "no Mend sign-in on this port \u2014 network reach is the only gate";
5549
6242
  if (serverIsLocal(config)) {
5550
- if (service.authority !== null) say4(` ${cobalt2(serviceUrl(service))} ${dim4(gate)}`);
6243
+ if (service.authority !== null) say5(` ${cobalt2(serviceUrl(service))} ${dim5(gate)}`);
5551
6244
  return;
5552
6245
  }
5553
6246
  if (service.protocol === "udp") {
5554
6247
  if (service.authority !== null) {
5555
- 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}`)}`);
5556
6249
  }
5557
6250
  return;
5558
6251
  }
5559
6252
  if (!tunneling) {
5560
6253
  const local = service.hostPort ?? service.workspacePort;
5561
- say4(
5562
- ` ${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`)}`
5563
6256
  );
5564
6257
  }
5565
6258
  if (service.authority !== null) {
5566
- say4(` ${dim4(`server-side listener ${service.authority} \xB7 ${gate}`)}`);
6259
+ say5(` ${dim5(`server-side listener ${service.authority} \xB7 ${gate}`)}`);
5567
6260
  }
5568
6261
  };
5569
6262
  var printWorkspaceTtlFailure = (service) => {
5570
6263
  if (service.workspaceTtlRenewalError === null) return;
5571
- say4(amber(` workspace TTL renewal failed \xB7 ${service.workspaceTtlRenewalError}`));
5572
- say4(
5573
- dim4(
6264
+ say5(amber(` workspace TTL renewal failed \xB7 ${service.workspaceTtlRenewalError}`));
6265
+ say5(
6266
+ dim5(
5574
6267
  ` last renewed ${service.workspaceTtlRenewedAt ?? "unknown"} \xB7 known expiry ${service.workspaceExpiresAt ?? "unknown"} \xB7 failed ${service.workspaceTtlRenewalFailedAt ?? "unknown"}`
5575
6268
  )
5576
6269
  );
@@ -5580,11 +6273,11 @@ var printServiceEndpoint = (config, service, tunneling = false) => {
5580
6273
  printWorkspaceTtlFailure(service);
5581
6274
  };
5582
6275
  var printService = (config, service) => {
5583
- const status = service.status === "reachable" ? green4(service.status) : amber(service.status);
6276
+ const status = service.status === "reachable" ? green5(service.status) : amber(service.status);
5584
6277
  const port = `:${service.workspacePort ?? "?"}${service.protocol === "udp" ? "/udp" : ""}`;
5585
6278
  const statusPad = " ".repeat(Math.max(1, 12 - service.status.length));
5586
- say4(
5587
- `${(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))}`
5588
6281
  );
5589
6282
  printServiceAccess(config, service);
5590
6283
  printWorkspaceTtlFailure(service);
@@ -5595,21 +6288,21 @@ var serviceAdd = async (config, args) => {
5595
6288
  const protocol = args.includes("--udp") ? "udp" : "tcp";
5596
6289
  const http = args.includes("--http");
5597
6290
  const https = args.includes("--https");
5598
- 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.");
5599
6292
  const browserScheme = https ? "https" : http ? "http" : null;
5600
6293
  if (protocol === "udp" && browserScheme !== null) {
5601
- return fail4("UDP Services cannot use --http or --https");
6294
+ return fail5("UDP Services cannot use --http or --https");
5602
6295
  }
5603
6296
  const positional = args.filter(
5604
6297
  (a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1)
5605
6298
  );
5606
6299
  const portRaw = positional.find((a) => /^\d+$/.test(a));
5607
6300
  if (portRaw === void 0) {
5608
- return fail4(usageOf("service add"));
6301
+ return fail5(usageOf("service add"));
5609
6302
  }
5610
6303
  const port = Number(portRaw);
5611
6304
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
5612
- return fail4(`"${portRaw}" is not a port`);
6305
+ return fail5(`"${portRaw}" is not a port`);
5613
6306
  }
5614
6307
  const prefix = positional.find((a) => a !== portRaw);
5615
6308
  const session = await resolveLiveSession(config, prefix, "service add");
@@ -5619,52 +6312,52 @@ var serviceAdd = async (config, args) => {
5619
6312
  protocol,
5620
6313
  browserScheme
5621
6314
  });
5622
- say4(`${green4("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
6315
+ say5(`${green5("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
5623
6316
  printServiceEndpoint(config, service);
5624
6317
  if (protocol === "udp") {
5625
- 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`));
5626
6319
  } else if (service.status !== "reachable") {
5627
- 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`));
5628
6321
  }
5629
6322
  };
5630
6323
  var serviceList = async (config) => {
5631
6324
  const services = await fetchServices(config);
5632
6325
  if (services.length === 0) {
5633
- 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"));
5634
6327
  return;
5635
6328
  }
5636
6329
  for (const service of services) printService(config, service);
5637
6330
  };
5638
6331
  var serviceStop = async (config, args) => {
5639
6332
  const needle = args.find((a) => !a.startsWith("--"));
5640
- if (needle === void 0) return fail4(usageOf("service stop"));
6333
+ if (needle === void 0) return fail5(usageOf("service stop"));
5641
6334
  const services = await fetchServices(config);
5642
6335
  const matches = services.filter(
5643
6336
  (service) => service.label === needle || service.id.startsWith(needle)
5644
6337
  );
5645
- if (matches.length === 0) return fail4(`no live service matches "${needle}"`);
6338
+ if (matches.length === 0) return fail5(`no live service matches "${needle}"`);
5646
6339
  const match = matches[0];
5647
6340
  if (matches.length > 1 || match === void 0) {
5648
- return fail4(`"${needle}" is ambiguous \u2014 use more of the id`);
6341
+ return fail5(`"${needle}" is ambiguous \u2014 use more of the id`);
5649
6342
  }
5650
6343
  const stoppedService = await mutateService(config, "POST", `/services/${match.id}/stop`);
5651
- 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)}`);
5652
6345
  };
5653
6346
  var findLiveService = async (config, needle) => {
5654
6347
  const services = await fetchServices(config);
5655
6348
  const matches = services.filter(
5656
6349
  (service) => service.label === needle || service.id.startsWith(needle)
5657
6350
  );
5658
- if (matches.length === 0) return fail4(`no live service matches "${needle}"`);
6351
+ if (matches.length === 0) return fail5(`no live service matches "${needle}"`);
5659
6352
  const match = matches[0];
5660
6353
  if (matches.length > 1 || match === void 0) {
5661
- return fail4(`"${needle}" is ambiguous \u2014 use more of the id`);
6354
+ return fail5(`"${needle}" is ambiguous \u2014 use more of the id`);
5662
6355
  }
5663
6356
  return match;
5664
6357
  };
5665
6358
  var willAutoConnect = (config, service, optOut) => !optOut && !serverIsLocal(config) && service.protocol !== "udp";
5666
6359
  var autoConnect = async (config, service) => {
5667
- 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"));
5668
6361
  await tunnelServices(config, [service], null);
5669
6362
  };
5670
6363
  var serviceRun = async (config, args) => {
@@ -5673,7 +6366,7 @@ var serviceRun = async (config, args) => {
5673
6366
  if (dashdash === -1) {
5674
6367
  const positionals = args.filter((a) => !a.startsWith("--"));
5675
6368
  const name2 = positionals.at(-1);
5676
- if (name2 === void 0) return fail4(usage);
6369
+ if (name2 === void 0) return fail5(usage);
5677
6370
  const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
5678
6371
  const session2 = await resolveLiveSession(config, prefix2, "service run");
5679
6372
  const recipes = await api(
@@ -5684,7 +6377,7 @@ var serviceRun = async (config, args) => {
5684
6377
  const recipe = recipes.find((entry) => entry.name === name2);
5685
6378
  if (recipe === void 0) {
5686
6379
  const known = recipes.map((entry) => entry.name).join(", ");
5687
- return fail4(
6380
+ return fail5(
5688
6381
  recipes.length === 0 ? `no mend.toml recipes in this worktree \u2014 declare [service.${name2}] first` : `no recipe named "${name2}" \u2014 declared: ${known}`
5689
6382
  );
5690
6383
  }
@@ -5695,26 +6388,26 @@ var serviceRun = async (config, args) => {
5695
6388
  })
5696
6389
  );
5697
6390
  const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect"));
5698
- say4(`${green4("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
6391
+ say5(`${green5("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
5699
6392
  printServiceEndpoint(config, service2, tunneling2);
5700
- 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)}`));
5701
6394
  if (tunneling2) await autoConnect(config, service2);
5702
6395
  return;
5703
6396
  }
5704
6397
  const argv = args.slice(dashdash + 1);
5705
- if (argv.length === 0) return fail4(usage);
6398
+ if (argv.length === 0) return fail5(usage);
5706
6399
  const head = args.slice(0, dashdash);
5707
6400
  const portFlag = head.indexOf("--port");
5708
6401
  const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
5709
- if (!Number.isInteger(port) || port < 1 || port > 65535) return fail4(usage);
6402
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return fail5(usage);
5710
6403
  const nameFlag = head.indexOf("--name");
5711
6404
  const name = nameFlag !== -1 && head[nameFlag + 1] !== void 0 ? String(head[nameFlag + 1]) : null;
5712
6405
  const protocol = head.includes("--udp") ? "udp" : "tcp";
5713
6406
  const http = head.includes("--http");
5714
6407
  const https = head.includes("--https");
5715
- if (http && https) return fail4(usage);
6408
+ if (http && https) return fail5(usage);
5716
6409
  const browserScheme = https ? "https" : http ? "http" : null;
5717
- if (protocol === "udp" && browserScheme !== null) return fail4(usage);
6410
+ if (protocol === "udp" && browserScheme !== null) return fail5(usage);
5718
6411
  const prefix = head.find(
5719
6412
  (a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
5720
6413
  );
@@ -5730,9 +6423,9 @@ var serviceRun = async (config, args) => {
5730
6423
  })
5731
6424
  );
5732
6425
  const tunneling = willAutoConnect(config, service, head.includes("--no-connect"));
5733
- say4(`${green4("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
6426
+ say5(`${green5("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
5734
6427
  printServiceEndpoint(config, service, tunneling);
5735
- 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)}`));
5736
6429
  if (tunneling) await autoConnect(config, service);
5737
6430
  };
5738
6431
  var serviceLogs = async (config, args) => {
@@ -5742,32 +6435,32 @@ var serviceLogs = async (config, args) => {
5742
6435
  (argument, index) => !argument.startsWith("--") && index !== fromFlag + 1
5743
6436
  );
5744
6437
  if (needle === void 0 || from === void 0 || !/^(0|[1-9]\d*)$/.test(from)) {
5745
- return fail4(usageOf("service logs"));
6438
+ return fail5(usageOf("service logs"));
5746
6439
  }
5747
6440
  const everything = await fetchServiceViews(config, true);
5748
6441
  const matches = everything.filter(
5749
6442
  (view2) => view2.service.name === needle || view2.service.id.startsWith(needle) || view2.attempts.some((attempt3) => attempt3.id.startsWith(needle))
5750
6443
  );
5751
- if (matches.length === 0) return fail4(`no service matches "${needle}"`);
6444
+ if (matches.length === 0) return fail5(`no service matches "${needle}"`);
5752
6445
  const view = matches.find((candidate) => {
5753
6446
  const current = candidate.service.currentAttemptId === null ? null : candidate.attempts.find((attempt3) => attempt3.id === candidate.service.currentAttemptId);
5754
6447
  return current !== null && current !== void 0 && current.exitedAt === null;
5755
6448
  }) ?? matches[0];
5756
- if (view === void 0) return fail4(`no service matches "${needle}"`);
6449
+ if (view === void 0) return fail5(`no service matches "${needle}"`);
5757
6450
  const currentAttempt = view.service.currentAttemptId === null ? null : view.attempts.find((candidate) => candidate.id === view.service.currentAttemptId) ?? null;
5758
6451
  const attempt2 = view.attempts.find((candidate) => candidate.id.startsWith(needle)) ?? currentAttempt ?? view.attempts.findLast((candidate) => candidate.sealantSessionId !== null) ?? null;
5759
6452
  if (attempt2?.sealantSessionId === null || attempt2 === null) {
5760
- return fail4(
6453
+ return fail5(
5761
6454
  `"${needle}" is an adopted port \u2014 no process of Mend's, no logs. mend service run supervises.`
5762
6455
  );
5763
6456
  }
5764
6457
  const label = view.service.name;
5765
- say4(
5766
- dim4(
6458
+ say5(
6459
+ dim5(
5767
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}`
5768
6461
  )
5769
6462
  );
5770
- say4("");
6463
+ say5("");
5771
6464
  let cursor = from;
5772
6465
  let telemetryReported = false;
5773
6466
  for (; ; ) {
@@ -5777,7 +6470,7 @@ var serviceLogs = async (config, args) => {
5777
6470
  `/processes/${attempt2.id}/logs?from=${encodeURIComponent(cursor)}&limit=256`
5778
6471
  );
5779
6472
  if (!telemetryReported) {
5780
- say4(dim4(`telemetry loss: ${page.telemetryLoss} \xB7 ${page.telemetryNote}`));
6473
+ say5(dim5(`telemetry loss: ${page.telemetryLoss} \xB7 ${page.telemetryNote}`));
5781
6474
  telemetryReported = true;
5782
6475
  }
5783
6476
  for (const chunk of page.chunks) {
@@ -5789,29 +6482,29 @@ var serviceLogs = async (config, args) => {
5789
6482
  if (ended && !advanced) break;
5790
6483
  if (!advanced) await new Promise((resolve4) => setTimeout(resolve4, 250));
5791
6484
  }
5792
- say4("");
5793
- say4(dim4(`stream ended \xB7 next sequence ${cursor}`));
6485
+ say5("");
6486
+ say5(dim5(`stream ended \xB7 next sequence ${cursor}`));
5794
6487
  };
5795
6488
  var serviceRestart = async (config, args) => {
5796
6489
  const needle = args.find((a) => !a.startsWith("--"));
5797
- if (needle === void 0) return fail4(usageOf("service restart"));
6490
+ if (needle === void 0) return fail5(usageOf("service restart"));
5798
6491
  const service = await findLiveService(config, needle);
5799
6492
  const restarted = await withSpinner(
5800
6493
  `restarting ${service.label ?? service.id.slice(0, 8)}\u2026`,
5801
6494
  mutateService(config, "POST", `/services/${service.id}/restart`)
5802
6495
  );
5803
- say4(`${green4("\u2713")} restarted \xB7 ${restarted.status}`);
6496
+ say5(`${green5("\u2713")} restarted \xB7 ${restarted.status}`);
5804
6497
  printServiceEndpoint(config, restarted);
5805
6498
  };
5806
6499
  var serviceInit = async (args) => {
5807
6500
  const root = gitTopLevel(process.cwd()) ?? process.cwd();
5808
- const target = path10.join(root, "mend.toml");
5809
- if (fs9.existsSync(target)) {
5810
- 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)`);
5811
6504
  }
5812
- const rootFiles = fs9.readdirSync(root);
6505
+ const rootFiles = fs11.readdirSync(root);
5813
6506
  const proposals = [];
5814
- const readRoot = (name) => fs9.readFileSync(path10.join(root, name), "utf8");
6507
+ const readRoot = (name) => fs11.readFileSync(path12.join(root, name), "utf8");
5815
6508
  if (rootFiles.includes("package.json")) {
5816
6509
  proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
5817
6510
  }
@@ -5821,13 +6514,13 @@ var serviceInit = async (args) => {
5821
6514
  );
5822
6515
  for (const glob of globs) {
5823
6516
  const parent = glob.replace(/\/?\*+$/, "");
5824
- 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];
5825
6518
  for (const dir of dirs) {
5826
- const manifest = path10.join(root, dir, "package.json");
5827
- if (!fs9.existsSync(manifest)) continue;
6519
+ const manifest = path12.join(root, dir, "package.json");
6520
+ if (!fs11.existsSync(manifest)) continue;
5828
6521
  for (const proposal of proposeFromWorkspacePackage(
5829
- path10.basename(dir),
5830
- fs9.readFileSync(manifest, "utf8"),
6522
+ path12.basename(dir),
6523
+ fs11.readFileSync(manifest, "utf8"),
5831
6524
  rootFiles
5832
6525
  )) {
5833
6526
  if (!proposals.some((existing) => existing.name === proposal.name)) {
@@ -5849,58 +6542,60 @@ var serviceInit = async (args) => {
5849
6542
  }
5850
6543
  }
5851
6544
  if (proposals.length === 0) {
5852
- return fail4(
6545
+ return fail5(
5853
6546
  "nothing to propose \u2014 no server-ish package.json script with a nameable port, no compose ports"
5854
6547
  );
5855
6548
  }
5856
6549
  const toml = renderMendToml(proposals);
5857
- say4(dim4(`proposed ${target}:`));
5858
- say4("");
6550
+ say5(dim5(`proposed ${target}:`));
6551
+ say5("");
5859
6552
  process.stdout.write(toml);
5860
- say4("");
6553
+ say5("");
5861
6554
  if (!args.includes("--yes")) {
5862
6555
  if (process.stdin.isTTY !== true) {
5863
- return fail4("non-interactive \u2014 pass --yes to write the file");
6556
+ return fail5("non-interactive \u2014 pass --yes to write the file");
5864
6557
  }
5865
6558
  const readline = await import("node:readline/promises");
5866
6559
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5867
6560
  const answer = await rl.question("write it? [y/N] ");
5868
6561
  rl.close();
5869
6562
  if (answer.trim().toLowerCase() !== "y") {
5870
- say4(dim4("nothing written"));
6563
+ say5(dim5("nothing written"));
5871
6564
  return;
5872
6565
  }
5873
6566
  }
5874
- fs9.writeFileSync(target, toml);
5875
- 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}`);
5876
6569
  };
5877
6570
  var tunnelServices = async (config, services, portOverride) => {
5878
- const tunnelUrl = (serviceId) => {
5879
- const url = parseMendUrl(`${config.url}/api/service-tunnel`);
5880
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
5881
- url.searchParams.set("service", serviceId);
5882
- if (config.token !== null) url.searchParams.set("token", config.token);
5883
- return url;
5884
- };
6571
+ const tunnelUrl = (serviceId) => socketUrl(config, "service-tunnel", { service: serviceId });
5885
6572
  for (const service of services) {
5886
6573
  const port = portOverride ?? service.hostPort ?? service.workspacePort;
5887
6574
  const server = net3.createServer((socket) => {
5888
6575
  socket.pause();
5889
- const ws = new WebSocket(tunnelUrl(service.id));
5890
- ws.binaryType = "arraybuffer";
5891
- ws.addEventListener("open", () => socket.resume(), { once: true });
5892
- ws.addEventListener("message", (event) => {
5893
- if (typeof event.data === "string") return;
5894
- socket.write(Buffer.from(event.data));
5895
- });
5896
- ws.addEventListener("close", () => socket.end(), { once: true });
5897
- ws.addEventListener("error", () => socket.destroy(), { once: true });
5898
- socket.on("data", (chunk) => ws.send(new Uint8Array(chunk)));
5899
- socket.on("end", () => {
5900
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ t: "eof" }));
5901
- });
5902
- socket.on("close", () => ws.close());
5903
- 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
+ );
5904
6599
  });
5905
6600
  await new Promise((resolve4, reject) => {
5906
6601
  server.once("error", (error) => {
@@ -5911,12 +6606,12 @@ var tunnelServices = async (config, services, portOverride) => {
5911
6606
  );
5912
6607
  });
5913
6608
  server.listen(port, "127.0.0.1", () => resolve4());
5914
- }).catch((error) => fail4(error.message));
5915
- say4(
5916
- `${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})`)}`
5917
6612
  );
5918
6613
  }
5919
- say4(dim4(" connections are authenticated as you \xB7 Ctrl-C stops"));
6614
+ say5(dim5(" connections are authenticated as you \xB7 Ctrl-C stops"));
5920
6615
  await new Promise(() => {
5921
6616
  });
5922
6617
  };
@@ -5924,18 +6619,18 @@ var serviceConnect = async (config, args) => {
5924
6619
  const portFlag = args.indexOf("--port");
5925
6620
  const portOverride = portFlag === -1 ? null : Number(args[portFlag + 1]);
5926
6621
  if (portOverride !== null && !Number.isInteger(portOverride)) {
5927
- return fail4("--port takes a port number");
6622
+ return fail5("--port takes a port number");
5928
6623
  }
5929
6624
  const names = args.filter((a, i) => !a.startsWith("--") && i !== portFlag + 1);
5930
6625
  const live = (await fetchServices(config)).filter((s) => s.protocol === "tcp");
5931
6626
  const picked = names.length === 0 ? live : live.filter((s) => names.some((n) => s.label === n || s.id.startsWith(n)));
5932
6627
  if (picked.length === 0) {
5933
- return fail4(
6628
+ return fail5(
5934
6629
  names.length === 0 ? "no live TCP services \u2014 mend service run starts one" : `no live TCP service matches "${names.join('", "')}"`
5935
6630
  );
5936
6631
  }
5937
6632
  if (portOverride !== null && picked.length !== 1) {
5938
- return fail4("--port applies to exactly one service \u2014 name it");
6633
+ return fail5("--port applies to exactly one service \u2014 name it");
5939
6634
  }
5940
6635
  await tunnelServices(config, picked, portOverride);
5941
6636
  };
@@ -5964,23 +6659,23 @@ var serviceCommand = async (config, args) => {
5964
6659
  }
5965
6660
  };
5966
6661
  var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
5967
- var readIfExists = (file) => fs9.existsSync(file) ? fs9.readFileSync(file, "utf8") : null;
6662
+ var readIfExists = (file) => fs11.existsSync(file) ? fs11.readFileSync(file, "utf8") : null;
5968
6663
  var localCredential = (provider) => {
5969
- const home = os6.homedir();
6664
+ const home = os7.homedir();
5970
6665
  switch (provider) {
5971
6666
  case "codex":
5972
6667
  return readIfExists(
5973
- 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")
5974
6669
  );
5975
6670
  case "claude":
5976
6671
  return readIfExists(
5977
- path10.join(
5978
- process.env["CLAUDE_CONFIG_DIR"] ?? path10.join(home, ".claude"),
6672
+ path12.join(
6673
+ process.env["CLAUDE_CONFIG_DIR"] ?? path12.join(home, ".claude"),
5979
6674
  ".credentials.json"
5980
6675
  )
5981
6676
  );
5982
6677
  case "github": {
5983
- const result = spawnSync2("gh", ["auth", "token"], { encoding: "utf8" });
6678
+ const result = spawnSync3("gh", ["auth", "token"], { encoding: "utf8" });
5984
6679
  const token = result.status === 0 ? result.stdout.trim() : "";
5985
6680
  return token === "" ? null : token;
5986
6681
  }
@@ -6012,16 +6707,94 @@ var accountsCommand = async (config) => {
6012
6707
  );
6013
6708
  }
6014
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
+ };
6015
6788
  var connectCommand = async (config, args) => {
6016
6789
  const [providerArg, ...flags] = args;
6017
6790
  if (!isProvider(providerArg)) {
6018
- return fail4(usageOf("connect"));
6791
+ return fail5(usageOf("connect"));
6019
6792
  }
6020
6793
  const provider = providerArg;
6021
6794
  if (flags.includes("--remove")) {
6022
6795
  const identity = await api(config, "GET", "/me/sealant");
6023
6796
  const account2 = identity.accounts.find((row) => row.provider === provider);
6024
- if (account2 === void 0) return fail4(`${provider}: nothing connected`);
6797
+ if (account2 === void 0) return fail5(`${provider}: nothing connected`);
6025
6798
  await api(config, "DELETE", `/me/sealant/accounts/${account2.id}`);
6026
6799
  process.stdout.write(`${provider}: disconnected
6027
6800
  `);
@@ -6029,35 +6802,60 @@ var connectCommand = async (config, args) => {
6029
6802
  }
6030
6803
  let secret;
6031
6804
  if (flags.includes("--from-stdin")) {
6032
- secret = fs9.readFileSync(0, "utf8").trim();
6033
- 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;
6034
6810
  } else {
6035
6811
  secret = localCredential(provider);
6036
6812
  if (secret === null) {
6037
- 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";
6038
- 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
+ );
6039
6822
  }
6040
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
+ }
6041
6828
  const account = await withSpinner(
6042
6829
  `connecting ${provider}`,
6043
- api(config, "POST", "/me/sealant/accounts", { provider, secret })
6830
+ api(config, "POST", "/me/sealant/accounts", {
6831
+ provider,
6832
+ secret: narrowed.secret
6833
+ })
6044
6834
  );
6045
6835
  process.stdout.write(`${accountLine(account)}
6046
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
+ }
6047
6845
  };
6048
- var takeFlagValue3 = (args, flag) => {
6846
+ var takeFlagValue4 = (args, flag) => {
6049
6847
  const at = args.indexOf(flag);
6050
6848
  return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
6051
6849
  };
6052
6850
  var saveCliConfig = (next) => {
6053
- fs9.mkdirSync(path10.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
6054
- fs9.writeFileSync(
6851
+ fs11.mkdirSync(path12.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
6852
+ fs11.writeFileSync(
6055
6853
  CONFIG_PATH,
6056
6854
  `${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
6057
6855
  `,
6058
6856
  { mode: 384 }
6059
6857
  );
6060
- fs9.chmodSync(CONFIG_PATH, 384);
6858
+ fs11.chmodSync(CONFIG_PATH, 384);
6061
6859
  };
6062
6860
  var login = async (config, args) => {
6063
6861
  await loginCommand(args, {
@@ -6065,25 +6863,25 @@ var login = async (config, args) => {
6065
6863
  defaultUrl: config.url,
6066
6864
  save: (next) => {
6067
6865
  saveCliConfig({ url: next.url, token: next.token, deviceId: next.deviceId });
6068
- say4(dim4(` token saved to ${CONFIG_PATH} (0600)`));
6866
+ say5(dim5(` token saved to ${CONFIG_PATH} (0600)`));
6069
6867
  }
6070
6868
  });
6071
6869
  };
6072
6870
  var logout = async (config) => {
6073
- if (!fs9.existsSync(CONFIG_PATH) && config.token === null) {
6074
- 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"));
6075
6873
  return;
6076
6874
  }
6077
6875
  if (config.token !== null && config.deviceId !== null) {
6078
6876
  try {
6079
6877
  await request(config, "DELETE", `/me/devices/${config.deviceId}`);
6080
- say4(`${green4("\u2713")} device revoked on ${config.url}`);
6878
+ say5(`${green5("\u2713")} device revoked on ${config.url}`);
6081
6879
  } catch {
6082
- 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"));
6083
6881
  }
6084
6882
  }
6085
6883
  saveCliConfig({ ...config, token: null, deviceId: null });
6086
- 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}`)}`);
6087
6885
  };
6088
6886
  var UNINSTALL_CHOICES = [
6089
6887
  { scope: "all", text: "everything \xB7 the server on this machine and this machine's Mend files" },
@@ -6095,20 +6893,20 @@ var UNINSTALL_CHOICES = [
6095
6893
  ];
6096
6894
  var askUninstallScope = async () => {
6097
6895
  if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
6098
- return fail4(`${UNINSTALL_USAGE} \xB7 no terminal to ask on`);
6896
+ return fail5(`${UNINSTALL_USAGE} \xB7 no terminal to ask on`);
6099
6897
  }
6100
- say4("what should go?");
6101
- 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}`));
6102
6900
  const readline = await import("node:readline/promises");
6103
6901
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6104
6902
  const answer = (await rl.question(" 1, 2 or 3: ")).trim();
6105
6903
  rl.close();
6106
6904
  const chosen = UNINSTALL_CHOICES[Number(answer) - 1];
6107
- 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`);
6108
6906
  return chosen.scope;
6109
6907
  };
6110
6908
  var confirmUninstall = async (word) => {
6111
- 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");
6112
6910
  const readline = await import("node:readline/promises");
6113
6911
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6114
6912
  const answer = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
@@ -6117,14 +6915,14 @@ var confirmUninstall = async (word) => {
6117
6915
  };
6118
6916
  var uninstallCommand = async (config, args) => {
6119
6917
  const parsed = parseUninstallArgs(args);
6120
- if ("error" in parsed) return fail4(parsed.error);
6918
+ if ("error" in parsed) return fail5(parsed.error);
6121
6919
  const scope = parsed.scope ?? await askUninstallScope();
6122
6920
  const server = nodeServerRuntime();
6123
6921
  const runtime = {
6124
6922
  server,
6125
6923
  cliHome: mendCliHome(),
6126
- sshConfigFile: path10.join(os6.homedir(), ".ssh", "config"),
6127
- 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 },
6128
6926
  revokeDevice: async () => {
6129
6927
  if (config.deviceId === null) return "no device id saved";
6130
6928
  try {
@@ -6136,42 +6934,42 @@ var uninstallCommand = async (config, args) => {
6136
6934
  }
6137
6935
  };
6138
6936
  const plan = await describeUninstall(runtime, scope);
6139
- say4(dim4(`mend uninstall \xB7 ${scope === "all" ? "everything" : scope}`));
6140
- 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}`);
6141
6939
  const nothing = (plan.server === null || plan.server === "none") && (plan.home === null || plan.home.cliConfig === null && plan.home.sshDirectory === null && plan.home.managedSshBlocks === 0);
6142
6940
  if (nothing) {
6143
- say4(dim4("nothing to remove"));
6941
+ say5(dim5("nothing to remove"));
6144
6942
  return;
6145
6943
  }
6146
6944
  if (planDeletesData(plan)) {
6147
- 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");
6148
6946
  }
6149
6947
  if (!parsed.yes && !await confirmUninstall(planDeletesData(plan) ? "delete" : "y")) {
6150
- say4(dim4("nothing removed"));
6948
+ say5(dim5("nothing removed"));
6151
6949
  return;
6152
6950
  }
6153
6951
  const outcome = await executeUninstall(runtime, plan);
6154
- for (const line of outcome.leftovers) say4(dim4(` kept \xB7 ${line}`));
6952
+ for (const line of outcome.leftovers) say5(dim5(` kept \xB7 ${line}`));
6155
6953
  if (outcome.failures.length > 0) {
6156
- return fail4(outcome.failures.join("\n"));
6954
+ return fail5(outcome.failures.join("\n"));
6157
6955
  }
6158
- say4(
6159
- `${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"}`
6160
6958
  );
6161
6959
  };
6162
6960
  var printGitKey = (key) => {
6163
6961
  if (key.publicKey === null) return;
6164
- say4(key.publicKey);
6165
- if (key.fingerprint !== null) say4(dim4(` ${key.fingerprint}`));
6166
- say4(dim4(" add this to your git account's SSH keys (GitHub: settings \u2192 SSH keys) so"));
6167
- say4(dim4(" every repository you can reach works; for one repository only, add it"));
6168
- 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)"));
6169
6967
  };
6170
6968
  var keysShow = async (config) => {
6171
6969
  const key = await api(config, "GET", "/keys/git");
6172
6970
  if (!key.exists) {
6173
- say4(
6174
- `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)")}`
6175
6973
  );
6176
6974
  return;
6177
6975
  }
@@ -6179,46 +6977,41 @@ var keysShow = async (config) => {
6179
6977
  };
6180
6978
  var keysInit = async (config) => {
6181
6979
  const key = await api(config, "POST", "/keys/git");
6182
- 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)")}`);
6183
6981
  printGitKey(key);
6184
6982
  };
6185
6983
  var keysShare = async (config) => {
6186
6984
  const agentSock = process.env["SSH_AUTH_SOCK"];
6187
6985
  if (agentSock === void 0 || agentSock === "") {
6188
- 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");
6189
6987
  }
6190
- const url = bridgeUrlOf(
6191
- parseMendUrl(config.url).toString().replace(/\/$/, ""),
6192
- config.token,
6193
- os6.hostname()
6194
- );
6195
6988
  await shareAgent({
6196
- url,
6989
+ url: () => socketUrl(config, "keys-bridge", { host: os7.hostname() }),
6197
6990
  agentSock,
6198
6991
  signal: new AbortController().signal,
6199
6992
  onEvent: (event) => {
6200
6993
  switch (event.kind) {
6201
6994
  case "connected":
6202
- say4(`${green4("\u25CF")} sharing this machine's ssh-agent with ${config.url}`);
6203
- 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`));
6204
6997
  return;
6205
6998
  case "disconnected":
6206
- say4(
6207
- 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)`)
6208
7001
  );
6209
7002
  return;
6210
7003
  case "sign-requested":
6211
- say4(`${amber("\u270E")} signature requested by mend ${dim4(`(${event.context})`)}`);
6212
- 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"));
6213
7006
  return;
6214
7007
  case "identities-requested":
6215
- say4(dim4(` identities requested (${event.context})`));
7008
+ say5(dim5(` identities requested (${event.context})`));
6216
7009
  return;
6217
7010
  case "signed":
6218
- say4(`${green4("\u2713")} signed ${dim4(`(${event.seconds.toFixed(1)}s)`)}`);
7011
+ say5(`${green5("\u2713")} signed ${dim5(`(${event.seconds.toFixed(1)}s)`)}`);
6219
7012
  return;
6220
7013
  case "not-signed":
6221
- say4(`${amber("\u2717")} not signed \u2014 ${event.message}`);
7014
+ say5(`${amber("\u2717")} not signed \u2014 ${event.message}`);
6222
7015
  return;
6223
7016
  }
6224
7017
  }
@@ -6227,19 +7020,19 @@ var keysShare = async (config) => {
6227
7020
  var keysMode = async (config, args) => {
6228
7021
  const [value] = args;
6229
7022
  if (value !== void 0 && value !== "mend-key" && value !== "bridge") {
6230
- return fail4(`keys mode takes mend-key or bridge, not "${value}"`);
7023
+ return fail5(`keys mode takes mend-key or bridge, not "${value}"`);
6231
7024
  }
6232
7025
  const access = value === void 0 ? await api(config, "GET", "/me/git-access") : await api(config, "PUT", "/me/git-access", { mode: value });
6233
- if (value !== void 0) say4(`${green4("\u2713")} git access \xB7 ${value}`);
7026
+ if (value !== void 0) say5(`${green5("\u2713")} git access \xB7 ${value}`);
6234
7027
  if (access.mode === "mend-key") {
6235
- 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")}`);
6236
7029
  if (access.key.exists) printGitKey(access.key);
6237
- 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"));
6238
7031
  return;
6239
7032
  }
6240
- say4(`bridge ${dim4("\u2014 this machine's ssh-agent signs; only while a mend command runs here")}`);
6241
- say4(
6242
- 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)")
6243
7036
  );
6244
7037
  };
6245
7038
  var keysCommand = async (config, args) => {
@@ -6255,75 +7048,75 @@ var keysCommand = async (config, args) => {
6255
7048
  case void 0:
6256
7049
  return keysShow(config);
6257
7050
  default:
6258
- 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`);
6259
7052
  }
6260
7053
  };
6261
7054
  var formatDotfileBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
6262
7055
  var dotfilesShow = async (config) => {
6263
7056
  const dotfiles = await api(config, "GET", "/dotfiles");
6264
7057
  if (dotfiles.repository === null) {
6265
- say4(`repo ${dim4("none")}`);
7058
+ say5(`repo ${dim5("none")}`);
6266
7059
  } else {
6267
7060
  const branch = dotfiles.repository.ref ?? "default branch";
6268
7061
  const subdir = dotfiles.repository.subdirectory === null ? [] : [`${dotfiles.repository.subdirectory}/`];
6269
7062
  const bootstrap = dotfiles.repository.bootstrap ? "install.sh on" : "install.sh off";
6270
- say4(
6271
- `repo ${dotfiles.repository.url} ${dim4(`(${[branch, ...subdir, bootstrap].join(" \xB7 ")})`)}`
7063
+ say5(
7064
+ `repo ${dotfiles.repository.url} ${dim5(`(${[branch, ...subdir, bootstrap].join(" \xB7 ")})`)}`
6272
7065
  );
6273
7066
  }
6274
7067
  if (dotfiles.snapshot === null) {
6275
- 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")}`);
6276
7069
  return;
6277
7070
  }
6278
7071
  const snapshot = dotfiles.snapshot;
6279
- say4(
6280
- `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))}`
6281
7074
  );
6282
7075
  for (const file of snapshot.files) {
6283
- say4(` ${file.path.padEnd(36)} ${dim4(formatDotfileBytes(file.bytes))}`);
7076
+ say5(` ${file.path.padEnd(36)} ${dim5(formatDotfileBytes(file.bytes))}`);
6284
7077
  }
6285
7078
  };
6286
7079
  var dotfilesSync = async (config, args) => {
6287
7080
  const all = args.includes("--all");
6288
7081
  const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
6289
- const home = os6.homedir();
7082
+ const home = os7.homedir();
6290
7083
  if (!all && requestedPaths.length === 0) {
6291
7084
  const found = scanDotfileCandidates(home);
6292
7085
  if (found.length === 0) {
6293
- say4(dim4("no known config files found under ~"));
7086
+ say5(dim5("no known config files found under ~"));
6294
7087
  return;
6295
7088
  }
6296
7089
  let group = "";
6297
7090
  for (const entry of found) {
6298
7091
  if (entry.group !== group) {
6299
7092
  group = entry.group;
6300
- say4(dim4(group));
7093
+ say5(dim5(group));
6301
7094
  }
6302
- say4(` ${entry.path.padEnd(36)} ${dim4(formatDotfileBytes(entry.bytes))}`);
7095
+ say5(` ${entry.path.padEnd(36)} ${dim5(formatDotfileBytes(entry.bytes))}`);
6303
7096
  }
6304
- say4("");
6305
- say4(
7097
+ say5("");
7098
+ say5(
6306
7099
  `sync everything with ${cobalt2("mend dotfiles sync --all")}, or pick: ${cobalt2("mend dotfiles sync .zshrc .gitconfig")}`
6307
7100
  );
6308
7101
  return;
6309
7102
  }
6310
7103
  const selected = all ? scanDotfileCandidates(home).map((entry) => entry.path) : requestedPaths;
6311
- 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 ~");
6312
7105
  const read = readSyncFiles(home, selected);
6313
- if ("error" in read) return fail4(read.error);
7106
+ if ("error" in read) return fail5(read.error);
6314
7107
  const result = await api(config, "POST", "/dotfiles/snapshot", {
6315
7108
  files: read.files,
6316
- source: os6.hostname(),
7109
+ source: os7.hostname(),
6317
7110
  merge: false
6318
7111
  });
6319
7112
  const snapshot = result.snapshot;
6320
- if (snapshot === null) return fail4("the server accepted the sync but reports no snapshot");
6321
- say4(
6322
- `${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")}`
6323
7116
  );
6324
7117
  };
6325
7118
  var envLoad = async (config, args) => {
6326
- const explicitProject = takeFlagValue3(args, "--project");
7119
+ const explicitProject = takeFlagValue4(args, "--project");
6327
7120
  const secretFlag = args.indexOf("--secret");
6328
7121
  const secretArg = secretFlag === -1 ? void 0 : args[secretFlag + 1];
6329
7122
  const secretNames = secretArg !== void 0 && !secretArg.startsWith("--") && !secretArg.includes("/") && !secretArg.includes(".") ? secretArg.split(",").map((s) => s.trim()).filter((s) => s !== "") : [];
@@ -6333,15 +7126,15 @@ var envLoad = async (config, args) => {
6333
7126
  const positional = args.filter(
6334
7127
  (arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
6335
7128
  );
6336
- const file = path10.resolve(positional[0] ?? ".env");
7129
+ const file = path12.resolve(positional[0] ?? ".env");
6337
7130
  let contents;
6338
7131
  try {
6339
- contents = fs9.readFileSync(file, "utf8");
7132
+ contents = fs11.readFileSync(file, "utf8");
6340
7133
  } catch {
6341
- 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`);
6342
7135
  }
6343
7136
  if (contents.trim() === "") {
6344
- say4(dim4(`${file} is empty`));
7137
+ say5(dim5(`${file} is empty`));
6345
7138
  return;
6346
7139
  }
6347
7140
  const project2 = await findProject(config, explicitProject);
@@ -6352,46 +7145,46 @@ var envLoad = async (config, args) => {
6352
7145
  { contents, allSecret, secretNames }
6353
7146
  );
6354
7147
  if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
6355
- say4(dim4(`${path10.basename(file)} has no variables`));
7148
+ say5(dim5(`${path12.basename(file)} has no variables`));
6356
7149
  return;
6357
7150
  }
6358
- say4(`${green4("\u2713")} loaded ${path10.basename(file)} into ${project2.name}`);
6359
- 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);
6360
7153
  const plaintextUrls = report.loaded.filter(
6361
7154
  (entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
6362
7155
  );
6363
7156
  if (plaintextUrls.length > 0) {
6364
- say4(
7157
+ say5(
6365
7158
  amber(
6366
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(",")}`
6367
7160
  )
6368
7161
  );
6369
7162
  }
6370
7163
  if (report.malformedLines.length > 0) {
6371
- say4(
7164
+ say5(
6372
7165
  amber(
6373
7166
  ` skipped ${report.malformedLines.length} malformed line${report.malformedLines.length === 1 ? "" : "s"}: ${report.malformedLines.join(", ")}`
6374
7167
  )
6375
7168
  );
6376
7169
  }
6377
- say4(
6378
- dim4(
7170
+ say5(
7171
+ dim5(
6379
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`
6380
7173
  )
6381
7174
  );
6382
7175
  };
6383
7176
  var envShow = async (config, args) => {
6384
- const project2 = await findProject(config, takeFlagValue3(args, "--project"));
7177
+ const project2 = await findProject(config, takeFlagValue4(args, "--project"));
6385
7178
  const [environment, secrets, cluster] = await Promise.all([
6386
7179
  api(config, "GET", `/projects/${project2.id}/environment`),
6387
7180
  api(config, "GET", `/projects/${project2.id}/secrets`),
6388
7181
  api(config, "GET", `/projects/${project2.id}/cluster-bindings`)
6389
7182
  ]);
6390
- say4(
6391
- `${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}`)}`
6392
7185
  );
6393
7186
  if (environment.variables.length === 0 && secrets.secrets.length === 0 && cluster.bindings.length === 0 && cluster.serviceAccount === null) {
6394
- 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")}`));
6395
7188
  return;
6396
7189
  }
6397
7190
  const bindingNames = cluster.bindings.map((b) => `${b.kind}/${b.objectName}`);
@@ -6402,23 +7195,23 @@ var envShow = async (config, args) => {
6402
7195
  ...bindingNames.map((name) => name.length)
6403
7196
  );
6404
7197
  for (const variable of environment.variables) {
6405
- say4(` ${variable.name.padEnd(width)} configuration ${dim4("\xB7 plaintext")}`);
7198
+ say5(` ${variable.name.padEnd(width)} configuration ${dim5("\xB7 plaintext")}`);
6406
7199
  }
6407
7200
  for (const secret of secrets.secrets) {
6408
- 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")}`);
6409
7202
  }
6410
7203
  for (const name of bindingNames) {
6411
- say4(
6412
- ` ${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")}`
6413
7206
  );
6414
7207
  }
6415
7208
  if (cluster.serviceAccount !== null) {
6416
- say4(
6417
- ` ${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")}`
6418
7211
  );
6419
7212
  }
6420
7213
  if (!cluster.clusterCapable && (cluster.bindings.length > 0 || cluster.serviceAccount !== null)) {
6421
- say4(
7214
+ say5(
6422
7215
  amber(
6423
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`
6424
7217
  )
@@ -6426,54 +7219,54 @@ var envShow = async (config, args) => {
6426
7219
  }
6427
7220
  };
6428
7221
  var envCluster = async (config, args) => {
6429
- const explicitProject = takeFlagValue3(args, "--project");
7222
+ const explicitProject = takeFlagValue4(args, "--project");
6430
7223
  const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
6431
7224
  const [verb, ...rest] = positional;
6432
7225
  const usage = usageOf("env cluster");
6433
7226
  const project2 = await findProject(config, explicitProject);
6434
7227
  const route = `/projects/${project2.id}/cluster-bindings`;
6435
- const appliesLine = () => say4(
6436
- dim4(
7228
+ const appliesLine = () => say5(
7229
+ dim5(
6437
7230
  " applies from the next workspace launch; running workspaces keep what they started with"
6438
7231
  )
6439
7232
  );
6440
7233
  if (verb === "add") {
6441
7234
  const [kind, objectName] = rest;
6442
7235
  if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
6443
- 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
6444
7237
  ${usage}`);
6445
7238
  }
6446
7239
  const result = await api(config, "POST", route, {
6447
7240
  kind,
6448
7241
  objectName
6449
7242
  });
6450
- say4(`${green4("\u2713")} bound ${kind}/${objectName} ${dim4(`\xB7 cluster r${result.revision}`)}`);
6451
- 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"));
6452
7245
  return appliesLine();
6453
7246
  }
6454
7247
  if (verb === "remove") {
6455
7248
  const [ref] = rest;
6456
7249
  const [kind, objectName] = ref?.split("/", 2) ?? [];
6457
7250
  if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
6458
- 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
6459
7252
  ${usage}`);
6460
7253
  }
6461
7254
  const snapshot = await api(config, "GET", route);
6462
7255
  const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
6463
- 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}`);
6464
7257
  const result = await api(
6465
7258
  config,
6466
7259
  "DELETE",
6467
7260
  `${route}/${binding.id}`
6468
7261
  );
6469
- 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}`)}`);
6470
7263
  return appliesLine();
6471
7264
  }
6472
7265
  if (verb === "sa") {
6473
7266
  const clear = args.includes("--clear");
6474
7267
  const [name] = rest;
6475
7268
  if (!clear && name === void 0)
6476
- return fail4(`env cluster sa takes a name or --clear
7269
+ return fail5(`env cluster sa takes a name or --clear
6477
7270
  ${usage}`);
6478
7271
  const result = await api(
6479
7272
  config,
@@ -6481,11 +7274,11 @@ ${usage}`);
6481
7274
  `${route}/service-account`,
6482
7275
  { serviceAccount: clear ? null : name }
6483
7276
  );
6484
- say4(
6485
- 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}`)}`
6486
7279
  );
6487
7280
  if (result.serviceAccount !== null) {
6488
- say4(
7281
+ say5(
6489
7282
  amber(
6490
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"
6491
7284
  )
@@ -6493,7 +7286,7 @@ ${usage}`);
6493
7286
  }
6494
7287
  return appliesLine();
6495
7288
  }
6496
- return fail4(`unknown env cluster command "${verb ?? ""}"
7289
+ return fail5(`unknown env cluster command "${verb ?? ""}"
6497
7290
  ${usage}`);
6498
7291
  };
6499
7292
  var envCommand = async (config, args) => {
@@ -6507,7 +7300,7 @@ var envCommand = async (config, args) => {
6507
7300
  case void 0:
6508
7301
  return envShow(config, rest);
6509
7302
  default:
6510
- 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`);
6511
7304
  }
6512
7305
  };
6513
7306
  var dotfilesCommand = async (config, args) => {
@@ -6519,12 +7312,12 @@ var dotfilesCommand = async (config, args) => {
6519
7312
  case void 0:
6520
7313
  return dotfilesShow(config);
6521
7314
  default:
6522
- 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`);
6523
7316
  }
6524
7317
  };
6525
7318
  var skillsList = async (config, args) => {
6526
7319
  const forProject = args.includes("--project");
6527
- const project2 = forProject ? await findProject(config, takeFlagValue3(args, "--project")) : null;
7320
+ const project2 = forProject ? await findProject(config, takeFlagValue4(args, "--project")) : null;
6528
7321
  const skills = await api(
6529
7322
  config,
6530
7323
  "GET",
@@ -6532,25 +7325,25 @@ var skillsList = async (config, args) => {
6532
7325
  );
6533
7326
  const library = project2 === null ? "your library" : project2.name;
6534
7327
  if (skills.length === 0) {
6535
- 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`));
6536
7329
  return;
6537
7330
  }
6538
7331
  for (const skill of skills) {
6539
- say4(
6540
- ` ${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)}`)}`
6541
7334
  );
6542
- if (skill.description !== "") say4(` ${dim4(skill.description)}`);
7335
+ if (skill.description !== "") say5(` ${dim5(skill.description)}`);
6543
7336
  }
6544
7337
  };
6545
7338
  var skillsPush = async (config, args) => {
6546
- const dirFlag = takeFlagValue3(args, "--dir");
6547
- 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);
6548
7341
  const scanned = scanSkillLibrary(root);
6549
- if ("error" in scanned) return fail4(scanned.error);
6550
- for (const note of scanned.notes) say4(dim4(` ${note.skill}: ${note.message}`));
6551
- 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}`);
6552
7345
  const forProject = args.includes("--project");
6553
- const project2 = forProject ? await findProject(config, takeFlagValue3(args, "--project")) : null;
7346
+ const project2 = forProject ? await findProject(config, takeFlagValue4(args, "--project")) : null;
6554
7347
  const report = await api(config, "POST", "/skills/sync", {
6555
7348
  scope: project2 === null ? "user" : "project",
6556
7349
  projectId: project2?.id ?? null,
@@ -6568,10 +7361,10 @@ var skillsPush = async (config, args) => {
6568
7361
  `${report.unchanged.length} unchanged`,
6569
7362
  ...report.removed.length > 0 ? [`${report.removed.length} removed`] : []
6570
7363
  ].join(" \xB7 ");
6571
- say4(
6572
- `${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)}`
6573
7366
  );
6574
- say4(dim4("sessions receive them from the next launch"));
7367
+ say5(dim5("sessions receive them from the next launch"));
6575
7368
  };
6576
7369
  var skillsCommand = async (config, args) => {
6577
7370
  const [verb, ...rest] = args;
@@ -6584,7 +7377,7 @@ var skillsCommand = async (config, args) => {
6584
7377
  return skillsList(config, args);
6585
7378
  default:
6586
7379
  if (verb.startsWith("--")) return skillsList(config, args);
6587
- 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`);
6588
7381
  }
6589
7382
  };
6590
7383
  var completeCommand = async (config, args) => {
@@ -6688,7 +7481,7 @@ var completionsCommand = (args) => {
6688
7481
  process.stdout.write(BASH_COMPLETIONS);
6689
7482
  return;
6690
7483
  default:
6691
- 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"`);
6692
7485
  }
6693
7486
  };
6694
7487
  var supervisedRun = async (config, session, argv) => {
@@ -6696,15 +7489,15 @@ var supervisedRun = async (config, session, argv) => {
6696
7489
  "provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
6697
7490
  api(config, "POST", `/sessions/${session.id}/launch`, { argv })
6698
7491
  );
6699
- say4(
6700
- `${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`
6701
7494
  );
6702
- say4(dim4(` ${argv.join(" ")} \u2014 live record:`));
6703
- say4("");
7495
+ say5(dim5(` ${argv.join(" ")} \u2014 live record:`));
7496
+ say5("");
6704
7497
  const headers = {};
6705
7498
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
6706
7499
  const response = await fetch(`${config.url}/api/events`, { headers });
6707
- if (response.body === null) return fail4("event stream unavailable");
7500
+ if (response.body === null) return fail5("event stream unavailable");
6708
7501
  const reader = response.body.getReader();
6709
7502
  const decoder = new TextDecoder();
6710
7503
  let buffer = "";
@@ -6726,7 +7519,7 @@ var supervisedRun = async (config, session, argv) => {
6726
7519
  }
6727
7520
  if (event.sessionId !== session.id) continue;
6728
7521
  if (event.type === "session-progress" && event.line !== void 0) {
6729
- say4(dim4(` ${event.line}`));
7522
+ say5(dim5(` ${event.line}`));
6730
7523
  }
6731
7524
  if (event.type === "session") {
6732
7525
  const detail = await api(config, "GET", `/sessions/${session.id}`);
@@ -6736,9 +7529,9 @@ var supervisedRun = async (config, session, argv) => {
6736
7529
  }
6737
7530
  }
6738
7531
  await reader.cancel().catch(() => void 0);
6739
- say4("");
6740
- say4(`${green4("\u2713")} session ${settled ?? "settled"} \xB7 recorded \xB7 checkpoint taken`);
6741
- 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}`);
6742
7535
  process.exit(settled === "failed" ? 1 : 0);
6743
7536
  };
6744
7537
  var continueSession = async (config, args) => {
@@ -6761,20 +7554,20 @@ var continueSession = async (config, args) => {
6761
7554
  }
6762
7555
  }
6763
7556
  if (sessionId === null || followUp === null) {
6764
- 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");
6765
7558
  }
6766
7559
  } else {
6767
7560
  followUp = await api(config, "GET", `/sessions/${sessionId}/follow-up`);
6768
- 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`);
6769
7562
  }
6770
7563
  const detail = await api(config, "GET", `/sessions/${sessionId}`);
6771
7564
  const session = detail.session;
6772
- say4(`${green4("\u2713")} follow-up for session ${dim4(session.id.slice(0, 8))} \xB7 ${session.branch}`);
6773
- say4(dim4(" instruction:"));
6774
- for (const line of followUp.instruction.split("\n").slice(0, 6)) say4(dim4(` \u2502 ${line}`));
6775
- 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"));
6776
7569
  if (followUp.reviewSliceId === null || followUp.checkpointAId === null || followUp.checkpointBId === null || followUp.diffDigest === null || followUp.idempotencyKey === null) {
6777
- 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");
6778
7571
  }
6779
7572
  const delivered = await withSpinner(
6780
7573
  "delivering persisted Review bundle \u2014 retries reconcile one process\u2026",
@@ -6789,16 +7582,16 @@ var continueSession = async (config, args) => {
6789
7582
  })
6790
7583
  );
6791
7584
  if (delivered.status === "pending") {
6792
- 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");
6793
7586
  }
6794
7587
  if (delivered.status === "delivery_failed") {
6795
- return fail4(delivered.deliveryError ?? "delivery failed before process membership finalized");
7588
+ return fail5(delivered.deliveryError ?? "delivery failed before process membership finalized");
6796
7589
  }
6797
- say4(
6798
- `${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()}`
6799
7592
  );
6800
- say4(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
6801
- say4("");
7593
+ say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
7594
+ say5("");
6802
7595
  await attachOrExit(config, session.id, session.harness);
6803
7596
  exitAfterSessionEnd(config, session.id);
6804
7597
  };
@@ -6815,27 +7608,27 @@ var resumeCommand = async (config, args) => {
6815
7608
  const detail = await api(config, "GET", `/projects/${project2.id}`);
6816
7609
  const match = prefix === void 0 ? detail.sessions.find((s) => !agentLiveIn(detail, s)) : detail.sessions.find((s) => s.id.startsWith(prefix));
6817
7610
  if (match === void 0) {
6818
- return fail4(
7611
+ return fail5(
6819
7612
  prefix === void 0 ? "no settled session to resume \u2014 mend status lists sessions" : `no session matches "${prefix}"`
6820
7613
  );
6821
7614
  }
6822
7615
  if (agentLiveIn(detail, match)) {
6823
- return fail4(
7616
+ return fail5(
6824
7617
  `session ${match.id.slice(0, 8)} is live \u2014 attach: mend attach ${match.id.slice(0, 8)}`
6825
7618
  );
6826
7619
  }
6827
- say4(
6828
- `${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}`}`
6829
7622
  );
6830
- say4(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
7623
+ say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
6831
7624
  const priorAgent = detail.annotations.find((annotation) => annotation.sessionId === match.id)?.currentAgent ?? null;
6832
7625
  const protocolPrior = priorAgent?.kind === "agent-protocol" && withHarness === null;
6833
7626
  await withSpinner(
6834
7627
  protocolPrior ? "reopening as a terminal \u2014 same conversation\u2026" : "resuming \u2014 a fresh workspace restores the saved session state\u2026",
6835
7628
  protocolPrior ? api(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : api(config, "POST", `/sessions/${match.id}/resume`, { harness: withHarness })
6836
7629
  );
6837
- say4(`${green4("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
6838
- say4("");
7630
+ say5(`${green5("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
7631
+ say5("");
6839
7632
  await attachOrExit(config, match.id, withHarness ?? match.harness);
6840
7633
  exitAfterSessionEnd(config, match.id);
6841
7634
  };
@@ -6849,7 +7642,7 @@ var resumeForRejoin = async (config, sessionId, label) => {
6849
7642
  } catch (error) {
6850
7643
  const refreshed = await api(config, "GET", `/sessions/${sessionId}`);
6851
7644
  if (agentIsLive(refreshed.session, refreshed.currentAgent)) return false;
6852
- return fail4(error instanceof Error ? error.message : String(error));
7645
+ return fail5(error instanceof Error ? error.message : String(error));
6853
7646
  }
6854
7647
  };
6855
7648
  var rejoinCommand = async (config, args) => {
@@ -6863,20 +7656,20 @@ var rejoinCommand = async (config, args) => {
6863
7656
  const matches = prefix === void 0 ? newest === void 0 ? [] : [newest] : eligible.filter((session2) => session2.id.startsWith(prefix));
6864
7657
  if (matches.length === 0) {
6865
7658
  const harnessDescription = harness === null ? "" : ` ${harness}`;
6866
- return fail4(
7659
+ return fail5(
6867
7660
  prefix === void 0 ? `no${harnessDescription} session to rejoin in project ${project2.name}` : `no${harnessDescription} session matches "${prefix}" in project ${project2.name}`
6868
7661
  );
6869
7662
  }
6870
7663
  if (matches.length > 1) {
6871
- 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`);
6872
7665
  }
6873
7666
  const session = matches[0];
6874
- if (session === void 0) return fail4("session selection failed");
7667
+ if (session === void 0) return fail5("session selection failed");
6875
7668
  const alreadyLive = agentLiveIn(detail, session);
6876
- say4(
6877
- `${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"}`
6878
7671
  );
6879
- say4(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
7672
+ say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
6880
7673
  let restored = false;
6881
7674
  const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ?? null;
6882
7675
  if (currentAgent?.kind === "agent-protocol") {
@@ -6892,10 +7685,10 @@ var rejoinCommand = async (config, args) => {
6892
7685
  "resuming \u2014 a fresh workspace restores the saved session state\u2026"
6893
7686
  );
6894
7687
  }
6895
- say4(
6896
- 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()}`
6897
7690
  );
6898
- say4("");
7691
+ say5("");
6899
7692
  let outcome = await attachTty(config, session.id, session.harness, 0n, void 0, {
6900
7693
  handleSignals: true
6901
7694
  });
@@ -6921,7 +7714,7 @@ var projectsCommand = async (config) => {
6921
7714
  api(config, "GET", "/sessions")
6922
7715
  ]);
6923
7716
  if (projects.length === 0) {
6924
- say4(dim4("no adopted projects \u2014 mend adopt brings one in"));
7717
+ say5(dim5("no adopted projects \u2014 mend adopt brings one in"));
6925
7718
  return;
6926
7719
  }
6927
7720
  const liveByProject = /* @__PURE__ */ new Map();
@@ -6933,30 +7726,30 @@ var projectsCommand = async (config) => {
6933
7726
  const here = matchProjectByCwd(projects, cwdFacts(process.cwd()));
6934
7727
  for (const project2 of projects) {
6935
7728
  const live = liveByProject.get(project2.id) ?? 0;
6936
- const liveLabel = live > 0 ? green4(`${live} live`) : dim4("\u2014");
7729
+ const liveLabel = live > 0 ? green5(`${live} live`) : dim5("\u2014");
6937
7730
  const marker = project2.id === here?.id ? cobalt2("\u25B8 ") : " ";
6938
- say4(
6939
- `${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)}`
6940
7733
  );
6941
7734
  }
6942
- 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`));
6943
7736
  };
6944
7737
  var refreshCommand = async (config, args) => {
6945
- const explicit = args.find((a) => !a.startsWith("--")) ?? takeFlagValue3(args, "--project");
7738
+ const explicit = args.find((a) => !a.startsWith("--")) ?? takeFlagValue4(args, "--project");
6946
7739
  const project2 = await findProject(config, explicit ?? null);
6947
7740
  const branches = await api(
6948
7741
  config,
6949
7742
  "POST",
6950
7743
  `/projects/${project2.id}/refresh`
6951
7744
  );
6952
- say4(`${green4("\u2713")} refreshed ${project2.name} ${dim4(`\xB7 ${branches.length} branches`)}`);
7745
+ say5(`${green5("\u2713")} refreshed ${project2.name} ${dim5(`\xB7 ${branches.length} branches`)}`);
6953
7746
  for (const branch of branches.slice(0, 12)) {
6954
7747
  const marker = branch.isDefault ? cobalt2("\u25B8 ") : " ";
6955
- say4(
6956
- `${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))}`
6957
7750
  );
6958
7751
  }
6959
- if (branches.length > 12) say4(dim4(` \u2026 ${branches.length - 12} more`));
7752
+ if (branches.length > 12) say5(dim5(` \u2026 ${branches.length - 12} more`));
6960
7753
  };
6961
7754
  var printSessionRow = (row) => {
6962
7755
  const { session, annotation } = row;
@@ -6970,8 +7763,8 @@ var printSessionRow = (row) => {
6970
7763
  if (annotation !== void 0 && annotation.pendingFollowUp)
6971
7764
  facts.push(amber("follow-up pending"));
6972
7765
  const base = session.baseRef === null ? session.baseSha.slice(0, 12) : session.baseRef;
6973
- say4(
6974
- `${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 "))}` : ""}`
6975
7768
  );
6976
7769
  };
6977
7770
  var sessionsCommand = async (config, args) => {
@@ -6979,7 +7772,7 @@ var sessionsCommand = async (config, args) => {
6979
7772
  if (args.includes("--json=v2")) {
6980
7773
  const projectFlag2 = args.indexOf("--project");
6981
7774
  const projectName2 = projectFlag2 !== -1 && args[projectFlag2 + 1] !== void 0 ? String(args[projectFlag2 + 1]) : null;
6982
- say4(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
7775
+ say5(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
6983
7776
  return;
6984
7777
  }
6985
7778
  const json = args.includes("--json");
@@ -6988,7 +7781,7 @@ var sessionsCommand = async (config, args) => {
6988
7781
  const projects = await api(config, "GET", "/projects");
6989
7782
  const scope = projectName === null ? projects : projects.filter((p) => p.name === projectName);
6990
7783
  if (projectName !== null && scope.length === 0) {
6991
- return fail4(`no adopted project named "${projectName}"`);
7784
+ return fail5(`no adopted project named "${projectName}"`);
6992
7785
  }
6993
7786
  let rows;
6994
7787
  if (all || projectName !== null) {
@@ -7015,11 +7808,11 @@ var sessionsCommand = async (config, args) => {
7015
7808
  }
7016
7809
  if (rows.length === 0) {
7017
7810
  if (json) {
7018
- say4(JSON.stringify({ version: 1, sessions: [] }, null, 2));
7811
+ say5(JSON.stringify({ version: 1, sessions: [] }, null, 2));
7019
7812
  return;
7020
7813
  }
7021
- say4(
7022
- 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")
7023
7816
  );
7024
7817
  return;
7025
7818
  }
@@ -7053,7 +7846,7 @@ var sessionsCommand = async (config, args) => {
7053
7846
  }
7054
7847
  }))
7055
7848
  };
7056
- say4(JSON.stringify(payload, null, 2));
7849
+ say5(JSON.stringify(payload, null, 2));
7057
7850
  return;
7058
7851
  }
7059
7852
  for (const row of rows) printSessionRow(row);
@@ -7133,11 +7926,11 @@ var worktreesCommand = async (config, args) => {
7133
7926
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
7134
7927
  const payload = await buildWorktreesJson(config, projectName);
7135
7928
  if (json) {
7136
- say4(JSON.stringify(payload, null, 2));
7929
+ say5(JSON.stringify(payload, null, 2));
7137
7930
  return;
7138
7931
  }
7139
7932
  if (payload.worktrees.length === 0) {
7140
- 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"));
7141
7934
  return;
7142
7935
  }
7143
7936
  for (const worktree of payload.worktrees) {
@@ -7145,19 +7938,19 @@ var worktreesCommand = async (config, args) => {
7145
7938
  const facts = [
7146
7939
  `${worktree.sessions.length} session${worktree.sessions.length === 1 ? "" : "s"}`
7147
7940
  ];
7148
- if (live > 0) facts.push(green4(`${live} live`));
7941
+ if (live > 0) facts.push(green5(`${live} live`));
7149
7942
  if (worktree.review !== null && worktree.review.openComments > 0) {
7150
7943
  facts.push(amber(`${worktree.review.openComments} open`));
7151
7944
  }
7152
7945
  const base = worktree.baseRef ?? worktree.baseSha.slice(0, 12);
7153
- say4(
7154
- `${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 "))}`
7155
7948
  );
7156
7949
  for (const session of worktree.sessions) {
7157
7950
  const status = session.status.padEnd(9);
7158
7951
  const name = session.label ?? `session ${session.id.slice(0, 8)}`;
7159
- say4(
7160
- ` \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}`
7161
7954
  );
7162
7955
  }
7163
7956
  }
@@ -7172,24 +7965,24 @@ var hasNodeFfi = () => {
7172
7965
  };
7173
7966
  var dashboard = async (config) => {
7174
7967
  if (process.stdout.isTTY !== true) {
7175
- say4(renderIndex());
7968
+ say5(renderIndex());
7176
7969
  return;
7177
7970
  }
7178
7971
  if (!hasNodeFfi()) {
7179
7972
  const major = Number(process.versions.node.split(".")[0]);
7180
7973
  if (Number.isNaN(major) || major < 26) {
7181
- return fail4(
7974
+ return fail5(
7182
7975
  `the dashboard needs Node >= 26 (node:ffi) \u2014 this is ${process.version}; every other command still works`
7183
7976
  );
7184
7977
  }
7185
- const rerun = spawnSync2(
7978
+ const rerun = spawnSync3(
7186
7979
  process.execPath,
7187
7980
  ["--experimental-ffi", "--disable-warning=ExperimentalWarning", ...process.argv.slice(1)],
7188
7981
  { stdio: "inherit" }
7189
7982
  );
7190
7983
  process.exit(rerun.status ?? 0);
7191
7984
  }
7192
- const { runDashboard } = await import("./dashboard-4WQC5MZ4.js");
7985
+ const { runDashboard } = await import("./dashboard-JJYC4SNU.js");
7193
7986
  const agentShare = await startShareIfBridge(config);
7194
7987
  try {
7195
7988
  await runDashboard({
@@ -7215,11 +8008,7 @@ var startShareIfBridge = async (config) => {
7215
8008
  }
7216
8009
  if (access.mode !== "bridge") return null;
7217
8010
  return startAgentShare({
7218
- url: bridgeUrlOf(
7219
- parseMendUrl(config.url).toString().replace(/\/$/, ""),
7220
- config.token,
7221
- os6.hostname()
7222
- ),
8011
+ url: () => socketUrl(config, "keys-bridge", { host: os7.hostname() }),
7223
8012
  agentSock
7224
8013
  });
7225
8014
  };
@@ -7233,38 +8022,38 @@ var withAgentShare = async (config, work) => {
7233
8022
  };
7234
8023
  var helpCommand = (words) => {
7235
8024
  if (words.length === 0) {
7236
- say4(renderIndex());
8025
+ say5(renderIndex());
7237
8026
  return;
7238
8027
  }
7239
8028
  const doc = findCommand(words);
7240
8029
  if (doc !== null && (doc.name.split(" ").length > 1 || words.length === 1)) {
7241
- say4(renderCommand(doc));
8030
+ say5(renderCommand(doc));
7242
8031
  return;
7243
8032
  }
7244
8033
  const group = renderGroup(words[0] ?? "");
7245
8034
  if (doc !== null) {
7246
- say4(renderCommand(doc));
8035
+ say5(renderCommand(doc));
7247
8036
  return;
7248
8037
  }
7249
8038
  if (group !== null) {
7250
- say4(group);
8039
+ say5(group);
7251
8040
  return;
7252
8041
  }
7253
- return fail4(`no command "${words.join(" ")}" \xB7 mend help lists them`);
8042
+ return fail5(`no command "${words.join(" ")}" \xB7 mend help lists them`);
7254
8043
  };
7255
8044
  var manCommand = (words) => {
7256
8045
  const doc = words.length === 0 ? null : findCommand(words);
7257
8046
  if (words.length > 0 && doc === null) {
7258
- return fail4(`no command "${words.join(" ")}" \xB7 mend help lists them`);
8047
+ return fail5(`no command "${words.join(" ")}" \xB7 mend help lists them`);
7259
8048
  }
7260
8049
  const version = cliVersion();
7261
8050
  const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
7262
- const file = path10.join(os6.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
7263
- fs9.writeFileSync(file, page);
7264
- const result = spawnSync2("man", ["-l", file], { stdio: "inherit" });
7265
- 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 });
7266
8055
  if (result.error !== void 0 || result.status !== 0) {
7267
- say4(doc === null ? renderIndex() : renderCommand(doc));
8056
+ say5(doc === null ? renderIndex() : renderCommand(doc));
7268
8057
  }
7269
8058
  };
7270
8059
  var main = async () => {
@@ -7275,7 +8064,7 @@ var main = async () => {
7275
8064
  }
7276
8065
  if (command === "server") {
7277
8066
  const result = await serverCommand(rest);
7278
- if (result._tag === "error") return fail4(result.message);
8067
+ if (result._tag === "error") return fail5(result.message);
7279
8068
  return;
7280
8069
  }
7281
8070
  const config = loadConfig();
@@ -7313,11 +8102,30 @@ var main = async () => {
7313
8102
  return connectCommand(config, rest);
7314
8103
  case "pair":
7315
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`);
7316
8121
  // Hidden in the catalog: the installer renders its own QR through this.
7317
8122
  case "qr":
7318
8123
  return qrCommand(rest);
7319
8124
  case "doctor":
7320
- return doctorCommand(config, localCredential);
8125
+ return doctorCommand(config, localCredential, () => {
8126
+ const read = readGrant(claudeGrantDir(mendCliHome()));
8127
+ return read.kind === "missing" ? null : read.secret;
8128
+ });
7321
8129
  case "env":
7322
8130
  return envCommand(config, rest);
7323
8131
  case "ssh":
@@ -7354,10 +8162,10 @@ var main = async () => {
7354
8162
  case "--version":
7355
8163
  case "-v":
7356
8164
  for (const line of versionLines(cliVersion(), await fetchServerVersion(config.url)))
7357
- say4(line);
8165
+ say5(line);
7358
8166
  return;
7359
8167
  default:
7360
- return fail4(`unknown command "${command}" \xB7 mend help lists them`);
8168
+ return fail5(`unknown command "${command}" \xB7 mend help lists them`);
7361
8169
  }
7362
8170
  };
7363
8171
  await main();