@sealant/mend 0.35.1 → 0.36.0-next.602

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 (89) hide show
  1. package/dist/{chunk-SMH65F6Z.js → chunk-5MPE4D2O.js} +559 -4
  2. package/dist/{dashboard-MGR4YUFD.js → dashboard-AVABQC2T.js} +225 -98
  3. package/dist/main.js +2495 -490
  4. package/man/mend-accounts.1 +1 -1
  5. package/man/mend-adopt.1 +1 -1
  6. package/man/mend-attach.1 +1 -1
  7. package/man/mend-codex.1 +7 -7
  8. package/man/mend-completions.1 +1 -1
  9. package/man/mend-connect-pi.1 +28 -0
  10. package/man/mend-connect.1 +4 -2
  11. package/man/mend-continue.1 +1 -1
  12. package/man/mend-doctor.1 +1 -1
  13. package/man/mend-dotfiles-repo.1 +1 -1
  14. package/man/mend-dotfiles-sync.1 +1 -1
  15. package/man/mend-dotfiles.1 +1 -1
  16. package/man/mend-env-cluster.1 +1 -1
  17. package/man/mend-env-load.1 +1 -1
  18. package/man/mend-env-show.1 +1 -1
  19. package/man/mend-folder-create.1 +1 -1
  20. package/man/mend-folder-list.1 +1 -1
  21. package/man/mend-folder-push.1 +1 -1
  22. package/man/mend-folder-rm.1 +1 -1
  23. package/man/mend-git-author.1 +1 -1
  24. package/man/mend-help.1 +1 -1
  25. package/man/mend-invite.1 +1 -1
  26. package/man/mend-keys-init.1 +1 -1
  27. package/man/mend-keys-mode.1 +1 -1
  28. package/man/mend-keys-share.1 +1 -1
  29. package/man/mend-keys-show.1 +1 -1
  30. package/man/mend-land.1 +1 -1
  31. package/man/mend-login.1 +1 -1
  32. package/man/mend-logout.1 +1 -1
  33. package/man/mend-man.1 +1 -1
  34. package/man/mend-members.1 +1 -1
  35. package/man/mend-memory-import.1 +23 -0
  36. package/man/mend-memory-rm.1 +15 -0
  37. package/man/mend-memory-show.1 +15 -0
  38. package/man/mend-memory.1 +32 -0
  39. package/man/mend-models.1 +22 -0
  40. package/man/mend-operator-exposure.1 +1 -1
  41. package/man/mend-operator-gate.1 +1 -1
  42. package/man/mend-operator-grant-owner.1 +1 -1
  43. package/man/mend-operator-org-create.1 +1 -1
  44. package/man/mend-operator-org-invite-owner.1 +1 -1
  45. package/man/mend-operator-org-list.1 +1 -1
  46. package/man/mend-operator-org-rename.1 +1 -1
  47. package/man/mend-operator-reset-link.1 +1 -1
  48. package/man/mend-pair.1 +1 -1
  49. package/man/mend-projects.1 +1 -1
  50. package/man/mend-pull.1 +1 -1
  51. package/man/mend-refresh.1 +1 -1
  52. package/man/mend-rejoin.1 +1 -1
  53. package/man/mend-resume.1 +1 -1
  54. package/man/mend-run.1 +1 -1
  55. package/man/mend-secrets-add.1 +24 -0
  56. package/man/mend-secrets-rm.1 +14 -0
  57. package/man/mend-secrets.1 +18 -0
  58. package/man/mend-server-logs.1 +1 -1
  59. package/man/mend-server-restart.1 +1 -1
  60. package/man/mend-server-setup.1 +27 -3
  61. package/man/mend-server-start.1 +1 -1
  62. package/man/mend-server-status.1 +5 -3
  63. package/man/mend-server-stop.1 +3 -3
  64. package/man/mend-server-upgrade.1 +7 -2
  65. package/man/mend-server.1 +1 -1
  66. package/man/mend-service-add.1 +1 -1
  67. package/man/mend-service-connect.1 +1 -1
  68. package/man/mend-service-init.1 +1 -1
  69. package/man/mend-service-list.1 +1 -1
  70. package/man/mend-service-logs.1 +1 -1
  71. package/man/mend-service-restart.1 +1 -1
  72. package/man/mend-service-run.1 +1 -1
  73. package/man/mend-service-stop.1 +1 -1
  74. package/man/mend-session-share.1 +2 -2
  75. package/man/mend-sessions.1 +1 -1
  76. package/man/mend-shell.1 +1 -1
  77. package/man/mend-skills-push.1 +1 -1
  78. package/man/mend-skills.1 +1 -1
  79. package/man/mend-snake.1 +1 -1
  80. package/man/mend-ssh-setup.1 +1 -1
  81. package/man/mend-ssh.1 +1 -1
  82. package/man/mend-stop.1 +1 -1
  83. package/man/mend-ui.1 +1 -1
  84. package/man/mend-uninstall.1 +1 -1
  85. package/man/mend-version.1 +1 -1
  86. package/man/mend-worktrees-rm.1 +29 -0
  87. package/man/mend-worktrees.1 +2 -2
  88. package/man/mend.1 +35 -5
  89. package/package.json +19 -38
package/dist/main.js CHANGED
@@ -1,21 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AGENT_MEMORY_FILES,
4
+ AGENT_MEMORY_MAX_DATABASE_BYTES,
5
+ AGENT_MEMORY_MAX_FILE_BYTES,
6
+ AGENT_MEMORY_ROOTS,
7
+ CODEX_MEMORY_DATABASE,
3
8
  GATEWAY_STATUSES,
4
9
  HARNESS_COMMANDS,
10
+ HOST_USER_NAMESPACE_FILES,
5
11
  LIVE_STATUSES,
6
12
  MendRequestError,
13
+ PI_PROFILE_AGENT_PATH,
14
+ PI_PROFILE_LOCAL_PACKAGES,
15
+ PI_PROFILE_MAX_FILE_BYTES,
16
+ WORKTREE_REMOVAL_FORCE_HINT,
7
17
  agentIsLive,
18
+ agentMemoryNameOf,
8
19
  agentOutcome,
9
20
  agentStartingFacts,
10
21
  agentStartingLine,
11
22
  captureLineOf,
12
23
  claudeGrantFacts,
13
24
  createSseParser,
25
+ csiUKeysOf,
14
26
  cwdFacts,
27
+ firstPositional,
15
28
  gitAuthorIssue,
16
29
  gitCurrentBranch,
17
30
  gitOriginUrl,
18
31
  gitTopLevel,
32
+ hostUserNamespacesFix,
33
+ hostUserNamespacesOf,
19
34
  isDetachChunk,
20
35
  isPasteChunk,
21
36
  landingFactLine,
@@ -33,16 +48,275 @@ import {
33
48
  servicesHoldOf,
34
49
  sessionDisplayName,
35
50
  spoken,
36
- trackBracketedPaste
37
- } from "./chunk-SMH65F6Z.js";
51
+ terminalReadOnlyLine,
52
+ trackBracketedPaste,
53
+ validatePiProfile,
54
+ validateSecretFilePath
55
+ } from "./chunk-5MPE4D2O.js";
38
56
 
39
57
  // src/main.ts
40
58
  import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
41
- import * as fs17 from "node:fs";
59
+ import * as fs20 from "node:fs";
42
60
  import { createRequire } from "node:module";
43
- import * as net4 from "node:net";
44
- import * as os11 from "node:os";
45
- import * as path18 from "node:path";
61
+ import * as net5 from "node:net";
62
+ import * as os13 from "node:os";
63
+ import * as path21 from "node:path";
64
+
65
+ // src/agent-memory.ts
66
+ import { randomUUID } from "node:crypto";
67
+ import * as fs from "node:fs";
68
+ import * as os from "node:os";
69
+ import * as path from "node:path";
70
+ import { DatabaseSync } from "node:sqlite";
71
+ import * as zlib from "node:zlib";
72
+ var CLAUDE_ROOT = AGENT_MEMORY_ROOTS.find((root) => root.harness === "claude")?.root ?? "";
73
+ var claudeHome = () => process.env["CLAUDE_CONFIG_DIR"] ?? path.join(os.homedir(), ".claude");
74
+ var claudeMemoryDirFor = (repoRoot, home = claudeHome()) => {
75
+ const keys = [repoRoot.replace(/[^a-zA-Z0-9]/g, "-"), repoRoot.replace(/[/.]/g, "-")];
76
+ for (const key of new Set(keys)) {
77
+ const dir = path.join(home, "projects", key, "memory");
78
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) return dir;
79
+ }
80
+ return null;
81
+ };
82
+ var scanClaudeMemory = (dir) => {
83
+ const files = [];
84
+ const notes = [];
85
+ const walk2 = (abs, rel) => {
86
+ for (const entry of fs.readdirSync(abs, { withFileTypes: true }).toSorted((a, b) => a.name < b.name ? -1 : 1)) {
87
+ const at = rel === "" ? entry.name : `${rel}/${entry.name}`;
88
+ const full = path.join(abs, entry.name);
89
+ if (entry.isDirectory()) {
90
+ walk2(full, at);
91
+ continue;
92
+ }
93
+ if (!entry.isFile()) continue;
94
+ const bytes = fs.readFileSync(full);
95
+ if (bytes.byteLength > AGENT_MEMORY_MAX_FILE_BYTES) {
96
+ notes.push(`${at}: over ${AGENT_MEMORY_MAX_FILE_BYTES / 1024} KB, left out`);
97
+ continue;
98
+ }
99
+ const text = bytes.includes(0) ? null : bytes.toString("utf8");
100
+ files.push(
101
+ text !== null && Buffer.from(text, "utf8").equals(bytes) ? { path: `${CLAUDE_ROOT}/${at}`, encoding: "utf8", contents: text } : {
102
+ path: `${CLAUDE_ROOT}/${at}`,
103
+ encoding: "base64",
104
+ contents: bytes.toString("base64")
105
+ }
106
+ );
107
+ }
108
+ };
109
+ walk2(dir, "");
110
+ return { dir, files, notes };
111
+ };
112
+ var machineIdFor = (configDir, create) => {
113
+ const file = path.join(configDir, "machine-id");
114
+ try {
115
+ const existing = fs.readFileSync(file, "utf8").trim();
116
+ if (existing !== "") return existing;
117
+ } catch {
118
+ }
119
+ if (!create) return null;
120
+ const id = randomUUID();
121
+ fs.mkdirSync(configDir, { recursive: true });
122
+ fs.writeFileSync(file, `${id}
123
+ `, { mode: 384 });
124
+ return id;
125
+ };
126
+ var importSourceFor = (configDir, repoRoot, options) => {
127
+ const machine = machineIdFor(configDir, options.create);
128
+ if (machine === null) return null;
129
+ return {
130
+ id: `${machine}:${repoRoot}`.slice(0, 1024),
131
+ label: (options.hostname ?? os.hostname()).replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 64) || "this machine"
132
+ };
133
+ };
134
+ var memoryDisplayName = (filePath) => {
135
+ const named = agentMemoryNameOf(filePath);
136
+ if (named === null) return filePath;
137
+ return named.harness === "claude" ? named.name : `${named.harness}:${named.name}`;
138
+ };
139
+ var lost = (n, whose) => `${n} of ${whose} line${n === 1 ? " is" : "s are"} not in the result: its copy kept as a version`;
140
+ var ADDED_NAMED = 20;
141
+ var importReportLines = (report) => {
142
+ const lines = [];
143
+ const row = (word, filePath, why) => lines.push(
144
+ ` ${word.padEnd(9)} ${memoryDisplayName(filePath)}${why === void 0 ? "" : ` \xB7 ${why}`}`
145
+ );
146
+ report.added.slice(0, ADDED_NAMED).forEach((p) => row("added", p));
147
+ if (report.added.length > ADDED_NAMED) {
148
+ lines.push(` ${"added".padEnd(9)} and ${report.added.length - ADDED_NAMED} more`);
149
+ }
150
+ for (const p of report.updated ?? []) {
151
+ row("updated", p, "unchanged in Mend since this machine's last import");
152
+ }
153
+ for (const merge of report.merged ?? []) {
154
+ const missing = merge.missingLines ?? 0;
155
+ const storeMissing = merge.storeMissingLines ?? 0;
156
+ row(
157
+ "merged",
158
+ merge.path,
159
+ missing > 0 ? lost(missing, "this machine's") : storeMissing > 0 ? lost(storeMissing, "Mend's") : merge.against === "last-import" ? "both changed since this machine's last import, both sides' lines kept" : merge.against === "summaries" ? "both sides' summaries kept, the newer one per conversation" : "both sides' lines kept, no earlier import to compare against"
160
+ );
161
+ }
162
+ for (const p of report.keptStored ?? []) {
163
+ row("kept", p, "changed in Mend since this machine's last import, not here");
164
+ }
165
+ for (const p of report.removedInMend ?? []) {
166
+ row("not added", p, "removed in Mend since this machine's last import");
167
+ }
168
+ for (const p of report.conflicting) {
169
+ row(
170
+ "conflict",
171
+ p,
172
+ "both changed, not mergeable: Mend's kept, this machine's kept as a version"
173
+ );
174
+ }
175
+ for (const p of report.skipped ?? []) row("skipped", p, "outside the memory limits");
176
+ return lines;
177
+ };
178
+ var importReportCounts = (report) => [
179
+ [report.added.length, "added"],
180
+ [report.unchanged.length, "unchanged"],
181
+ [report.updated?.length ?? 0, "updated"],
182
+ [report.merged?.length ?? 0, "merged"],
183
+ [report.keptStored?.length ?? 0, "kept as Mend has it"],
184
+ [report.removedInMend?.length ?? 0, "not added again"],
185
+ [report.conflicting.length, report.conflicting.length === 1 ? "conflict" : "conflicts"],
186
+ [report.skipped?.length ?? 0, "skipped"]
187
+ ].filter(([count]) => count !== 0).map(([count, word]) => `${count} ${word}`).join(" \xB7 ") || "nothing to import";
188
+ var CODEX_THREADS_ROOT = AGENT_MEMORY_ROOTS.find((root) => root.root.endsWith("codex-threads"))?.root ?? ".mend/codex-threads";
189
+ var firstLineOf = async (file) => {
190
+ const plain = fs.existsSync(file) ? file : null;
191
+ const compressed = plain === null && fs.existsSync(`${file}.zst`) ? `${file}.zst` : null;
192
+ if (plain === null && compressed === null) return null;
193
+ const source = fs.createReadStream(plain ?? compressed ?? file);
194
+ const stream = compressed === null ? source : source.pipe(zlib.createZstdDecompress());
195
+ let text = "";
196
+ try {
197
+ for await (const chunk of stream) {
198
+ text += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
199
+ const end = text.indexOf("\n");
200
+ if (end !== -1) return text.slice(0, end);
201
+ if (text.length > 1024 * 1024) return null;
202
+ }
203
+ return null;
204
+ } catch {
205
+ return null;
206
+ } finally {
207
+ source.destroy();
208
+ }
209
+ };
210
+ var codexHome = () => process.env["CODEX_HOME"] ?? path.join(os.homedir(), ".codex");
211
+ var codexStateDatabase = (home) => {
212
+ const names = fs.existsSync(home) ? fs.readdirSync(home).filter((name) => /^state_\d+\.sqlite$/.test(name)) : [];
213
+ const newest = names.toSorted(
214
+ (a, b) => Number(/\d+/.exec(b)?.[0] ?? 0) - Number(/\d+/.exec(a)?.[0] ?? 0)
215
+ )[0];
216
+ return newest === void 0 ? null : path.join(home, newest);
217
+ };
218
+ var scanCodexMemory = async (repoRoot, home = codexHome()) => {
219
+ const memories = path.join(home, "memories_1.sqlite");
220
+ const state = codexStateDatabase(home);
221
+ if (!fs.existsSync(memories) || state === null) {
222
+ return { home, files: [], summaries: 0, notes: [] };
223
+ }
224
+ const threads = new DatabaseSync(state, { readOnly: true });
225
+ const inRepo = (() => {
226
+ try {
227
+ return new Map(
228
+ threads.prepare(
229
+ threads.prepare("select name from pragma_table_info('threads') where name = 'memory_mode'").get() === void 0 ? "select id, cwd, rollout_path from threads" : "select id, cwd, rollout_path from threads where memory_mode = 'enabled'"
230
+ ).all().flatMap((row) => {
231
+ const cwd = row["cwd"];
232
+ const id = row["id"];
233
+ const rollout = row["rollout_path"];
234
+ return typeof id === "string" && typeof cwd === "string" && (cwd === repoRoot || cwd.startsWith(`${repoRoot}/`)) ? [[id, typeof rollout === "string" ? rollout : null]] : [];
235
+ })
236
+ );
237
+ } finally {
238
+ threads.close();
239
+ }
240
+ })();
241
+ const ids = [...inRepo.keys()];
242
+ if (ids.length === 0) return { home, files: [], summaries: 0, notes: [] };
243
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mend-codex-import-"));
244
+ try {
245
+ const out = path.join(dir, "memories_1.sqlite");
246
+ const target = new DatabaseSync(out);
247
+ let summaries = 0;
248
+ try {
249
+ target.exec(`attach database '${memories.replaceAll("'", "''")}' as source`);
250
+ const schema = target.prepare(
251
+ "select type, name, sql from source.sqlite_master where sql is not null and name not like 'sqlite_%' order by type = 'index', name"
252
+ ).all();
253
+ for (const row of schema) {
254
+ if (typeof row["sql"] === "string") target.exec(row["sql"]);
255
+ }
256
+ target.exec("insert into main._sqlx_migrations select * from source._sqlx_migrations");
257
+ try {
258
+ target.exec(
259
+ "insert into main.consolidation_progress select * from source.consolidation_progress"
260
+ );
261
+ } catch {
262
+ }
263
+ const insert = target.prepare(
264
+ "insert into main.stage1_outputs select * from source.stage1_outputs where thread_id = ?"
265
+ );
266
+ for (const id of ids) summaries += Number(insert.run(id).changes);
267
+ target.exec(
268
+ "update main.stage1_outputs set selected_for_phase2 = 0, selected_for_phase2_source_updated_at = null"
269
+ );
270
+ target.exec("detach database source");
271
+ } finally {
272
+ target.close();
273
+ }
274
+ if (summaries === 0) return { home, files: [], summaries: 0, notes: [] };
275
+ const summarisedIds = (() => {
276
+ const copy = new DatabaseSync(out, { readOnly: true });
277
+ try {
278
+ return copy.prepare("select thread_id from stage1_outputs").all().flatMap((row) => typeof row["thread_id"] === "string" ? [row["thread_id"]] : []);
279
+ } finally {
280
+ copy.close();
281
+ }
282
+ })();
283
+ const threadLines = [];
284
+ for (const id of summarisedIds) {
285
+ const rollout = inRepo.get(id) ?? null;
286
+ const head = rollout === null ? null : await firstLineOf(rollout);
287
+ if (head !== null) {
288
+ threadLines.push({
289
+ path: `${CODEX_THREADS_ROOT}/${id}.jsonl`,
290
+ encoding: "utf8",
291
+ contents: `${head}
292
+ `
293
+ });
294
+ }
295
+ }
296
+ const bytes = fs.readFileSync(out);
297
+ if (bytes.byteLength > AGENT_MEMORY_MAX_DATABASE_BYTES) {
298
+ return {
299
+ home,
300
+ files: [],
301
+ summaries,
302
+ notes: [
303
+ `codex: ${summaries} summaries, over ${AGENT_MEMORY_MAX_DATABASE_BYTES / 1024 / 1024} MB, left out`
304
+ ]
305
+ };
306
+ }
307
+ return {
308
+ home,
309
+ files: [
310
+ { path: CODEX_MEMORY_DATABASE, encoding: "base64", contents: bytes.toString("base64") },
311
+ ...threadLines
312
+ ],
313
+ summaries,
314
+ notes: []
315
+ };
316
+ } finally {
317
+ fs.rmSync(dir, { recursive: true, force: true });
318
+ }
319
+ };
46
320
 
47
321
  // src/agent-share.ts
48
322
  import * as net from "node:net";
@@ -50,7 +324,7 @@ import { setTimeout as delay } from "node:timers/promises";
50
324
  var retryDelayMs = (attempt2) => Math.min(3e4, 1e3 * 2 ** Math.min(Math.max(attempt2 - 1, 0), 5));
51
325
  var SSH_AGENTC_REQUEST_IDENTITIES = 11;
52
326
  var SSH_AGENTC_SIGN_REQUEST = 13;
53
- var askLocalAgent = (agentSock, payload) => new Promise((resolve7, reject) => {
327
+ var askLocalAgent = (agentSock, payload) => new Promise((resolve8, reject) => {
54
328
  const connection = net.connect(agentSock, () => {
55
329
  connection.write(payload);
56
330
  });
@@ -64,7 +338,7 @@ var askLocalAgent = (agentSock, payload) => new Promise((resolve7, reject) => {
64
338
  if (pending.length >= 4 && pending.length >= 4 + pending.readUInt32BE(0)) {
65
339
  clearTimeout(timer);
66
340
  connection.end();
67
- resolve7(pending.subarray(0, 4 + pending.readUInt32BE(0)));
341
+ resolve8(pending.subarray(0, 4 + pending.readUInt32BE(0)));
68
342
  }
69
343
  });
70
344
  connection.on("error", (error) => {
@@ -94,12 +368,12 @@ var shareAgent = async (options) => {
94
368
  const ws = new WebSocket(target);
95
369
  const onAbort = () => ws.close();
96
370
  signal.addEventListener("abort", onAbort, { once: true });
97
- const closed = new Promise((resolve7) => {
98
- ws.addEventListener("close", () => resolve7(), { once: true });
371
+ const closed = new Promise((resolve8) => {
372
+ ws.addEventListener("close", () => resolve8(), { once: true });
99
373
  });
100
- const opened = await new Promise((resolve7) => {
101
- ws.addEventListener("open", () => resolve7(true), { once: true });
102
- ws.addEventListener("error", () => resolve7(false), { once: true });
374
+ const opened = await new Promise((resolve8) => {
375
+ ws.addEventListener("open", () => resolve8(true), { once: true });
376
+ ws.addEventListener("error", () => resolve8(false), { once: true });
103
377
  });
104
378
  if (opened) {
105
379
  attempt2 = 0;
@@ -216,25 +490,34 @@ var firstOutputGate = (options) => {
216
490
  };
217
491
  };
218
492
 
493
+ // src/attach-watch.ts
494
+ var watchesTerminal = (detail) => detail?.control?.terminalInput === false;
495
+ var ownerNameOf = (ownerUserId, members) => members.find((member) => member.userId === ownerUserId)?.name ?? "its owner";
496
+ var watchNotice = (ownerName) => terminalReadOnlyLine(ownerName);
497
+ var isInterruptChunk = (data) => data.includes(3) || csiUKeysOf(data).some(
498
+ (key) => key.code === 99 && (key.mods & ~1) === 4 && (key.event === 1 || key.event === 2)
499
+ );
500
+ var watchKey = (data, detachKeyEnabled2) => detachKeyEnabled2 && isDetachChunk(data) || isInterruptChunk(data) ? "detach" : null;
501
+
219
502
  // src/claude-grant.ts
220
503
  import { spawnSync } from "node:child_process";
221
504
  import { createHash } from "node:crypto";
222
- import * as fs from "node:fs";
223
- import * as os from "node:os";
224
- import * as path from "node:path";
225
- var claudeGrantDir = (home) => path.join(home, "claude-grant");
505
+ import * as fs2 from "node:fs";
506
+ import * as os2 from "node:os";
507
+ import * as path2 from "node:path";
508
+ var claudeGrantDir = (home) => path2.join(home, "claude-grant");
226
509
  var forgetGrant = (configDir, platform = process.platform) => {
227
- fs.rmSync(configDir, { recursive: true, force: true });
510
+ fs2.rmSync(configDir, { recursive: true, force: true });
228
511
  if (platform !== "darwin") return;
229
512
  spawnSync(
230
513
  "security",
231
- ["delete-generic-password", "-a", os.userInfo().username, "-s", keychainService(configDir)],
514
+ ["delete-generic-password", "-a", os2.userInfo().username, "-s", keychainService(configDir)],
232
515
  { stdio: "ignore", timeout: 6e4 }
233
516
  );
234
517
  };
235
518
  var personalClaudeDir = (environment = process.env) => {
236
519
  const configured = environment["CLAUDE_CONFIG_DIR"];
237
- return configured === void 0 || configured === "" ? path.join(os.homedir(), ".claude") : configured;
520
+ return configured === void 0 || configured === "" ? path2.join(os2.homedir(), ".claude") : configured;
238
521
  };
239
522
  var keychainService = (configDir) => {
240
523
  const suffix = createHash("sha256").update(configDir).digest("hex").slice(0, 8);
@@ -256,10 +539,10 @@ var grantStatus = (cli, configDir) => {
256
539
  try {
257
540
  const parsed = JSON.parse(result.stdout.slice(start));
258
541
  if (typeof parsed !== "object" || parsed === null) return null;
259
- const record = { ...parsed };
260
- const text = (key) => typeof record[key] === "string" ? record[key] : null;
542
+ const record2 = { ...parsed };
543
+ const text = (key) => typeof record2[key] === "string" ? record2[key] : null;
261
544
  return {
262
- loggedIn: record["loggedIn"] === true,
545
+ loggedIn: record2["loggedIn"] === true,
263
546
  authMethod: text("authMethod"),
264
547
  configDirectory: text("configDirectory"),
265
548
  email: text("email"),
@@ -270,7 +553,7 @@ var grantStatus = (cli, configDir) => {
270
553
  }
271
554
  };
272
555
  var runClaudeLogin = (cli, configDir) => {
273
- fs.mkdirSync(configDir, { recursive: true, mode: 448 });
556
+ fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
274
557
  const result = spawnSync(cli.bin, ["auth", "login", "--claudeai"], {
275
558
  stdio: "inherit",
276
559
  env: { ...cli.env, CLAUDE_CONFIG_DIR: configDir }
@@ -278,16 +561,16 @@ var runClaudeLogin = (cli, configDir) => {
278
561
  return result.error === void 0 && result.status === 0;
279
562
  };
280
563
  var readGrant = (configDir, platform = process.platform) => {
281
- const file = path.join(configDir, ".credentials.json");
282
- if (fs.existsSync(file)) {
283
- const secret2 = fs.readFileSync(file, "utf8").trim();
564
+ const file = path2.join(configDir, ".credentials.json");
565
+ if (fs2.existsSync(file)) {
566
+ const secret2 = fs2.readFileSync(file, "utf8").trim();
284
567
  if (secret2 !== "") return { kind: "file", secret: secret2, path: file };
285
568
  }
286
569
  if (platform !== "darwin") return { kind: "missing", triedPath: file, triedService: null };
287
570
  const service = keychainService(configDir);
288
571
  const result = spawnSync(
289
572
  "security",
290
- ["find-generic-password", "-a", os.userInfo().username, "-w", "-s", service],
573
+ ["find-generic-password", "-a", os2.userInfo().username, "-w", "-s", service],
291
574
  { encoding: "utf8", timeout: 6e4 }
292
575
  );
293
576
  const secret = typeof result.stdout === "string" ? result.stdout.trim() : "";
@@ -308,13 +591,13 @@ var parsePngfHex = (output) => {
308
591
  return Buffer.from(match[1], "hex");
309
592
  };
310
593
  var MAX_BYTES = 32 * 1024 * 1024;
311
- var run = (command, args) => new Promise((resolve7) => {
594
+ var run = (command, args) => new Promise((resolve8) => {
312
595
  execFile(
313
596
  command,
314
597
  [...args],
315
598
  { encoding: "buffer", maxBuffer: MAX_BYTES, timeout: 3e3 },
316
599
  (error, stdout) => {
317
- resolve7(error === null ? stdout : null);
600
+ resolve8(error === null ? stdout : null);
318
601
  }
319
602
  );
320
603
  });
@@ -353,17 +636,17 @@ var readClipboardImage = async (platform = process.platform, env = process.env)
353
636
 
354
637
  // src/codex-grant.ts
355
638
  import { spawnSync as spawnSync2 } from "node:child_process";
356
- import * as fs3 from "node:fs";
357
- import * as os2 from "node:os";
358
- import * as path3 from "node:path";
639
+ import * as fs4 from "node:fs";
640
+ import * as os3 from "node:os";
641
+ import * as path4 from "node:path";
359
642
 
360
643
  // src/login-dir.ts
361
- import * as fs2 from "node:fs";
362
- import * as path2 from "node:path";
644
+ import * as fs3 from "node:fs";
645
+ import * as path3 from "node:path";
363
646
  var throwawayLoginDir = (parent, prefix) => {
364
- fs2.mkdirSync(parent, { recursive: true, mode: 448 });
365
- const dir = fs2.mkdtempSync(path2.join(parent, prefix));
366
- fs2.chmodSync(dir, 448);
647
+ fs3.mkdirSync(parent, { recursive: true, mode: 448 });
648
+ const dir = fs3.mkdtempSync(path3.join(parent, prefix));
649
+ fs3.chmodSync(dir, 448);
367
650
  return dir;
368
651
  };
369
652
 
@@ -374,12 +657,12 @@ var codexCli = (environment = process.env) => ({
374
657
  });
375
658
  var personalCodexHome = (environment = process.env) => {
376
659
  const configured = environment["CODEX_HOME"];
377
- return configured === void 0 || configured === "" ? path3.join(os2.homedir(), ".codex") : configured;
660
+ return configured === void 0 || configured === "" ? path4.join(os3.homedir(), ".codex") : configured;
378
661
  };
379
- var runCodexDeviceLogin = (cli, codexHome) => {
662
+ var runCodexDeviceLogin = (cli, codexHome2) => {
380
663
  const result = spawnSync2(cli.bin, ["login", "--device-auth"], {
381
664
  stdio: "inherit",
382
- env: { ...cli.env, CODEX_HOME: codexHome }
665
+ env: { ...cli.env, CODEX_HOME: codexHome2 }
383
666
  });
384
667
  return result.error === void 0 && result.status === 0;
385
668
  };
@@ -407,11 +690,11 @@ var codexGrant = (input) => {
407
690
  if (!runCodexDeviceLogin(input.cli, home)) {
408
691
  return { kind: "failed", reason: "codex: the login did not complete" };
409
692
  }
410
- const file = path3.join(home, "auth.json");
411
- if (!fs3.existsSync(file)) {
693
+ const file = path4.join(home, "auth.json");
694
+ if (!fs4.existsSync(file)) {
412
695
  return { kind: "failed", reason: `codex: logged in, but no auth.json appeared in ${home}` };
413
696
  }
414
- const secret = fs3.readFileSync(file, "utf8").trim();
697
+ const secret = fs4.readFileSync(file, "utf8").trim();
415
698
  if (refreshTokenOf(secret) === null) {
416
699
  return {
417
700
  kind: "failed",
@@ -426,19 +709,259 @@ var codexGrant = (input) => {
426
709
  }
427
710
  return { kind: "grant", secret };
428
711
  } finally {
429
- fs3.rmSync(home, { recursive: true, force: true });
712
+ fs4.rmSync(home, { recursive: true, force: true });
430
713
  }
431
714
  };
432
715
 
433
716
  // src/doctor-bundle-collectors.ts
434
- import * as fs5 from "node:fs";
435
- import * as os3 from "node:os";
436
- import * as path5 from "node:path";
717
+ import * as fs6 from "node:fs";
718
+ import * as os4 from "node:os";
719
+ import * as path6 from "node:path";
720
+
721
+ // src/server-edge.ts
722
+ import * as net2 from "node:net";
723
+
724
+ // src/server-edge-files.ts
725
+ var EDGE_COMPOSE_OVERLAY = [
726
+ "# Opt-in overlay: a TLS edge in front of a packaged Mend install",
727
+ '# (docs/adr/0004-access-without-a-private-network.md, "The access model").',
728
+ "#",
729
+ "# `mend server setup --edge <host>` writes this file and the Caddyfile into the install's",
730
+ "# generation beside compose.yaml, with MEND_EDGE_HOST in server.env, and every start, restart and",
731
+ "# upgrade of that install runs the two files together. The CLI carries a copy of both",
732
+ "# (apps/cli/src/server-edge.ts); its tests hold the copy to this file.",
733
+ "#",
734
+ "# For a Compose install you run yourself, from compose.v2.yaml and your own .env:",
735
+ "#",
736
+ "# docker compose -f compose.v2.yaml -f compose.edge.yaml up -d",
737
+ "#",
738
+ "# In .env, beside what compose.v2.yaml needs:",
739
+ "# MEND_EDGE_HOST=mend.example.com the name the certificate is for; its DNS points here",
740
+ "# APP_URL=https://mend.example.com exactly https://<MEND_EDGE_HOST>",
741
+ "# MEND_EXPOSURE=public or private, when admission to this host is yours to control",
742
+ "#",
743
+ "# Checked: the merged files render, and the Caddyfile validates. Not checked: a certificate issued",
744
+ "# and a browser session through it, end to end.",
745
+ "#",
746
+ "# The edge publishes 80 and 443. Mend's own port stays on loopback (MEND_BIND_HOST), so the only",
747
+ "# way in from outside is through TLS. Ports 80/443 on a public address mean a public certificate,",
748
+ "# and the hostname appears in certificate transparency logs.",
749
+ "#",
750
+ "# This does not make an instance fit to expose. `mend operator exposure` reports what Mend observes",
751
+ "# and what is still open; the last items are yours to verify.",
752
+ "services:",
753
+ " edge:",
754
+ " image: caddy:2.10-alpine",
755
+ " restart: unless-stopped",
756
+ " depends_on:",
757
+ " mend:",
758
+ " condition: service_started",
759
+ " ports:",
760
+ ' - "${MEND_EDGE_BIND_HOST:-0.0.0.0}:80:80"',
761
+ ' - "${MEND_EDGE_BIND_HOST:-0.0.0.0}:443:443"',
762
+ ' - "${MEND_EDGE_BIND_HOST:-0.0.0.0}:443:443/udp"',
763
+ " environment:",
764
+ " MEND_EDGE_HOST:",
765
+ ' "${MEND_EDGE_HOST:?set MEND_EDGE_HOST in .env, the name the certificate is for}"',
766
+ " volumes:",
767
+ " - ./Caddyfile:/etc/caddy/Caddyfile:ro",
768
+ " - mend-edge-data:/data",
769
+ " - mend-edge-config:/config",
770
+ " # The edge talks to Mend and to nothing else: it is not on the network that holds Postgres,",
771
+ " # the bucket and the workspaces.",
772
+ " networks: [edge]",
773
+ " cap_drop: [ALL]",
774
+ " cap_add: [NET_BIND_SERVICE]",
775
+ ' security_opt: ["no-new-privileges:true"]',
776
+ "",
777
+ " mend:",
778
+ " networks: [default, edge]",
779
+ " environment:",
780
+ " # The one hop whose X-Forwarded-For entries Mend believes: the edge's network, fixed below.",
781
+ " # Both follow MEND_EDGE_SUBNET, so moving the subnet cannot leave the old range trusted.",
782
+ " MEND_TRUSTED_PROXIES: ${MEND_TRUSTED_PROXIES:-${MEND_EDGE_SUBNET:-192.168.250.0/28}}",
783
+ " MEND_EXPOSURE: ${MEND_EXPOSURE:-private}",
784
+ " # Executors reach the session channel over plain http on this Compose network, which never",
785
+ " # leaves the host. That is this overlay's statement, reported by Mend as declared.",
786
+ " MEND_EXECUTOR_NETWORK: ${MEND_EXECUTOR_NETWORK:-private}",
787
+ " MEND_URL_BEARERS: ${MEND_URL_BEARERS:-accept}",
788
+ " MEND_EXPOSURE_REASSESSED: ${MEND_EXPOSURE_REASSESSED:-}",
789
+ " MEND_EXPOSURE_DECLARED: ${MEND_EXPOSURE_DECLARED:-}",
790
+ "",
791
+ "networks:",
792
+ " edge:",
793
+ " ipam:",
794
+ " config:",
795
+ " # Sixteen addresses for one edge and one Mend. Set MEND_EDGE_SUBNET when this range is",
796
+ " # already routed where the host lives (no private range is free everywhere).",
797
+ " - subnet: ${MEND_EDGE_SUBNET:-192.168.250.0/28}",
798
+ "",
799
+ "volumes:",
800
+ " mend-edge-data:",
801
+ " mend-edge-config:"
802
+ ].join("\n") + "\n";
803
+ var EDGE_CADDYFILE = [
804
+ "# The edge for a packaged Mend install (docs/adr/0004-access-without-a-private-network.md).",
805
+ "# TLS ends here: Caddy obtains and renews a certificate for MEND_EDGE_HOST and redirects port 80.",
806
+ "# Everything goes to Mend's web tier, which proxies /api, the event stream and every WebSocket on",
807
+ "# this one origin. Nothing else is routed: not the API, not Sealant, not the session channel, not",
808
+ "# the bucket.",
809
+ "{$MEND_EDGE_HOST} {",
810
+ " encode zstd gzip",
811
+ "",
812
+ " # Mend's web tier deletes inbound Forwarded, X-Forwarded-Host and X-Forwarded-Proto and appends",
813
+ " # the address it saw to X-Forwarded-For. Caddy sets X-Forwarded-For to the client it accepted;",
814
+ " # it does not trust one a client sent, since no trusted_proxies are configured above it.",
815
+ " reverse_proxy mend:3105 {",
816
+ " # Terminals and the event stream are long-lived: never buffer, never time them out.",
817
+ " flush_interval -1",
818
+ " }",
819
+ "",
820
+ " log {",
821
+ " output stdout",
822
+ " # The terminal, tunnel and key bridge sockets and the phone's embed carry a single-use",
823
+ " # ticket in the query, and pairing carries a code. A log holds none of them. Caddy already",
824
+ " # redacts Authorization, Cookie and Set-Cookie.",
825
+ " format filter {",
826
+ " wrap json",
827
+ " fields {",
828
+ " request>uri query {",
829
+ " replace ticket REDACTED",
830
+ " replace token REDACTED",
831
+ " replace code REDACTED",
832
+ " }",
833
+ " request>headers>Referer delete",
834
+ " }",
835
+ " }",
836
+ " }",
837
+ "}"
838
+ ].join("\n") + "\n";
839
+
840
+ // src/server-edge.ts
841
+ var EXPOSURES = ["loopback", "private", "public"];
842
+ var TENANCIES = ["single", "multi"];
843
+ var isExposure = (value) => EXPOSURES.some((exposure) => exposure === value);
844
+ var isTenancy = (value) => TENANCIES.some((tenancy) => tenancy === value);
845
+ var EDGE_IMAGE = "caddy:2.10-alpine";
846
+ var EDGE_COMPOSE_FILE = "compose.edge.yaml";
847
+ var POSTURE_COMPOSE_FILE = "compose.posture.yaml";
848
+ var parseEdgeHost = (value) => {
849
+ const host = value.trim().toLowerCase().replace(/\.$/, "");
850
+ const labels = host.split(".");
851
+ const label = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
852
+ if (host.length === 0 || host.length > 253 || labels.length < 2 || !labels.every((part) => label.test(part)) || net2.isIP(host) !== 0) {
853
+ return null;
854
+ }
855
+ return host;
856
+ };
857
+ var postureEnvironment = (posture) => {
858
+ const gate = posture.tenancy === "multi" || posture.exposure === "public";
859
+ return [
860
+ ...posture.exposure === void 0 ? [] : [["MEND_EXPOSURE", posture.exposure]],
861
+ ...posture.tenancy === void 0 ? [] : [["MEND_TENANCY", posture.tenancy]],
862
+ ...gate ? [["MEND_SOURCE_POLICY", "tenant"], ["MEND_CAPTURE_REQUIRE_SIZES", "true"]] : [],
863
+ ...posture.exposure === "public" ? [["MEND_URL_BEARERS", "refuse"]] : []
864
+ ];
865
+ };
866
+ var postureEnvLines = (posture) => [
867
+ ...posture.edgeHost === void 0 ? [] : [`MEND_EDGE_HOST=${posture.edgeHost}`],
868
+ ...postureEnvironment(posture).map(([key, value]) => `${key}=${value}`)
869
+ ];
870
+ var renderPostureOverlay = (posture) => {
871
+ const entries = postureEnvironment(posture);
872
+ if (entries.length === 0) return void 0;
873
+ return [
874
+ "# Written by mend server setup: the posture this install declares, read from server.env",
875
+ "# (docs/adr/0003-organizations-and-tenancy.md, docs/adr/0004-access-without-a-private-network.md).",
876
+ "# MEND_EXPOSURE and MEND_TENANCY are the operator's statements. With multi tenancy or a public",
877
+ "# exposure, the multi mode gate's configuration items follow: Mend's own git keeps to the tenant",
878
+ "# source policy and every capture upload is signed for its size. MEND_GIT_TRANSPORT_BIND_ORIGIN",
879
+ "# and MEND_SERVICE_HOSTS are not set, which is what the gate asks of them.",
880
+ "services:",
881
+ " mend:",
882
+ " environment:",
883
+ ...entries.map(([key]) => ` ${key}: \${${key}:?set ${key} in server.env}`),
884
+ ""
885
+ ].join("\n");
886
+ };
887
+ var composeOverlays = (posture) => [
888
+ ...posture.edgeHost === void 0 ? [] : [EDGE_COMPOSE_FILE],
889
+ ...renderPostureOverlay(posture) === void 0 ? [] : [POSTURE_COMPOSE_FILE]
890
+ ];
891
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? new Map(Object.entries(value)) : null;
892
+ var healthPosture = (body) => {
893
+ const fields = record(body);
894
+ const tenancy = fields?.get("tenancy");
895
+ const gate = record(fields?.get("tenancyGate"));
896
+ const passed = gate?.get("passed");
897
+ const failing = gate?.get("failing");
898
+ const exposure = record(fields?.get("exposure"));
899
+ const declared = exposure?.get("declared");
900
+ const open = exposure?.get("open");
901
+ const unobservable = exposure?.get("unobservable");
902
+ return {
903
+ tenancy: typeof tenancy === "string" && isTenancy(tenancy) ? tenancy : null,
904
+ tenancyGate: typeof passed === "boolean" && Array.isArray(failing) ? {
905
+ passed,
906
+ failing: failing.filter((item) => typeof item === "string")
907
+ } : null,
908
+ exposure: typeof declared === "string" && isExposure(declared) && typeof open === "number" && typeof unobservable === "number" ? { declared, open, unobservable } : null
909
+ };
910
+ };
911
+ var declaredPostureLines = (posture) => [
912
+ ...posture.edgeHost === void 0 ? [] : [`edge \xB7 ${posture.edgeHost} \xB7 ${EDGE_IMAGE} on 80 and 443 \xB7 Mend's own port on loopback`],
913
+ `exposure \xB7 declared ${posture.exposure ?? "private"}${posture.exposure === void 0 ? " \xB7 the default, not set on this install" : ""}`,
914
+ `tenancy \xB7 declared ${posture.tenancy ?? "single"}${posture.tenancy === void 0 ? " \xB7 the default, not set on this install" : ""}`
915
+ ];
916
+ var observedEdgeLine = (host, observed) => [
917
+ `edge \xB7 ${host}`,
918
+ observed.running ? "container running" : "container not running",
919
+ observed.certificate.kind === "observed" ? `certificate observed in Caddy's data \xB7 ${observed.certificate.file}` : observed.certificate.kind === "none" ? "no certificate in Caddy's data yet \xB7 mend server logs shows what Caddy tried" : `certificate not observed \xB7 ${observed.certificate.reason}`
920
+ ].join(" \xB7 ");
921
+ var observedPostureLines = (posture, health) => {
922
+ const lines = [];
923
+ if (health.exposure === null) {
924
+ lines.push("exposure \xB7 observed \xB7 this server reports no exposure \xB7 it predates the gate");
925
+ } else {
926
+ const { declared, open, unobservable } = health.exposure;
927
+ const observable = open - unobservable;
928
+ lines.push(
929
+ [
930
+ `exposure \xB7 observed ${declared}`,
931
+ open === 0 ? "public exposure gate \xB7 nothing open" : `public exposure gate \xB7 ${open} item${open === 1 ? "" : "s"} open \xB7 ${observable} this build can observe \xB7 ${unobservable} no build can`,
932
+ "mend operator exposure lists them"
933
+ ].join(" \xB7 ")
934
+ );
935
+ if (posture.exposure !== void 0 && declared !== posture.exposure) {
936
+ lines.push(
937
+ `exposure \xB7 the running server declares ${declared}, this install ${posture.exposure}: it was started before the last mend server setup`
938
+ );
939
+ }
940
+ }
941
+ if (health.tenancy === null || health.tenancyGate === null) {
942
+ lines.push("tenancy \xB7 observed \xB7 this server reports no tenancy \xB7 it predates organizations");
943
+ } else {
944
+ const { failing } = health.tenancyGate;
945
+ lines.push(
946
+ [
947
+ `tenancy \xB7 observed ${health.tenancy}`,
948
+ failing.length === 0 ? "multi mode gate \xB7 nothing open" : `multi mode gate \xB7 open: ${failing.join(", ")}`,
949
+ "mend operator gate lists every item"
950
+ ].join(" \xB7 ")
951
+ );
952
+ if (posture.tenancy !== void 0 && health.tenancy !== posture.tenancy) {
953
+ lines.push(
954
+ `tenancy \xB7 the running server declares ${health.tenancy}, this install ${posture.tenancy}: it was started before the last mend server setup`
955
+ );
956
+ }
957
+ }
958
+ return lines;
959
+ };
437
960
 
438
961
  // src/server-runtime.ts
439
962
  import { spawn } from "node:child_process";
440
- import * as fs4 from "node:fs";
441
- import * as path4 from "node:path";
963
+ import * as fs5 from "node:fs";
964
+ import * as path5 from "node:path";
442
965
  var serverProcessDeadlines = {
443
966
  ordinary: 3e4,
444
967
  pull: 10 * 6e4,
@@ -481,10 +1004,10 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
481
1004
  ]) {
482
1005
  if (parentEnvironment[key] !== void 0) env[key] = parentEnvironment[key];
483
1006
  }
484
- return new Promise((resolve7) => {
1007
+ return new Promise((resolve8) => {
485
1008
  let fd;
486
1009
  try {
487
- if (options.stdoutFile !== void 0) fd = fs4.openSync(options.stdoutFile, "wx", 384);
1010
+ if (options.stdoutFile !== void 0) fd = fs5.openSync(options.stdoutFile, "wx", 384);
488
1011
  const child = spawn(command, [...args], {
489
1012
  env,
490
1013
  detached: process.platform !== "win32",
@@ -535,17 +1058,17 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
535
1058
  child.once("close", (status) => {
536
1059
  globalThis.clearTimeout(timer);
537
1060
  try {
538
- if (fd !== void 0) fs4.fsyncSync(fd);
1061
+ if (fd !== void 0) fs5.fsyncSync(fd);
539
1062
  } catch {
540
1063
  failure2 ??= "Could not durably write process output.";
541
1064
  } finally {
542
1065
  try {
543
- if (fd !== void 0) fs4.closeSync(fd);
1066
+ if (fd !== void 0) fs5.closeSync(fd);
544
1067
  } catch {
545
1068
  failure2 ??= "Could not close process output file.";
546
1069
  }
547
1070
  }
548
- resolve7({
1071
+ resolve8({
549
1072
  status: failure2 === void 0 ? status : null,
550
1073
  stdout,
551
1074
  stderr,
@@ -554,10 +1077,10 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
554
1077
  });
555
1078
  } catch (cause) {
556
1079
  try {
557
- if (fd !== void 0) fs4.closeSync(fd);
1080
+ if (fd !== void 0) fs5.closeSync(fd);
558
1081
  } catch {
559
1082
  }
560
- resolve7({
1083
+ resolve8({
561
1084
  status: null,
562
1085
  stdout: "",
563
1086
  stderr: "",
@@ -575,9 +1098,13 @@ var serverComposeArgs = (installation, command) => [
575
1098
  "--project-directory",
576
1099
  installation.directory,
577
1100
  "--env-file",
578
- path4.join(installation.directory, "server.env"),
1101
+ path5.join(installation.directory, "server.env"),
579
1102
  "-f",
580
- path4.join(installation.directory, "compose.yaml"),
1103
+ path5.join(installation.directory, "compose.yaml"),
1104
+ ...(installation.overlays ?? []).flatMap((file) => [
1105
+ "-f",
1106
+ path5.join(installation.directory, file)
1107
+ ]),
581
1108
  ...command
582
1109
  ];
583
1110
 
@@ -623,11 +1150,11 @@ var mapLimit = async (items, limit, work) => {
623
1150
  return results;
624
1151
  };
625
1152
  var pathOf = (command, env = process.env) => {
626
- for (const directory of (env["PATH"] ?? "").split(path5.delimiter)) {
1153
+ for (const directory of (env["PATH"] ?? "").split(path6.delimiter)) {
627
1154
  if (directory === "") continue;
628
- const candidate = path5.join(directory, command);
1155
+ const candidate = path6.join(directory, command);
629
1156
  try {
630
- fs5.accessSync(candidate, fs5.constants.X_OK);
1157
+ fs6.accessSync(candidate, fs6.constants.X_OK);
631
1158
  return candidate;
632
1159
  } catch {
633
1160
  }
@@ -645,9 +1172,9 @@ var cliCollector = (deps) => ({
645
1172
  node: process.versions.node,
646
1173
  platform: process.platform,
647
1174
  arch: process.arch,
648
- osType: os3.type(),
649
- osRelease: os3.release(),
650
- hostname: os3.hostname(),
1175
+ osType: os4.type(),
1176
+ osRelease: os4.release(),
1177
+ hostname: os4.hostname(),
651
1178
  term: deps.env["TERM"] ?? null,
652
1179
  shell: deps.env["SHELL"] ?? null,
653
1180
  lang: deps.env["LANG"] ?? null,
@@ -694,7 +1221,13 @@ var serverConfigCollector = (facts) => ({
694
1221
  envKeys: server.envKeys
695
1222
  })
696
1223
  },
697
- { path: "server-compose.yaml", content: server.compose }
1224
+ { path: "server-compose.yaml", content: server.compose },
1225
+ // The generation's own overlays, when it has them: the edge and the posture name which
1226
+ // variables reach the container and read every value from server.env, so they carry none.
1227
+ ...server.overlays.map((overlay) => ({
1228
+ path: `server-${overlay.name}`,
1229
+ content: overlay.content
1230
+ }))
698
1231
  ];
699
1232
  }
700
1233
  });
@@ -706,7 +1239,8 @@ var serverLogsCollector = (deps, facts) => ({
706
1239
  if (server === null) throw new Error(NO_SERVER);
707
1240
  const installation = {
708
1241
  directory: server.directory,
709
- dockerContext: server.config.dockerContext
1242
+ dockerContext: server.config.dockerContext,
1243
+ overlays: composeOverlays(server.config)
710
1244
  };
711
1245
  const listed = await deps.run(
712
1246
  "docker",
@@ -1023,9 +1557,9 @@ var sessionsCollector = (deps) => ({
1023
1557
  };
1024
1558
  }
1025
1559
  files2.push({ path: `sessions/${session.id}.json`, content: json(detail) });
1026
- const record = await recordFile(deps, detail);
1027
- if (record !== null)
1028
- files2.push({ path: `sessions/${session.id}-record.txt`, content: record });
1560
+ const record2 = await recordFile(deps, detail);
1561
+ if (record2 !== null)
1562
+ files2.push({ path: `sessions/${session.id}-record.txt`, content: record2 });
1029
1563
  const services = new Set(
1030
1564
  detail.processes.flatMap((row) => row.serviceId === null ? [] : [row.serviceId])
1031
1565
  );
@@ -1056,7 +1590,7 @@ var sessionsCollector = (deps) => ({
1056
1590
  services: services.size,
1057
1591
  liveServices: detail.liveServices ?? null,
1058
1592
  detail: `sessions/${session.id}.json`,
1059
- record: record === null ? null : `sessions/${session.id}-record.txt`
1593
+ record: record2 === null ? null : `sessions/${session.id}-record.txt`
1060
1594
  }
1061
1595
  };
1062
1596
  }
@@ -1117,8 +1651,8 @@ var bundleCollectors = (deps) => {
1117
1651
  };
1118
1652
 
1119
1653
  // src/doctor-bundle.ts
1120
- import * as fs6 from "node:fs";
1121
- import * as path6 from "node:path";
1654
+ import * as fs7 from "node:fs";
1655
+ import * as path7 from "node:path";
1122
1656
  import { gzipSync } from "node:zlib";
1123
1657
 
1124
1658
  // src/help.ts
@@ -1165,7 +1699,10 @@ var COMMANDS = [
1165
1699
  name: "connect",
1166
1700
  section: "start",
1167
1701
  summary: "send this machine's provider credential to the platform",
1168
- synopsis: ["<claude|codex|github> [--use-my-login] [--from-stdin] [--remove]"],
1702
+ synopsis: [
1703
+ "<claude|codex|github> [--use-my-login] [--from-stdin] [--remove]",
1704
+ "pi [--dir <path>] [--dry-run] [--remove]"
1705
+ ],
1169
1706
  description: [
1170
1707
  "Sessions run on the platform, so the platform needs your provider login. It is stored under your own user and used only for your work. Nothing is shared with other users.",
1171
1708
  "Claude and Codex get a login of Mend's own. Both rotate their refresh token, so two copies of one login fight and the one that refreshes second is signed out, and Mend's server refreshes on a schedule. So this logs in through the provider's own flow and your own login stays as it is: Claude in the browser, Codex with a device code, both sent and not kept here. Run it again whenever Mend says a login needs reconnecting. GitHub sends gh's token."
@@ -1183,7 +1720,24 @@ var COMMANDS = [
1183
1720
  { command: "mend connect codex", text: "a Codex login of Mend's own, with a device code" },
1184
1721
  { command: "mend connect github --remove", text: "" }
1185
1722
  ],
1186
- see: ["accounts"]
1723
+ see: ["accounts", "connect pi"]
1724
+ },
1725
+ {
1726
+ name: "connect pi",
1727
+ section: "start",
1728
+ summary: "send your pi setup to every pi session of yours",
1729
+ synopsis: ["[--dir <path>] [--dry-run] [--remove]"],
1730
+ description: [
1731
+ "Reads pi's agent directory on this machine (~/.pi/agent, or $PI_CODING_AGENT_DIR) and saves it as your pi profile: extensions, themes, prompt templates, settings, mcp.json and keybindings. Every pi session you start receives it, and before pi starts the session installs the packages settings.json declares and what your extensions import. A package that fails to install is left out of that session, and the terminal says why.",
1732
+ "Links are followed, so a setup a tool such as Home Manager links in is read as the files it points at. A package settings.json names by local path is copied into the profile. Your login stays here: pi runs on the ChatGPT login mend connect codex makes. Skills go with mend skills push. Run it again after you change your setup; sessions already running keep the profile they started with."
1733
+ ],
1734
+ options: [
1735
+ { flag: "--dir <path>", text: "read another agent directory" },
1736
+ { flag: "--dry-run", text: "say what would be sent, and send nothing" },
1737
+ { flag: "--remove", text: "delete the saved profile" }
1738
+ ],
1739
+ examples: [{ command: "mend connect pi --dry-run", text: "what your pi profile would hold" }],
1740
+ see: ["connect", "skills push"]
1187
1741
  },
1188
1742
  {
1189
1743
  name: "adopt",
@@ -1211,14 +1765,14 @@ var COMMANDS = [
1211
1765
  },
1212
1766
  {
1213
1767
  name: "codex",
1214
- aliases: ["claude", "opencode"],
1768
+ aliases: ["claude", "opencode", "pi"],
1215
1769
  section: "start",
1216
- summary: "launch codex, claude, or opencode in a recorded worktree",
1770
+ summary: "launch codex, claude, opencode or pi in a recorded worktree",
1217
1771
  synopsis: [
1218
1772
  '["prompt"] [--name <worktree>] [--worktree <existing>] [--model <id>] [--effort <level>] [--base <ref>] [--ask] [--fast] [--detach|-d] [--foreground] [--no-tunnel] [--land|--no-land] [--project <p>]'
1219
1773
  ],
1220
1774
  description: [
1221
- "mend codex, mend claude, and mend opencode are the same command with a different harness. The session runs in a workspace on the platform, in its own git worktree, and everything it does is recorded. This terminal attaches to it.",
1775
+ "mend codex, mend claude, mend opencode and mend pi are the same command with a different harness. The session runs in a workspace on the platform, in its own git worktree, and everything it does is recorded. This terminal attaches to it.",
1222
1776
  "The worktree's name is asked first. --name skips the ask; an existing name joins that worktree as a new session. --worktree joins only and fails if the name is unknown. A quoted prompt becomes the first message.",
1223
1777
  "Detach with Ctrl+] and the session keeps running. Reattach from any terminal with mend attach, or from the phone.",
1224
1778
  tunnelText,
@@ -1228,8 +1782,14 @@ var COMMANDS = [
1228
1782
  options: [
1229
1783
  { flag: "--name <worktree>", text: "name the worktree; an existing name joins it" },
1230
1784
  { flag: "--worktree <existing>", text: "join an existing worktree only" },
1231
- { flag: "--model <id>", text: "the harness's model id" },
1232
- { flag: "--effort <level>", text: "low, medium, high, xhigh, or max" },
1785
+ {
1786
+ flag: "--model <id>",
1787
+ text: "the harness's model id, one mend models lists. Default: the server's default for the harness"
1788
+ },
1789
+ {
1790
+ flag: "--effort <level>",
1791
+ text: "low, medium, high, xhigh, max, or ultra where the model takes it. Default: the harness's own"
1792
+ },
1233
1793
  { flag: "--base <ref>", text: "the branch or sha the worktree starts from" },
1234
1794
  { flag: "--ask", text: "keep the harness's permission prompts" },
1235
1795
  { flag: "--fast", text: "priority processing where the harness offers it (codex)" },
@@ -1253,7 +1813,23 @@ var COMMANDS = [
1253
1813
  text: "a second session in the same worktree"
1254
1814
  }
1255
1815
  ],
1256
- see: ["attach", "stop", "run", "sessions", "land"]
1816
+ see: ["attach", "stop", "run", "sessions", "land", "models"]
1817
+ },
1818
+ {
1819
+ name: "models",
1820
+ section: "start",
1821
+ summary: "the models each harness offers, as the server lists them",
1822
+ synopsis: ["[--json]"],
1823
+ description: [
1824
+ "One block per harness: each model's id, the one a launch runs when --model is not given, and the efforts a model takes where they are fewer than the harness's. The list is the server's; the phone, the web app, the desktop and VS Code offer the same one, and a session records which model it was started with.",
1825
+ "Pass an id to mend claude or mend codex with --model. An id the list does not know is passed through as given; the harness decides whether it exists."
1826
+ ],
1827
+ options: [{ flag: "--json", text: "the catalog as the server answers it" }],
1828
+ examples: [
1829
+ { command: "mend models", text: "" },
1830
+ { command: 'mend codex "fix the flaky test" --model gpt-6-astra --effort ultra', text: "" }
1831
+ ],
1832
+ see: ["codex", "sessions"]
1257
1833
  },
1258
1834
  {
1259
1835
  name: "pair",
@@ -1439,7 +2015,26 @@ var COMMANDS = [
1439
2015
  synopsis: ["[--project <p>] [--json]"],
1440
2016
  description: ["A worktree can hold several sessions over time; this shows them together."],
1441
2017
  options: [project(), { flag: "--json", text: "the v2 grouped shape" }],
1442
- see: ["sessions"]
2018
+ see: ["sessions", "worktrees rm"]
2019
+ },
2020
+ {
2021
+ name: "worktrees rm",
2022
+ section: "sessions",
2023
+ summary: "remove a worktree, its sessions and its change",
2024
+ synopsis: ["<name> [--force] [--project <p>]"],
2025
+ description: [
2026
+ "Removes the worktree directory from the store, with every session in it, its change, its checkpoints and its review. Nothing on origin is touched. <name> is the worktree's name as mend worktrees lists it.",
2027
+ "Refused while a session in it is live: the command names the sessions and the stops that free them. Refused while it holds a change that is not on origin, one never landed or one changed since its last landing: the refusal names the files and line counts, in the server's words. Land it with mend land, or pass --force to remove it anyway. A worktree whose workspace is still saving is refused until the save ends, and --force does not change that."
2028
+ ],
2029
+ options: [
2030
+ { flag: "--force", text: "remove it although its change is not on origin" },
2031
+ project()
2032
+ ],
2033
+ examples: [
2034
+ { command: "mend worktrees rm fix-login", text: "refused when its change was never landed" },
2035
+ { command: "mend worktrees rm fix-login --force", text: "remove it anyway" }
2036
+ ],
2037
+ see: ["worktrees", "land", "stop"]
1443
2038
  },
1444
2039
  {
1445
2040
  name: "projects",
@@ -1694,6 +2289,65 @@ var COMMANDS = [
1694
2289
  ],
1695
2290
  see: ["skills"]
1696
2291
  },
2292
+ {
2293
+ name: "memory",
2294
+ section: "project setup",
2295
+ summary: "what the agents remember about a project, for you",
2296
+ synopsis: [
2297
+ "[list] [--project <p>]",
2298
+ "show <file> [--project <p>]",
2299
+ "rm <file> [--project <p>]"
2300
+ ],
2301
+ description: [
2302
+ "Claude Code and Codex write what they learn about a repository to their memory. Mend keeps that memory per person per project: every session you start on the project receives it, and what the agent learned is saved back when it ends. When two of your sessions change the same text file, both sides' lines are kept.",
2303
+ "Codex builds its memory from your past conversations when a session starts, with model calls on your own login. Mend carries your earlier Codex conversations on the project into each new session so it has some to learn from.",
2304
+ "This lists your memory for the project, prints one file, or removes one. Removing keeps the last version on the server. Other people's sessions never see it."
2305
+ ],
2306
+ options: [project()],
2307
+ examples: [
2308
+ { command: "mend memory", text: "your memory for the project in this directory" },
2309
+ { command: "mend memory show MEMORY.md", text: "Claude's index of what it remembers" }
2310
+ ],
2311
+ see: ["memory import"]
2312
+ },
2313
+ {
2314
+ name: "memory import",
2315
+ section: "project setup",
2316
+ summary: "bring this machine's claude and codex memory for the repository",
2317
+ synopsis: ["[--project <p>] [--dry-run]"],
2318
+ description: [
2319
+ "Run inside the repository's checkout. Reads the memory Claude Code keeps on this machine for that directory, and the summaries Codex made of conversations held in it. Codex keeps one memory for everything, so only its summaries of this repository's conversations are read, never its consolidated memory. Transcripts, logins and settings are not read.",
2320
+ "A file Mend does not have is added. A file both sides have with other contents is merged, keeping both sides' lines (a line both wrote at different places stays twice), and a note's frontmatter is merged key by key, with this machine's value kept as a comment where the two differ. Mend remembers what it imported from this checkout on this machine, so the next import from here takes whichever side changed since and merges only what both changed. A file Mend removed since then is not added again. A file it cannot merge stays as Mend has it, and this machine's is kept as a version, as it is when a merge does not hold every line this machine sent. Every version Mend replaces is kept."
2321
+ ],
2322
+ options: [
2323
+ project(),
2324
+ { flag: "--dry-run", text: "show what the import would do, and write nothing" }
2325
+ ],
2326
+ examples: [{ command: "mend memory import --dry-run", text: "" }],
2327
+ see: ["memory", "adopt"]
2328
+ },
2329
+ {
2330
+ name: "memory show",
2331
+ section: "project setup",
2332
+ summary: "print one memory file",
2333
+ synopsis: ["<file> [--project <p>]"],
2334
+ description: [
2335
+ "Prints the file as Mend stores it, by the name mend memory lists. A name alone is Claude's; prefix another harness's with its name, as in codex:MEMORY.md."
2336
+ ],
2337
+ options: [project()],
2338
+ see: ["memory"]
2339
+ },
2340
+ {
2341
+ name: "memory rm",
2342
+ section: "project setup",
2343
+ summary: "remove one memory file",
2344
+ synopsis: ["<file> [--project <p>]"],
2345
+ description: [
2346
+ "Removes the file from your memory for the project. Sessions already running keep their copy; the next launch does not deliver it, and a session that still holds it unchanged moves it aside. The last version stays on the server."
2347
+ ],
2348
+ options: [project()],
2349
+ see: ["memory"]
2350
+ },
1697
2351
  {
1698
2352
  name: "dotfiles",
1699
2353
  section: "project setup",
@@ -1747,6 +2401,47 @@ var COMMANDS = [
1747
2401
  options: [{ flag: "--all", text: "every known config file" }],
1748
2402
  see: ["dotfiles", "dotfiles repo"]
1749
2403
  },
2404
+ {
2405
+ name: "secrets",
2406
+ section: "project setup",
2407
+ summary: "your secret files: written into every session you launch",
2408
+ synopsis: ["[list]"],
2409
+ description: [
2410
+ "A secret file is a file you keep in Mend, encrypted at rest, with its path under the home directory of the workspace: ~/.aws/credentials, a kubeconfig, an .npmrc token file. Every session you own receives your secret files before its agent starts, in every project. They are yours alone: no one else's sessions receive them, and the content never comes back out of the server. The web app's settings page shows the same list.",
2411
+ "A secret file is never captured. It is written into the workspace's own home directory, which no capture, change, checkpoint or transcript harvest covers, and a path under a directory sessions do capture, such as .claude or .codex, is refused.",
2412
+ "This lists your secret files: the path each takes in the workspace, its size, and when it last changed."
2413
+ ],
2414
+ examples: [{ command: "mend secrets", text: "" }],
2415
+ see: ["secrets add", "secrets rm", "env load"]
2416
+ },
2417
+ {
2418
+ name: "secrets add",
2419
+ section: "project setup",
2420
+ summary: "keep a file as a secret file, or replace one",
2421
+ synopsis: ["<path> [--from <file>]"],
2422
+ description: [
2423
+ "Reads the content from <file>, or from stdin without --from, and keeps it at <path> under the workspace home. A path on this machine under your home, such as ~/.aws/credentials, names the same place in the workspace. A file already kept at that path is replaced. Sessions launched from then on receive it; a running session keeps what it has.",
2424
+ "A file is at most 256 KB, and you may keep up to 64. Binary files are fine."
2425
+ ],
2426
+ options: [{ flag: "--from <file>", text: "read the content from this file instead of stdin" }],
2427
+ examples: [
2428
+ { command: "mend secrets add ~/.aws/credentials --from ~/.aws/credentials", text: "" },
2429
+ { command: "mend secrets add .kube/config --from ~/.kube/config", text: "" },
2430
+ { command: "mend secrets add .npmrc < ~/.npmrc", text: "" }
2431
+ ],
2432
+ see: ["secrets", "secrets rm"]
2433
+ },
2434
+ {
2435
+ name: "secrets rm",
2436
+ section: "project setup",
2437
+ summary: "remove a secret file",
2438
+ synopsis: ["<path>"],
2439
+ description: [
2440
+ "Removes the file kept at <path>. Sessions launched from then on do not receive it; a running session keeps what it has."
2441
+ ],
2442
+ examples: [{ command: "mend secrets rm .npmrc", text: "" }],
2443
+ see: ["secrets"]
2444
+ },
1750
2445
  {
1751
2446
  name: "git-author",
1752
2447
  section: "project setup",
@@ -1879,7 +2574,7 @@ var COMMANDS = [
1879
2574
  summary: "let everyone who can see a session steer it, or stop",
1880
2575
  synopsis: ["<session> on|off"],
1881
2576
  description: [
1882
- "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."
2577
+ "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 and interrupts, using your provider logins and Git access. They can read the terminal; only the owner types in it, opens a shell or starts a Service. Every act is recorded with who did it. A prefix of the session id is enough."
1883
2578
  ],
1884
2579
  examples: [{ command: "mend session share 3f2a on", text: "" }],
1885
2580
  see: ["sessions"]
@@ -1987,11 +2682,13 @@ var COMMANDS = [
1987
2682
  section: "this machine",
1988
2683
  summary: "install or repair the local Mend server",
1989
2684
  synopsis: [
1990
- "[--context <name>] [--version <version|latest>] [--bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
2685
+ "[--context <name>] [--version <version|latest>] [--bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--edge <host> | --no-edge] [--exposure <loopback|private|public>] [--tenancy <single|multi>] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
1991
2686
  ],
1992
2687
  description: [
1993
2688
  "Checks a local Unix-socket Docker context and the Compose plugin, downloads the compose and Postgres initialization assets for one Mend release, preserves existing data and secrets, and starts the server. Re-running repairs the same pinned version. A changed --version is refused; use mend server upgrade. Updating this CLI never updates an existing server pin.",
1994
2689
  "The default listens only on localhost at http://localhost:3105. Non-local access requires both --bind and --url. Every extra browser origin must be named with --origin; setup never guesses from the request Host header or network interfaces.",
2690
+ "--edge <host> runs a TLS edge in front of Mend: Caddy on ports 80 and 443 of every interface, which obtains and renews a certificate for the host and proxies to Mend's web tier. Mend's own port stays on loopback and the browser origin is https://<host>. The edge's compose overlay and Caddyfile are written into the generation beside compose.yaml, so start, restart and upgrade run them every time. --no-edge takes it away again, and the edge's container with it. A fresh install cannot start with the edge: until the first account exists, registration is open to whoever reaches the origin first, so set up on localhost, create the account, then add the edge.",
2691
+ "--exposure declares how the instance is reached, and --tenancy whether one organization or many use it. Both are written into the generation and kept across reruns and upgrades. public needs the edge and an existing first account. With multi, or with public, the multi mode gate's settings follow: MEND_SOURCE_POLICY=tenant and MEND_CAPTURE_REQUIRE_SIZES=true, and public sets MEND_URL_BEARERS=refuse. The server still decides whether it starts, and mend server status shows what it reports.",
1995
2692
  "Docker Desktop on Linux and macOS, and OrbStack on macOS, expose client-side proxy sockets. Containers use the daemon-side /var/run/docker.sock. --docker-socket overrides detection and is retained on reruns.",
1996
2693
  "Setup holds an exclusive process lock through startup and health checks. A busy lock reports its owner and manual recovery steps. Never remove a live lock. Private configuration uses immutable generations and an atomic active pointer; failed attempts retain credentials and never delete Docker volumes."
1997
2694
  ],
@@ -2015,6 +2712,19 @@ var COMMANDS = [
2015
2712
  flag: "--ssh-port <n>",
2016
2713
  text: "external workspace SSH port. Default: 2222; must differ from --port"
2017
2714
  },
2715
+ {
2716
+ flag: "--edge <host>",
2717
+ text: "run the Caddy TLS edge for this DNS name on 80 and 443; the origin becomes https://<host>, --bind stays on loopback, and the first account must already exist"
2718
+ },
2719
+ { flag: "--no-edge", text: "take a saved edge away; the origin returns to http://localhost" },
2720
+ {
2721
+ flag: "--exposure <v>",
2722
+ text: "declare loopback, private or public; kept across reruns and upgrades. public needs --edge and an existing first account"
2723
+ },
2724
+ {
2725
+ flag: "--tenancy <v>",
2726
+ text: "declare single or multi; kept across reruns and upgrades. multi also sets the multi mode gate's variables"
2727
+ },
2018
2728
  {
2019
2729
  flag: "--assets-dir <dir>",
2020
2730
  text: "copy compose.v2.yaml and postgres-init.sh from a release directory; fresh setup requires --version"
@@ -2034,19 +2744,28 @@ var COMMANDS = [
2034
2744
  {
2035
2745
  command: "mend server setup --version 0.25.0 --assets-dir ./release-assets --offline",
2036
2746
  text: "install from local release assets and preloaded images"
2747
+ },
2748
+ {
2749
+ command: "mend server setup --edge mend.example.com",
2750
+ text: "a TLS edge for mend.example.com; its DNS points at this machine and 80 and 443 reach it"
2751
+ },
2752
+ {
2753
+ command: "mend server setup --exposure public --tenancy multi",
2754
+ text: "on an install with an edge and a first account: declare public, many organizations"
2037
2755
  }
2038
2756
  ],
2039
- see: ["server", "login", "doctor"]
2757
+ see: ["server", "server status", "login", "doctor", "operator exposure"]
2040
2758
  },
2041
2759
  {
2042
2760
  name: "server status",
2043
2761
  section: "this machine",
2044
- summary: "show the saved pin, generation and container state",
2762
+ summary: "show the pin, generation, containers, edge and posture",
2045
2763
  synopsis: [""],
2046
2764
  description: [
2047
- "Reads the existing installation without changing its files. A running Mend must answer health with the exact pinned version. A stopped server makes no health claim. Never installs a server implicitly."
2765
+ "Reads the existing installation without changing its files. A running Mend must answer health with the exact pinned version. A stopped server makes no health claim. Never installs a server implicitly.",
2766
+ "Then the posture, declared beside observed. Declared is what this install's configuration says: the edge host, MEND_EXPOSURE and MEND_TENANCY, a default named as one. Observed is what was seen: whether the edge's container runs and whether Caddy's data holds a certificate for the host, and what the running server reports in its health, the exposure it runs with and how many public exposure gate items are open, the tenancy and which multi mode gate items are open. When this machine is signed in to the install as the operator, every item of both gates follows with its detail, as mend operator gate and mend operator exposure print them. None of it is a verdict: the report says what was declared and what was observed."
2048
2767
  ],
2049
- see: ["server logs", "server start"]
2768
+ see: ["server logs", "server start", "operator gate", "operator exposure"]
2050
2769
  },
2051
2770
  {
2052
2771
  name: "server start",
@@ -2062,10 +2781,10 @@ var COMMANDS = [
2062
2781
  {
2063
2782
  name: "server stop",
2064
2783
  section: "this machine",
2065
- summary: "stop Mend, Postgres and Garage without deleting data",
2784
+ summary: "stop Mend, Postgres, Garage and the edge without deleting data",
2066
2785
  synopsis: [""],
2067
2786
  description: [
2068
- "Stops only the installation's Compose services. Connections are interrupted. Workspace containers and volumes remain, but active work may lose connectivity and need reconnection. No volume deletion or Docker prune is performed."
2787
+ "Stops only the installation's Compose services, the edge among them when one is set. Connections are interrupted. Workspace containers and volumes remain, but active work may lose connectivity and need reconnection. No volume deletion or Docker prune is performed."
2069
2788
  ],
2070
2789
  see: ["server start", "server status"]
2071
2790
  },
@@ -2095,9 +2814,10 @@ var COMMANDS = [
2095
2814
  name: "server upgrade",
2096
2815
  section: "this machine",
2097
2816
  summary: "upgrade to an explicit version with a database backup",
2098
- synopsis: ["--version <target|latest> [--assets-dir <dir>] [--offline]"],
2817
+ synopsis: ["--version <target|latest> [--assets-dir <dir>] [--offline] [--from-preview]"],
2099
2818
  description: [
2100
2819
  "Preflights release assets and the canonical image's version label before interrupting Mend. Downgrades are refused. latest is resolved only when explicitly requested; a same-version request does nothing.",
2820
+ "The installed configuration stays: the bind, the origin, the ports, the edge host and the declared exposure and tenancy are carried into the new generation, which renders the same overlays beside the new compose.yaml. An upgrade never drops the edge or the posture; mend server setup is where they change.",
2101
2821
  "Stops app writers, starts official Postgres if needed, and streams pg_dumpall into a private database backup before selecting and starting the target. Connections are interrupted. Workspace containers and data are retained, but active work can lose connectivity and need reconnection. Stop any external database writers before upgrading.",
2102
2822
  "Preflight or backup failure retains the old pin and attempts to recover the old app if it was running. Once target startup may have begun, Mend never automatically downgrades or restores the database. The target pin, old generation and backup remain. Inspect logs, fix the target, then use mend server start. Recovery records are under the installation's backups/upgrade-UUID directory."
2103
2823
  ],
@@ -2113,6 +2833,10 @@ var COMMANDS = [
2113
2833
  {
2114
2834
  flag: "--offline",
2115
2835
  text: "no GitHub requests or release-image pulls; target assets and exact-version images must be local"
2836
+ },
2837
+ {
2838
+ flag: "--from-preview",
2839
+ text: "once, from a preview numbered X.Y.Z-preview.K to a next build X.Y.Z-next.N; refused unless the target carries every migration the server applied"
2116
2840
  }
2117
2841
  ],
2118
2842
  examples: [
@@ -2559,9 +3283,9 @@ var collectBundle = async (collectors) => {
2559
3283
  };
2560
3284
  var bundleFileName = (now) => `mend-bundle-${now.toISOString().replace(/\.\d{3}Z$/, "Z").replace(/:/g, "-")}.tgz`;
2561
3285
  var writeBundle = (files, outPath, now) => {
2562
- const directory = path6.dirname(outPath);
2563
- fs6.mkdirSync(directory, { recursive: true, mode: 448 });
2564
- const root = path6.basename(outPath).replace(/\.(?:tgz|tar\.gz)$/, "");
3286
+ const directory = path7.dirname(outPath);
3287
+ fs7.mkdirSync(directory, { recursive: true, mode: 448 });
3288
+ const root = path7.basename(outPath).replace(/\.(?:tgz|tar\.gz)$/, "");
2565
3289
  const entries = files.map((file) => ({
2566
3290
  path: file.path,
2567
3291
  content: Buffer.from(file.content, "utf8")
@@ -2570,8 +3294,8 @@ var writeBundle = (files, outPath, now) => {
2570
3294
  entries.map((entry) => ({ path: `${root}/${entry.path}`, content: entry.content })),
2571
3295
  now
2572
3296
  );
2573
- fs6.writeFileSync(outPath, bytes, { mode: 384 });
2574
- fs6.chmodSync(outPath, 384);
3297
+ fs7.writeFileSync(outPath, bytes, { mode: 384 });
3298
+ fs7.chmodSync(outPath, 384);
2575
3299
  return {
2576
3300
  path: outPath,
2577
3301
  bytes: bytes.length,
@@ -2632,7 +3356,7 @@ var doctorBundleCommand = async (args, deps) => {
2632
3356
  return;
2633
3357
  }
2634
3358
  const now = deps.now();
2635
- const outPath = path6.resolve(parsed.args.out ?? path6.join(deps.defaultDir, bundleFileName(now)));
3359
+ const outPath = path7.resolve(parsed.args.out ?? path7.join(deps.defaultDir, bundleFileName(now)));
2636
3360
  deps.warn("collecting \xB7 this can take a minute");
2637
3361
  const files = await collectBundle(deps.collectors(parsed.args.tail));
2638
3362
  const written = writeBundle(files, outPath, now);
@@ -2640,14 +3364,14 @@ var doctorBundleCommand = async (args, deps) => {
2640
3364
  };
2641
3365
 
2642
3366
  // src/doctor.ts
2643
- import * as fs8 from "node:fs";
2644
- import * as path8 from "node:path";
3367
+ import * as fs9 from "node:fs";
3368
+ import * as path9 from "node:path";
2645
3369
 
2646
3370
  // src/docker-shutdown.ts
2647
3371
  import { spawnSync as spawnSync3 } from "node:child_process";
2648
- import * as fs7 from "node:fs";
2649
- import * as os4 from "node:os";
2650
- import * as path7 from "node:path";
3372
+ import * as fs8 from "node:fs";
3373
+ import * as os5 from "node:os";
3374
+ import * as path8 from "node:path";
2651
3375
  var DOCKER_CAPTURE_STOP_GRACE_SECONDS = 3600;
2652
3376
  var DOCKERD_DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 15;
2653
3377
  var flagValue = (argv, name) => {
@@ -2672,12 +3396,12 @@ var daemonKindOf = (info) => {
2672
3396
  var defaultConfigPath = (facts, daemon) => {
2673
3397
  switch (daemon) {
2674
3398
  case "desktop":
2675
- return path7.join(facts.home, ".docker", "daemon.json");
3399
+ return path8.join(facts.home, ".docker", "daemon.json");
2676
3400
  case "orbstack":
2677
- return path7.join(facts.home, ".orbstack", "config", "docker.json");
3401
+ return path8.join(facts.home, ".orbstack", "config", "docker.json");
2678
3402
  case "rootless":
2679
- return path7.join(
2680
- facts.xdgConfigHome ?? path7.join(facts.home, ".config"),
3403
+ return path8.join(
3404
+ facts.xdgConfigHome ?? path8.join(facts.home, ".config"),
2681
3405
  "docker",
2682
3406
  "daemon.json"
2683
3407
  );
@@ -2806,7 +3530,7 @@ var parseDockerInfo = (stdout) => {
2806
3530
  };
2807
3531
  var readHostFile = (file) => {
2808
3532
  try {
2809
- return { kind: "read", text: fs7.readFileSync(file, "utf8") };
3533
+ return { kind: "read", text: fs8.readFileSync(file, "utf8") };
2810
3534
  } catch (cause) {
2811
3535
  return cause instanceof Error && "code" in cause && cause.code === "ENOENT" ? { kind: "absent" } : { kind: "unreadable" };
2812
3536
  }
@@ -2814,21 +3538,21 @@ var readHostFile = (file) => {
2814
3538
  var hostProcView = {
2815
3539
  list: (dir) => {
2816
3540
  try {
2817
- return fs7.readdirSync(dir);
3541
+ return fs8.readdirSync(dir);
2818
3542
  } catch {
2819
3543
  return null;
2820
3544
  }
2821
3545
  },
2822
3546
  read: (file) => {
2823
3547
  try {
2824
- return fs7.readFileSync(file, "utf8");
3548
+ return fs8.readFileSync(file, "utf8");
2825
3549
  } catch {
2826
3550
  return null;
2827
3551
  }
2828
3552
  },
2829
3553
  link: (file) => {
2830
3554
  try {
2831
- return fs7.readlinkSync(file);
3555
+ return fs8.readlinkSync(file);
2832
3556
  } catch {
2833
3557
  return null;
2834
3558
  }
@@ -2868,7 +3592,7 @@ var observeDockerd = (proc, host) => {
2868
3592
  const cmdline = proc.read(`/proc/${entry}/cmdline`);
2869
3593
  if (cmdline === null) continue;
2870
3594
  const argv = cmdline.split("\0").filter((arg) => arg !== "");
2871
- if (path7.basename(argv[0] ?? "") === "dockerd") daemons.push({ pid: entry, argv });
3595
+ if (path8.basename(argv[0] ?? "") === "dockerd") daemons.push({ pid: entry, argv });
2872
3596
  }
2873
3597
  if (daemons.length === 0) return { kind: "none" };
2874
3598
  const socket = unixSocketOf(host);
@@ -2954,7 +3678,7 @@ var hostDockerDaemonFacts = (infoStdout) => ({
2954
3678
  info: infoStdout === null ? null : parseDockerInfo(infoStdout),
2955
3679
  ...dockerdFactsOf(observeDockerd(hostProcView, hostDockerEndpoint())),
2956
3680
  readFile: readHostFile,
2957
- home: os4.homedir(),
3681
+ home: os5.homedir(),
2958
3682
  xdgConfigHome: process.env["XDG_CONFIG_HOME"] ?? null
2959
3683
  });
2960
3684
  var observeHostShutdownTimeout = () => {
@@ -3001,6 +3725,17 @@ var exposureCheck = (exposure) => {
3001
3725
  fix: plainBeyondMachine ? "serve it over https and set APP_URL to that origin" : gateOpen === 0 ? null : "mend operator exposure"
3002
3726
  };
3003
3727
  };
3728
+ var userNamespacesCheck = (observed) => observed.allowed ? {
3729
+ label: "workspaces",
3730
+ state: "ok",
3731
+ detail: "the server's host allows rootless Docker",
3732
+ fix: null
3733
+ } : {
3734
+ label: "workspaces",
3735
+ state: "failed",
3736
+ detail: "the server's host refuses user namespaces \xB7 no workspace can start",
3737
+ fix: observed.setting === null ? null : `on the server's host: ${hostUserNamespacesFix(observed.setting)}`
3738
+ };
3004
3739
  var LOGIN_COMMANDS = {
3005
3740
  claude: "claude setup-token",
3006
3741
  codex: "codex login",
@@ -3182,16 +3917,18 @@ var runChecks = async (config, probes) => {
3182
3917
  const machine = signedIn ? await getJson(config, "/machine") : null;
3183
3918
  const exposure = machine === null || machine.value === null ? void 0 : machine.value.exposure;
3184
3919
  checks.push(exposure === void 0 ? notChecked("exposure") : exposureCheck(exposure));
3920
+ const userNamespaces = machine === null || machine.value === null ? void 0 : machine.value.userNamespaces;
3921
+ if (userNamespaces !== void 0) checks.push(userNamespacesCheck(userNamespaces));
3185
3922
  if (probes.dockerShutdown !== void 0 && probes.onPath("docker")) {
3186
3923
  checks.push(dockerShutdownCheck(probes.dockerShutdown()));
3187
3924
  }
3188
3925
  return checks;
3189
3926
  };
3190
3927
  var onPath = (command) => {
3191
- for (const directory of (process.env["PATH"] ?? "").split(path8.delimiter)) {
3928
+ for (const directory of (process.env["PATH"] ?? "").split(path9.delimiter)) {
3192
3929
  if (directory === "") continue;
3193
3930
  try {
3194
- fs8.accessSync(path8.join(directory, command), fs8.constants.X_OK);
3931
+ fs9.accessSync(path9.join(directory, command), fs9.constants.X_OK);
3195
3932
  return true;
3196
3933
  } catch {
3197
3934
  }
@@ -3216,8 +3953,8 @@ var doctorCommand = async (config, localCredential2, claudeGrant2) => {
3216
3953
  };
3217
3954
 
3218
3955
  // src/dotfiles.ts
3219
- import * as fs9 from "node:fs";
3220
- import * as path9 from "node:path";
3956
+ import * as fs10 from "node:fs";
3957
+ import * as path10 from "node:path";
3221
3958
  var DOTFILE_CANDIDATES = [
3222
3959
  {
3223
3960
  group: "shell",
@@ -3258,44 +3995,44 @@ var DOTFILE_CANDIDATES = [
3258
3995
  }
3259
3996
  ];
3260
3997
  var MAX_FILE_BYTES = 1024 * 1024;
3261
- var statFile = (home, relative4) => {
3998
+ var statFile = (home, relative5) => {
3262
3999
  try {
3263
- const stat = fs9.statSync(path9.join(home, relative4));
4000
+ const stat = fs10.statSync(path10.join(home, relative5));
3264
4001
  return stat.isFile() ? stat : null;
3265
4002
  } catch {
3266
4003
  return null;
3267
4004
  }
3268
4005
  };
3269
4006
  var scanDotfileCandidates = (home) => DOTFILE_CANDIDATES.flatMap(
3270
- ({ group, paths }) => paths.flatMap((relative4) => {
3271
- const stat = statFile(home, relative4);
3272
- return stat === null ? [] : [{ path: relative4, group, bytes: stat.size }];
4007
+ ({ group, paths }) => paths.flatMap((relative5) => {
4008
+ const stat = statFile(home, relative5);
4009
+ return stat === null ? [] : [{ path: relative5, group, bytes: stat.size }];
3273
4010
  })
3274
4011
  );
3275
4012
  var homeRelativePath = (home, requested) => {
3276
- const relative4 = path9.relative(home, path9.resolve(home, requested));
3277
- const outside = relative4 === "" || relative4 === ".." || relative4.startsWith(`..${path9.sep}`) || path9.isAbsolute(relative4);
3278
- return outside ? null : relative4.split(path9.sep).join("/");
4013
+ const relative5 = path10.relative(home, path10.resolve(home, requested));
4014
+ const outside = relative5 === "" || relative5 === ".." || relative5.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative5);
4015
+ return outside ? null : relative5.split(path10.sep).join("/");
3279
4016
  };
3280
4017
  var readSyncFiles = (home, paths_) => {
3281
4018
  const files = [];
3282
4019
  for (const requested of paths_) {
3283
- const relative4 = homeRelativePath(home, requested);
3284
- if (relative4 === null) {
4020
+ const relative5 = homeRelativePath(home, requested);
4021
+ if (relative5 === null) {
3285
4022
  return {
3286
4023
  error: `${requested} is not under ${home} \u2014 only files in your home directory sync`
3287
4024
  };
3288
4025
  }
3289
- const stat = statFile(home, relative4);
4026
+ const stat = statFile(home, relative5);
3290
4027
  if (stat === null) {
3291
- return { error: `${relative4} is not a file under ${home}` };
4028
+ return { error: `${relative5} is not a file under ${home}` };
3292
4029
  }
3293
4030
  if (stat.size > MAX_FILE_BYTES) {
3294
- return { error: `${relative4} is over 1MB \u2014 dotfiles are text; trim the selection` };
4031
+ return { error: `${relative5} is over 1MB \u2014 dotfiles are text; trim the selection` };
3295
4032
  }
3296
4033
  files.push({
3297
- path: relative4,
3298
- contentsBase64: fs9.readFileSync(path9.join(home, relative4)).toString("base64"),
4034
+ path: relative5,
4035
+ contentsBase64: fs10.readFileSync(path10.join(home, relative5)).toString("base64"),
3299
4036
  // eslint-disable-next-line no-bitwise -- permission bits are the point
3300
4037
  mode: (stat.mode & 511).toString(8)
3301
4038
  });
@@ -3402,9 +4139,9 @@ var useHttp1 = () => {
3402
4139
 
3403
4140
  // src/landing.ts
3404
4141
  import { spawnSync as spawnSync4 } from "node:child_process";
3405
- import * as fs10 from "node:fs";
3406
- import * as os5 from "node:os";
3407
- import * as path10 from "node:path";
4142
+ import * as fs11 from "node:fs";
4143
+ import * as os6 from "node:os";
4144
+ import * as path11 from "node:path";
3408
4145
  var paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
3409
4146
  var dim = paint("2");
3410
4147
  var green = paint("32");
@@ -3642,9 +4379,9 @@ var gitRemotes = (cwd) => {
3642
4379
  var remoteForOrigin = (remotes, originUrl, cwd) => {
3643
4380
  if (originUrl === null) return null;
3644
4381
  const origin = normalizeRemoteUrl(originUrl);
3645
- const originPath = origin === null ? path10.resolve(originUrl.replace(/^file:\/\//, "")) : null;
4382
+ const originPath = origin === null ? path11.resolve(originUrl.replace(/^file:\/\//, "")) : null;
3646
4383
  return remotes.find(
3647
- (remote) => origin === null ? path10.resolve(cwd, remote.url.replace(/^file:\/\//, "")) === originPath : normalizeRemoteUrl(remote.url) === origin
4384
+ (remote) => origin === null ? path11.resolve(cwd, remote.url.replace(/^file:\/\//, "")) === originPath : normalizeRemoteUrl(remote.url) === origin
3648
4385
  ) ?? null;
3649
4386
  };
3650
4387
  var LOG_LINES = 10;
@@ -3667,10 +4404,10 @@ var fetchBundle = (cwd, bundle) => {
3667
4404
  const before = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
3668
4405
  const previous = before.status === 0 ? before.stdout.trim() : null;
3669
4406
  if (previous !== bundle.tip) {
3670
- const dir = fs10.mkdtempSync(path10.join(os5.tmpdir(), "mend-pull-"));
4407
+ const dir = fs11.mkdtempSync(path11.join(os6.tmpdir(), "mend-pull-"));
3671
4408
  try {
3672
- const file = path10.join(dir, "change.bundle");
3673
- fs10.writeFileSync(file, bundle.bytes, { mode: 384 });
4409
+ const file = path11.join(dir, "change.bundle");
4410
+ fs11.writeFileSync(file, bundle.bytes, { mode: 384 });
3674
4411
  const fetched = git(cwd, [
3675
4412
  "fetch",
3676
4413
  "--no-tags",
@@ -3680,7 +4417,7 @@ var fetchBundle = (cwd, bundle) => {
3680
4417
  ]);
3681
4418
  if (fetched.status !== 0) return refusedWith(gitWords(fetched));
3682
4419
  } finally {
3683
- fs10.rmSync(dir, { recursive: true, force: true });
4420
+ fs11.rmSync(dir, { recursive: true, force: true });
3684
4421
  }
3685
4422
  }
3686
4423
  const after = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).stdout.trim();
@@ -3803,7 +4540,9 @@ var agentRuns = (agent) => agent !== null && agent.kind !== "agent-protocol" &&
3803
4540
  var startingLineOf = (session) => {
3804
4541
  const summary = session.summary?.trim() ?? "";
3805
4542
  if (summary === "") return session.status;
3806
- if (/^[a-z][a-z -]*·/u.test(summary)) return summary;
4543
+ if (/^(starting|running|waiting|idle|stopping|completed|failed|stopped) ·/u.test(summary)) {
4544
+ return summary;
4545
+ }
3807
4546
  return `${session.status} \xB7 ${summary}`;
3808
4547
  };
3809
4548
  var refusalOf = (error) => error instanceof MendRequestError && error.kind === "http" && (error.status === null || !GATEWAY_STATUSES.has(error.status)) ? error.message : null;
@@ -3871,7 +4610,7 @@ var followStart = async (options) => {
3871
4610
 
3872
4611
  // src/login.ts
3873
4612
  import { spawn as spawn2 } from "node:child_process";
3874
- import * as os6 from "node:os";
4613
+ import * as os7 from "node:os";
3875
4614
 
3876
4615
  // src/pair.ts
3877
4616
  var paint2 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
@@ -4045,7 +4784,7 @@ var retryAfterSecondsOf = (json2) => {
4045
4784
  }
4046
4785
  return 5;
4047
4786
  };
4048
- var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
4787
+ var sleep2 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
4049
4788
  var resolveServerUrl = async (args, deps) => {
4050
4789
  const given = takeFlagValue2(args, "--url") ?? deps.configuredUrl;
4051
4790
  if (given !== null) {
@@ -4063,7 +4802,7 @@ var resolveServerUrl = async (args, deps) => {
4063
4802
  };
4064
4803
  var loginCommand = async (args, deps) => {
4065
4804
  const base = await resolveServerUrl(args, deps);
4066
- const started = await post(`${base}/api/cli/auth`, { name: os6.hostname() });
4805
+ const started = await post(`${base}/api/cli/auth`, { name: os7.hostname() });
4067
4806
  if (started.status === 429) {
4068
4807
  return fail3(
4069
4808
  `the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
@@ -4113,9 +4852,42 @@ var loginCommand = async (args, deps) => {
4113
4852
  return fail3("the authorize request expired before anyone approved \u2014 run mend login again");
4114
4853
  };
4115
4854
 
4855
+ // src/models.ts
4856
+ var contiguousIn = (efforts, scale) => {
4857
+ const from = scale.indexOf(efforts[0] ?? "");
4858
+ return from >= 0 && efforts.every((effort, index) => scale[from + index] === effort);
4859
+ };
4860
+ var effortRange = (efforts, scale) => efforts.length === 0 ? "none" : efforts.length <= 2 || !contiguousIn(efforts, scale) ? efforts.join(", ") : `${efforts[0]} \u2026 ${efforts.at(-1)}`;
4861
+ var modelCatalogLines = (catalogs, dim7 = (text) => text) => {
4862
+ if (catalogs.length === 0)
4863
+ return ["no models listed \xB7 the server's harness_models table is empty"];
4864
+ const lines = [];
4865
+ for (const [index, catalog] of catalogs.entries()) {
4866
+ if (index > 0) lines.push("");
4867
+ const efforts = effortRange(catalog.efforts, catalog.efforts);
4868
+ lines.push(
4869
+ `${catalog.harness} ${dim7(`effort ${efforts}${catalog.fastCapable ? " \xB7 --fast" : ""}`)}`
4870
+ );
4871
+ const idWidth = Math.max(...catalog.models.map((model) => model.id.length));
4872
+ for (const model of catalog.models) {
4873
+ const facts = [];
4874
+ if (model.id === catalog.defaultModel) facts.push("default");
4875
+ if (model.efforts !== null) {
4876
+ facts.push(`effort ${effortRange(model.efforts, catalog.efforts)}`);
4877
+ }
4878
+ lines.push(
4879
+ ` ${model.id.padEnd(idWidth)} ${model.label}${facts.length === 0 ? "" : ` ${dim7(facts.join(" \xB7 "))}`}`
4880
+ );
4881
+ }
4882
+ if (catalog.models.length === 0) lines.push(` ${dim7("no models listed \xB7 the harness picks")}`);
4883
+ }
4884
+ return lines;
4885
+ };
4886
+ var modelCatalogJson = (catalogs) => JSON.stringify({ version: 1, harnesses: catalogs }, null, 2);
4887
+
4116
4888
  // src/organization.ts
4117
- import * as fs11 from "node:fs";
4118
- import * as path11 from "node:path";
4889
+ import * as fs12 from "node:fs";
4890
+ import * as path12 from "node:path";
4119
4891
  var paint4 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
4120
4892
  var dim4 = paint4("2");
4121
4893
  var green4 = paint4("32");
@@ -4178,23 +4950,23 @@ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
4178
4950
  var scanFolder = (dir) => {
4179
4951
  const files = [];
4180
4952
  const skipped = [];
4181
- const walk = (relative4) => {
4182
- const entries = fs11.readdirSync(path11.join(dir, relative4), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
4953
+ const walk2 = (relative5) => {
4954
+ const entries = fs12.readdirSync(path12.join(dir, relative5), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
4183
4955
  for (const entry of entries) {
4184
- const child = relative4 === "" ? entry.name : `${relative4}/${entry.name}`;
4956
+ const child = relative5 === "" ? entry.name : `${relative5}/${entry.name}`;
4185
4957
  if (entry.isSymbolicLink()) {
4186
4958
  skipped.push({ path: child, reason: "symlink" });
4187
4959
  } else if (entry.isDirectory()) {
4188
4960
  if (SKIPPED_DIRECTORIES.has(entry.name)) skipped.push({ path: child, reason: "skipped" });
4189
- else walk(child);
4961
+ else walk2(child);
4190
4962
  } else if (entry.isFile()) {
4191
- const size = fs11.statSync(path11.join(dir, child)).size;
4963
+ const size = fs12.statSync(path12.join(dir, child)).size;
4192
4964
  if (size > FOLDER_FILE_BYTES) skipped.push({ path: child, reason: "over 1 MiB" });
4193
4965
  else files.push({ path: child, size });
4194
4966
  }
4195
4967
  }
4196
4968
  };
4197
- walk("");
4969
+ walk2("");
4198
4970
  return { files, skipped };
4199
4971
  };
4200
4972
  var uploadBatches = (files, cap = FOLDER_REQUEST_BYTES) => {
@@ -4241,7 +5013,7 @@ var folderCommand = async (api2, args) => {
4241
5013
  }
4242
5014
  case "push": {
4243
5015
  if (dir === void 0) return fail4("usage: mend folder push <name> <dir> [--replace]");
4244
- if (!fs11.existsSync(dir) || !fs11.statSync(dir).isDirectory()) {
5016
+ if (!fs12.existsSync(dir) || !fs12.statSync(dir).isDirectory()) {
4245
5017
  return fail4(`${dir} is not a directory`);
4246
5018
  }
4247
5019
  const folder = await folderNamed(api2, name);
@@ -4256,7 +5028,7 @@ var folderCommand = async (api2, args) => {
4256
5028
  files: batch.map((file) => ({
4257
5029
  path: file.path,
4258
5030
  // Read one batch at a time, so a large tree never sits in memory at once.
4259
- contentsBase64: fs11.readFileSync(path11.join(dir, file.path)).toString("base64")
5031
+ contentsBase64: fs12.readFileSync(path12.join(dir, file.path)).toString("base64")
4260
5032
  })),
4261
5033
  // --replace empties the folder with the first batch; the rest add to it.
4262
5034
  merge: !(replace && index === 0)
@@ -4436,12 +5208,334 @@ var operatorCommand = async (api2, tryApi, baseUrl, args) => {
4436
5208
  return fail4(`unknown operator command "${first ?? ""}" \xB7 mend help operator org list`);
4437
5209
  };
4438
5210
 
5211
+ // src/pi-profile.ts
5212
+ import * as fs13 from "node:fs";
5213
+ import * as os8 from "node:os";
5214
+ import * as path13 from "node:path";
5215
+ var piAgentDir = () => process.env["PI_CODING_AGENT_DIR"] ?? path13.join(os8.homedir(), ".pi", "agent");
5216
+ var RESOURCE_DIRS = ["extensions", "themes", "prompts"];
5217
+ var BESIDE = [
5218
+ ["package.json", "package.json"],
5219
+ ["package-lock.json", "package-lock.json"],
5220
+ ["mcp.json", "root/mcp.json"],
5221
+ ["keybindings.json", "root/keybindings.json"]
5222
+ ];
5223
+ var MACHINE_SETTINGS = /* @__PURE__ */ new Set(["lastChangelogVersion"]);
5224
+ var LEFT_HERE_WHY = {
5225
+ "auth.json": "auth.json (pi runs on your ChatGPT login: mend connect codex)",
5226
+ skills: "skills (mend skills push --dir <agent dir>/skills)",
5227
+ "AGENTS.md": "AGENTS.md (Mend writes its workspace note there)",
5228
+ npm: "npm (the session installs what settings.json declares)",
5229
+ git: "git (the session installs what settings.json declares)"
5230
+ };
5231
+ var SILENT = /* @__PURE__ */ new Set(["node_modules", "settings.json", ".gitignore"]);
5232
+ var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
5233
+ var utf8 = new TextDecoder("utf-8", { fatal: true });
5234
+ var asProfileFile = (profilePath, bytes) => {
5235
+ if (!bytes.includes(0)) {
5236
+ try {
5237
+ return { path: profilePath, encoding: "utf8", contents: utf8.decode(bytes) };
5238
+ } catch {
5239
+ }
5240
+ }
5241
+ return { path: profilePath, encoding: "base64", contents: bytes.toString("base64") };
5242
+ };
5243
+ var walk = (root, notes) => {
5244
+ const found = [];
5245
+ const seen = /* @__PURE__ */ new Set();
5246
+ const visit = (dir, rel) => {
5247
+ let real;
5248
+ try {
5249
+ real = fs13.realpathSync(dir);
5250
+ } catch {
5251
+ return;
5252
+ }
5253
+ if (seen.has(real)) return;
5254
+ seen.add(real);
5255
+ for (const name of fs13.readdirSync(dir).toSorted()) {
5256
+ if (SKIPPED_DIRS.has(name)) continue;
5257
+ const abs = path13.join(dir, name);
5258
+ const relPath = rel === "" ? name : `${rel}/${name}`;
5259
+ let stat;
5260
+ try {
5261
+ stat = fs13.statSync(abs);
5262
+ } catch {
5263
+ notes.push(`${relPath}: a link to nothing, left out`);
5264
+ continue;
5265
+ }
5266
+ if (stat.isDirectory()) visit(abs, relPath);
5267
+ else if (stat.isFile()) found.push([abs, relPath]);
5268
+ }
5269
+ };
5270
+ visit(root, "");
5271
+ return found;
5272
+ };
5273
+ var isLocalSource = (source) => !/^(npm|git):/.test(source) && !/^(https?|ssh|git):\/\//.test(source) && !source.startsWith("git@");
5274
+ var expandHome = (source) => source === "~" || source.startsWith("~/") ? path13.join(os8.homedir(), source.slice(1)) : source;
5275
+ var bundleName = (abs) => {
5276
+ try {
5277
+ const manifest = JSON.parse(fs13.readFileSync(path13.join(abs, "package.json"), "utf8"));
5278
+ if (manifest !== null && typeof manifest === "object" && "name" in manifest) {
5279
+ const name = manifest.name;
5280
+ if (typeof name === "string" && name !== "") return name.split("/").at(-1) ?? name;
5281
+ }
5282
+ } catch {
5283
+ }
5284
+ return path13.basename(abs).replace(/^[0-9a-z]{32}-/, "");
5285
+ };
5286
+ var HOST_PROVIDED = (name) => name.startsWith("@earendil-works/") || name === "typebox";
5287
+ var versionOf = (dir) => {
5288
+ try {
5289
+ const manifest = JSON.parse(fs13.readFileSync(path13.join(dir, "package.json"), "utf8"));
5290
+ return manifest !== null && typeof manifest === "object" && "version" in manifest && typeof manifest.version === "string" ? manifest.version : null;
5291
+ } catch {
5292
+ return null;
5293
+ }
5294
+ };
5295
+ var installedPackages = (nodeModules) => {
5296
+ const names = fs13.readdirSync(nodeModules).flatMap((entry) => {
5297
+ if (entry.startsWith(".")) return [];
5298
+ if (!entry.startsWith("@")) return [entry];
5299
+ try {
5300
+ return fs13.readdirSync(path13.join(nodeModules, entry)).map((scoped) => `${entry}/${scoped}`);
5301
+ } catch {
5302
+ return [];
5303
+ }
5304
+ });
5305
+ const dependencies = {};
5306
+ for (const name of names.toSorted()) {
5307
+ if (HOST_PROVIDED(name)) continue;
5308
+ const version = versionOf(path13.join(nodeModules, name));
5309
+ if (version !== null) dependencies[name] = version;
5310
+ }
5311
+ return dependencies;
5312
+ };
5313
+ var scanPiProfile = (agentDir) => {
5314
+ if (!fs13.existsSync(agentDir) || !fs13.statSync(agentDir).isDirectory()) {
5315
+ return { error: `no pi agent directory at ${agentDir}; run pi once, or pass --dir` };
5316
+ }
5317
+ const notes = [];
5318
+ const files = [];
5319
+ const add = (abs, profilePath) => {
5320
+ const bytes = fs13.readFileSync(abs);
5321
+ if (bytes.byteLength > PI_PROFILE_MAX_FILE_BYTES) {
5322
+ notes.push(`${profilePath}: over ${PI_PROFILE_MAX_FILE_BYTES / (1024 * 1024)} MB, left out`);
5323
+ return;
5324
+ }
5325
+ files.push(asProfileFile(profilePath, bytes));
5326
+ };
5327
+ for (const dir of RESOURCE_DIRS) {
5328
+ const abs = path13.join(agentDir, dir);
5329
+ if (!fs13.existsSync(abs)) continue;
5330
+ for (const [file, rel] of walk(abs, notes)) add(file, `${dir}/${rel}`);
5331
+ }
5332
+ const beside = [];
5333
+ for (const [name, profilePath] of BESIDE) {
5334
+ const abs = path13.join(agentDir, name);
5335
+ if (!fs13.existsSync(abs) || !fs13.statSync(abs).isFile()) continue;
5336
+ add(abs, profilePath);
5337
+ beside.push(name);
5338
+ }
5339
+ const nodeModules = path13.join(agentDir, "node_modules");
5340
+ if (!beside.includes("package.json") && fs13.existsSync(nodeModules)) {
5341
+ const dependencies = installedPackages(nodeModules);
5342
+ const count = Object.keys(dependencies).length;
5343
+ if (count > 0) {
5344
+ files.push({
5345
+ path: "package.json",
5346
+ encoding: "utf8",
5347
+ contents: `${JSON.stringify(
5348
+ {
5349
+ name: "pi-agent-dependencies",
5350
+ private: true,
5351
+ description: "Written by mend connect pi from the node_modules beside your extensions",
5352
+ dependencies
5353
+ },
5354
+ null,
5355
+ 2
5356
+ )}
5357
+ `
5358
+ });
5359
+ beside.push(`package.json (written from node_modules: ${count} packages)`);
5360
+ }
5361
+ }
5362
+ const packages = [];
5363
+ let settingNames = [];
5364
+ const settingsPath = path13.join(agentDir, "settings.json");
5365
+ if (fs13.existsSync(settingsPath)) {
5366
+ let settings;
5367
+ try {
5368
+ settings = JSON.parse(fs13.readFileSync(settingsPath, "utf8"));
5369
+ } catch {
5370
+ settings = null;
5371
+ }
5372
+ if (settings === null || typeof settings !== "object" || Array.isArray(settings)) {
5373
+ return { error: `${settingsPath} is not a JSON object; fix it or move it aside first` };
5374
+ }
5375
+ const carried = {};
5376
+ const names = /* @__PURE__ */ new Set();
5377
+ for (const [key, value] of Object.entries(settings)) {
5378
+ if (MACHINE_SETTINGS.has(key)) continue;
5379
+ if (key !== "packages") {
5380
+ carried[key] = value;
5381
+ continue;
5382
+ }
5383
+ if (!Array.isArray(value)) {
5384
+ notes.push("settings.json: packages is not a list, left out");
5385
+ continue;
5386
+ }
5387
+ const declared = [];
5388
+ for (const entry of value) {
5389
+ const source = typeof entry === "string" ? entry : entry !== null && typeof entry === "object" && "source" in entry ? entry.source : null;
5390
+ if (typeof source !== "string") {
5391
+ notes.push(`settings.json: a package entry without a source, left out`);
5392
+ continue;
5393
+ }
5394
+ if (!isLocalSource(source)) {
5395
+ declared.push(entry);
5396
+ packages.push({ source, from: null });
5397
+ continue;
5398
+ }
5399
+ const abs = path13.resolve(agentDir, expandHome(source));
5400
+ if (!fs13.existsSync(abs)) {
5401
+ notes.push(`package ${source}: not on this machine, left out`);
5402
+ continue;
5403
+ }
5404
+ let name = bundleName(abs);
5405
+ for (let n = 2; names.has(name); n += 1) name = `${bundleName(abs)}-${n}`;
5406
+ names.add(name);
5407
+ const bundled = `${PI_PROFILE_LOCAL_PACKAGES}/${name}`;
5408
+ if (fs13.statSync(abs).isDirectory()) {
5409
+ for (const [file, rel] of walk(abs, notes)) add(file, `${bundled}/${rel}`);
5410
+ } else {
5411
+ add(abs, `${bundled}/${path13.basename(abs)}`);
5412
+ }
5413
+ const rewritten = `./${PI_PROFILE_AGENT_PATH}/${bundled}${fs13.statSync(abs).isDirectory() ? "" : `/${path13.basename(abs)}`}`;
5414
+ declared.push(
5415
+ entry !== null && typeof entry === "object" ? { ...entry, source: rewritten } : rewritten
5416
+ );
5417
+ packages.push({ source: rewritten, from: source });
5418
+ }
5419
+ carried["packages"] = declared;
5420
+ }
5421
+ settingNames = Object.keys(carried).filter((key) => key !== "packages");
5422
+ files.push({
5423
+ path: "settings.json",
5424
+ encoding: "utf8",
5425
+ contents: `${JSON.stringify(carried, null, 2)}
5426
+ `
5427
+ });
5428
+ }
5429
+ const mcpPath = path13.join(agentDir, "mcp.json");
5430
+ if (fs13.existsSync(mcpPath)) {
5431
+ try {
5432
+ const mcp = JSON.parse(fs13.readFileSync(mcpPath, "utf8"));
5433
+ const servers = mcp !== null && typeof mcp === "object" && "mcpServers" in mcp ? mcp.mcpServers : null;
5434
+ if (servers !== null && typeof servers === "object") {
5435
+ for (const [name, server] of Object.entries(servers)) {
5436
+ const command = server !== null && typeof server === "object" && "command" in server ? server.command : null;
5437
+ if (typeof command === "string" && path13.isAbsolute(command)) {
5438
+ notes.push(
5439
+ `mcp.json: server ${name} runs ${command}, a path on this machine a session may not have`
5440
+ );
5441
+ }
5442
+ }
5443
+ }
5444
+ } catch {
5445
+ notes.push("mcp.json: not JSON; sent as it is");
5446
+ }
5447
+ }
5448
+ const carriedTop = /* @__PURE__ */ new Set([...RESOURCE_DIRS, ...BESIDE.map(([name]) => name)]);
5449
+ const leftHere = fs13.readdirSync(agentDir).toSorted().filter((name) => !carriedTop.has(name) && !SILENT.has(name)).map((name) => LEFT_HERE_WHY[name] ?? name);
5450
+ return {
5451
+ agentDir,
5452
+ files,
5453
+ bytes: files.reduce(
5454
+ (sum, file) => sum + (file.encoding === "utf8" ? Buffer.byteLength(file.contents, "utf8") : Buffer.from(file.contents, "base64").byteLength),
5455
+ 0
5456
+ ),
5457
+ extensions: [
5458
+ ...new Set(
5459
+ files.flatMap((file) => {
5460
+ const [top, name] = file.path.split("/");
5461
+ return top === "extensions" && name !== void 0 ? [name] : [];
5462
+ })
5463
+ )
5464
+ ],
5465
+ themes: files.filter((file) => file.path.startsWith("themes/") && file.path.endsWith(".json")).length,
5466
+ prompts: files.filter((file) => file.path.startsWith("prompts/") && file.path.endsWith(".md")).length,
5467
+ packages,
5468
+ settings: settingNames,
5469
+ beside,
5470
+ leftHere,
5471
+ notes
5472
+ };
5473
+ };
5474
+ var megabytes = (bytes) => bytes < 1024 * 1024 ? `${Math.max(1, Math.round(bytes / 1024))} KB` : `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5475
+ var list = (items) => items.length === 0 ? "none" : items.join(", ");
5476
+ var piProfileLines = (scan) => {
5477
+ return [
5478
+ `pi profile \xB7 ${scan.agentDir} \xB7 ${scan.files.length} files \xB7 ${megabytes(scan.bytes)}`,
5479
+ ` extensions ${scan.extensions.length === 0 ? "none" : `${scan.extensions.length} \xB7 ${list(scan.extensions)}`}`,
5480
+ ` themes ${scan.themes}`,
5481
+ ` prompts ${scan.prompts}`,
5482
+ ` packages ${list(
5483
+ scan.packages.map(
5484
+ (entry) => entry.from === null ? entry.source : `${entry.from} (copied in)`
5485
+ )
5486
+ )}`,
5487
+ ` settings ${list(scan.settings)}`,
5488
+ ...scan.beside.length === 0 ? [] : [
5489
+ ` also ${list(
5490
+ scan.beside.map(
5491
+ (name) => name === "mcp.json" ? "mcp.json (as it is, with any keys in it)" : name
5492
+ )
5493
+ )}`
5494
+ ],
5495
+ ...scan.leftHere.length === 0 ? [] : [` left here ${list(scan.leftHere)}`],
5496
+ ...scan.notes.map((note) => ` ! ${note}`)
5497
+ ];
5498
+ };
5499
+
5500
+ // src/secret-files.ts
5501
+ import * as path14 from "node:path";
5502
+ var secretFilePathOf = (input, home) => {
5503
+ const trimmed = input.trim();
5504
+ let relative5 = trimmed;
5505
+ if (trimmed === "~" || trimmed.startsWith("~/")) relative5 = trimmed.slice(2);
5506
+ else if (path14.isAbsolute(trimmed)) {
5507
+ const fromHome = path14.relative(home, trimmed);
5508
+ if (fromHome === "" || fromHome.startsWith("..") || path14.isAbsolute(fromHome)) {
5509
+ return {
5510
+ path: null,
5511
+ issue: `${trimmed} is outside your home directory; name the path under the workspace home, as .aws/credentials`
5512
+ };
5513
+ }
5514
+ relative5 = fromHome;
5515
+ }
5516
+ const normalized = path14.posix.normalize(relative5).replace(/^(\.\/)+/, "").replace(/\/$/, "");
5517
+ const issue = validateSecretFilePath(normalized === "." ? "" : normalized);
5518
+ return issue === null ? { path: normalized, issue: null } : { path: null, issue };
5519
+ };
5520
+ var secretFileUploadOf = (filePath, bytes) => {
5521
+ const text = bytes.includes(0) ? null : bytes.toString("utf8");
5522
+ return text !== null && Buffer.from(text, "utf8").equals(bytes) ? { path: filePath, encoding: "utf8", contents: text } : { path: filePath, encoding: "base64", contents: bytes.toString("base64") };
5523
+ };
5524
+ var sizeOf = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
5525
+ var secretFileLines = (files) => {
5526
+ const width = Math.max(0, ...files.map((file) => file.path.length + 2));
5527
+ return files.map(
5528
+ (file) => `~/${file.path}`.padEnd(width) + ` ${sizeOf(file.bytes).padStart(8)} ${file.updatedAt.slice(0, 16).replace("T", " ")}` + (file.revision > 1 ? ` \xB7 replaced ${file.revision - 1} time${file.revision === 2 ? "" : "s"}` : "")
5529
+ );
5530
+ };
5531
+
4439
5532
  // src/server-setup.ts
4440
5533
  import { createHmac, randomBytes } from "node:crypto";
5534
+ import * as fs16 from "node:fs";
4441
5535
  import { readFile } from "node:fs/promises";
4442
- import * as net2 from "node:net";
4443
- import * as os8 from "node:os";
4444
- import * as path13 from "node:path";
5536
+ import * as net3 from "node:net";
5537
+ import * as os10 from "node:os";
5538
+ import * as path16 from "node:path";
4445
5539
 
4446
5540
  // src/server-docker-volumes.ts
4447
5541
  import { createHash as createHash2 } from "node:crypto";
@@ -4658,10 +5752,10 @@ var claimServerDockerVolumes = (runtime, input) => ownership(runtime, input, "cl
4658
5752
  var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "verify");
4659
5753
 
4660
5754
  // src/server-store.ts
4661
- import { randomUUID } from "node:crypto";
4662
- import * as fs12 from "node:fs";
4663
- import * as os7 from "node:os";
4664
- import * as path12 from "node:path";
5755
+ import { randomUUID as randomUUID2 } from "node:crypto";
5756
+ import * as fs14 from "node:fs";
5757
+ import * as os9 from "node:os";
5758
+ import * as path15 from "node:path";
4665
5759
  var ServerStoreError = class extends Error {
4666
5760
  _tag = "ServerStoreError";
4667
5761
  };
@@ -4682,38 +5776,53 @@ var attempt = (operation) => {
4682
5776
  var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
4683
5777
  var readOptional = (file) => {
4684
5778
  try {
4685
- return fs12.readFileSync(file, "utf8");
5779
+ return fs14.readFileSync(file, "utf8");
4686
5780
  } catch (cause) {
4687
5781
  if (hasCode(cause, "ENOENT")) return null;
4688
5782
  throw cause;
4689
5783
  }
4690
5784
  };
4691
5785
  var syncDirectory = (directory) => {
4692
- const fd = fs12.openSync(directory, "r");
5786
+ const fd = fs14.openSync(directory, "r");
4693
5787
  try {
4694
- fs12.fsyncSync(fd);
5788
+ fs14.fsyncSync(fd);
4695
5789
  } finally {
4696
- fs12.closeSync(fd);
5790
+ fs14.closeSync(fd);
4697
5791
  }
4698
5792
  };
4699
5793
  var writeDurable = (file, content, mode = 384) => {
4700
- const fd = fs12.openSync(file, "wx", mode);
5794
+ const fd = fs14.openSync(file, "wx", mode);
4701
5795
  try {
4702
- fs12.writeFileSync(fd, content, "utf8");
4703
- fs12.fchmodSync(fd, mode);
4704
- fs12.fsyncSync(fd);
5796
+ fs14.writeFileSync(fd, content, "utf8");
5797
+ fs14.fchmodSync(fd, mode);
5798
+ fs14.fsyncSync(fd);
4705
5799
  } finally {
4706
- fs12.closeSync(fd);
5800
+ fs14.closeSync(fd);
4707
5801
  }
4708
5802
  };
4709
5803
  var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
5804
+ var optionalFileKeys = ["posture", "edge", "caddyfile"];
4710
5805
  var fileNames = {
4711
5806
  identity: "identity.env",
4712
5807
  config: "server.json",
4713
5808
  env: "server.env",
4714
5809
  compose: "compose.yaml",
4715
- postgresInit: "postgres-init.sh"
4716
- };
5810
+ postgresInit: "postgres-init.sh",
5811
+ posture: "compose.posture.yaml",
5812
+ edge: "compose.edge.yaml",
5813
+ caddyfile: "Caddyfile"
5814
+ };
5815
+ var fileModes = {
5816
+ identity: 384,
5817
+ config: 384,
5818
+ env: 384,
5819
+ compose: 384,
5820
+ postgresInit: 493,
5821
+ posture: 384,
5822
+ edge: 384,
5823
+ caddyfile: 420
5824
+ };
5825
+ var allFiles = (files) => [...fileKeys, ...optionalFileKeys].map((key) => [key, files[key]]);
4717
5826
  var parseLockOwner = (raw) => {
4718
5827
  if (typeof raw !== "object" || raw === null || !("pid" in raw) || !("hostname" in raw))
4719
5828
  return null;
@@ -4723,11 +5832,11 @@ var parseLockOwner = (raw) => {
4723
5832
  };
4724
5833
  var describeOwner = (lockDir) => {
4725
5834
  try {
4726
- const raw = JSON.parse(fs12.readFileSync(path12.join(lockDir, "owner.json"), "utf8"));
5835
+ const raw = JSON.parse(fs14.readFileSync(path15.join(lockDir, "owner.json"), "utf8"));
4727
5836
  const owner = parseLockOwner(raw);
4728
5837
  if (owner === null) return "owner metadata is invalid";
4729
5838
  const label = `PID ${owner.pid} on ${owner.hostname}`;
4730
- if (owner.hostname !== os7.hostname()) return label;
5839
+ if (owner.hostname !== os9.hostname()) return label;
4731
5840
  try {
4732
5841
  process.kill(owner.pid, 0);
4733
5842
  return `${label} is still live`;
@@ -4743,28 +5852,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
4743
5852
  );
4744
5853
  var acquireLock = (configDir, create) => {
4745
5854
  if (create) {
4746
- fs12.mkdirSync(configDir, { recursive: true, mode: 448 });
4747
- fs12.chmodSync(configDir, 448);
4748
- } else if (!fs12.existsSync(configDir)) {
5855
+ fs14.mkdirSync(configDir, { recursive: true, mode: 448 });
5856
+ fs14.chmodSync(configDir, 448);
5857
+ } else if (!fs14.existsSync(configDir)) {
4749
5858
  throw new ServerStoreError(
4750
5859
  "No Mend server is configured. Run mend server setup explicitly to install one."
4751
5860
  );
4752
5861
  }
4753
- const lockDir = path12.join(configDir, "server.lock");
5862
+ const lockDir = path15.join(configDir, "server.lock");
4754
5863
  try {
4755
- fs12.mkdirSync(lockDir, { mode: 448 });
5864
+ fs14.mkdirSync(lockDir, { mode: 448 });
4756
5865
  } catch (cause) {
4757
5866
  if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
4758
5867
  throw cause;
4759
5868
  }
4760
- const owned = fs12.statSync(lockDir);
4761
- const ownerFile = path12.join(lockDir, "owner.json");
4762
- const owner = `${JSON.stringify({ pid: process.pid, hostname: os7.hostname(), token: randomUUID() })}
5869
+ const owned = fs14.statSync(lockDir);
5870
+ const ownerFile = path15.join(lockDir, "owner.json");
5871
+ const owner = `${JSON.stringify({ pid: process.pid, hostname: os9.hostname(), token: randomUUID2() })}
4763
5872
  `;
4764
5873
  let open = true;
4765
5874
  const assertOwned = () => {
4766
- const current = fs12.statSync(lockDir);
4767
- if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs12.readFileSync(ownerFile, "utf8") !== owner) {
5875
+ const current = fs14.statSync(lockDir);
5876
+ if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs14.readFileSync(ownerFile, "utf8") !== owner) {
4768
5877
  throw new ServerStoreError(
4769
5878
  "Server lock ownership was lost. Stop and check for another server command before retrying."
4770
5879
  );
@@ -4780,8 +5889,8 @@ var acquireLock = (configDir, create) => {
4780
5889
  release: () => {
4781
5890
  const result = attempt(() => {
4782
5891
  assertOwned();
4783
- fs12.unlinkSync(ownerFile);
4784
- fs12.rmdirSync(lockDir);
5892
+ fs14.unlinkSync(ownerFile);
5893
+ fs14.rmdirSync(lockDir);
4785
5894
  });
4786
5895
  open = false;
4787
5896
  return result;
@@ -4790,7 +5899,7 @@ var acquireLock = (configDir, create) => {
4790
5899
  };
4791
5900
  var readIdentity = (paths) => {
4792
5901
  const identity = readOptional(paths.identity);
4793
- if (identity === null && (fs12.existsSync(paths.active) || fs12.existsSync(paths.generations) && fs12.readdirSync(paths.generations).length > 0)) {
5902
+ if (identity === null && (fs14.existsSync(paths.active) || fs14.existsSync(paths.generations) && fs14.readdirSync(paths.generations).length > 0)) {
4794
5903
  throw new ServerStoreError(
4795
5904
  "Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
4796
5905
  );
@@ -4800,7 +5909,7 @@ var readIdentity = (paths) => {
4800
5909
  var readActive = (paths) => {
4801
5910
  let target;
4802
5911
  try {
4803
- target = fs12.readlinkSync(paths.active);
5912
+ target = fs14.readlinkSync(paths.active);
4804
5913
  } catch (cause) {
4805
5914
  if (hasCode(cause, "ENOENT")) return null;
4806
5915
  throw cause;
@@ -4810,13 +5919,20 @@ var readActive = (paths) => {
4810
5919
  "Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
4811
5920
  );
4812
5921
  }
4813
- const directory = path12.join(paths.configDir, target);
5922
+ const directory = path15.join(paths.configDir, target);
5923
+ const optional = (key) => readOptional(path15.join(directory, fileNames[key])) ?? void 0;
5924
+ const posture = optional("posture");
5925
+ const edge = optional("edge");
5926
+ const caddyfile = optional("caddyfile");
4814
5927
  const files = {
4815
- identity: fs12.readFileSync(path12.join(directory, fileNames.identity), "utf8"),
4816
- config: fs12.readFileSync(path12.join(directory, fileNames.config), "utf8"),
4817
- env: fs12.readFileSync(path12.join(directory, fileNames.env), "utf8"),
4818
- compose: fs12.readFileSync(path12.join(directory, fileNames.compose), "utf8"),
4819
- postgresInit: fs12.readFileSync(path12.join(directory, fileNames.postgresInit), "utf8")
5928
+ identity: fs14.readFileSync(path15.join(directory, fileNames.identity), "utf8"),
5929
+ config: fs14.readFileSync(path15.join(directory, fileNames.config), "utf8"),
5930
+ env: fs14.readFileSync(path15.join(directory, fileNames.env), "utf8"),
5931
+ compose: fs14.readFileSync(path15.join(directory, fileNames.compose), "utf8"),
5932
+ postgresInit: fs14.readFileSync(path15.join(directory, fileNames.postgresInit), "utf8"),
5933
+ ...posture === void 0 ? {} : { posture },
5934
+ ...edge === void 0 ? {} : { edge },
5935
+ ...caddyfile === void 0 ? {} : { caddyfile }
4820
5936
  };
4821
5937
  if (readIdentity(paths) !== files.identity) {
4822
5938
  throw new ServerStoreError(
@@ -4826,23 +5942,20 @@ var readActive = (paths) => {
4826
5942
  return { directory, files };
4827
5943
  };
4828
5944
  var publishIdentity = (paths, identity) => {
4829
- const temporary = path12.join(paths.configDir, `.identity-${randomUUID()}`);
5945
+ const temporary = path15.join(paths.configDir, `.identity-${randomUUID2()}`);
4830
5946
  writeDurable(temporary, identity);
4831
- fs12.linkSync(temporary, paths.identity);
5947
+ fs14.linkSync(temporary, paths.identity);
4832
5948
  syncDirectory(paths.configDir);
4833
- fs12.unlinkSync(temporary);
5949
+ fs14.unlinkSync(temporary);
4834
5950
  };
4835
5951
  var prepareGeneration = (paths, files) => {
4836
- fs12.mkdirSync(paths.generations, { recursive: true, mode: 448 });
4837
- const generationName = `gen-${randomUUID()}`;
4838
- const directory = path12.join(paths.generations, generationName);
4839
- fs12.mkdirSync(directory, { mode: 448 });
4840
- for (const key of fileKeys) {
4841
- writeDurable(
4842
- path12.join(directory, fileNames[key]),
4843
- files[key],
4844
- key === "postgresInit" ? 493 : 384
4845
- );
5952
+ fs14.mkdirSync(paths.generations, { recursive: true, mode: 448 });
5953
+ const generationName = `gen-${randomUUID2()}`;
5954
+ const directory = path15.join(paths.generations, generationName);
5955
+ fs14.mkdirSync(directory, { mode: 448 });
5956
+ for (const [key, content] of allFiles(files)) {
5957
+ if (content === void 0) continue;
5958
+ writeDurable(path15.join(directory, fileNames[key]), content, fileModes[key]);
4846
5959
  }
4847
5960
  syncDirectory(directory);
4848
5961
  syncDirectory(paths.generations);
@@ -4850,18 +5963,18 @@ var prepareGeneration = (paths, files) => {
4850
5963
  return { directory, files };
4851
5964
  };
4852
5965
  var activateGeneration = (paths, generation) => {
4853
- const relative4 = path12.relative(paths.configDir, generation.directory);
4854
- if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative4) || readIdentity(paths) !== fs12.readFileSync(path12.join(generation.directory, "identity.env"), "utf8")) {
5966
+ const relative5 = path15.relative(paths.configDir, generation.directory);
5967
+ if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative5) || readIdentity(paths) !== fs14.readFileSync(path15.join(generation.directory, "identity.env"), "utf8")) {
4855
5968
  throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
4856
5969
  }
4857
- for (const key of fileKeys) {
4858
- if (fs12.readFileSync(path12.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
5970
+ for (const [key, content] of allFiles(generation.files)) {
5971
+ if (readOptional(path15.join(generation.directory, fileNames[key])) !== (content ?? null)) {
4859
5972
  throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
4860
5973
  }
4861
5974
  }
4862
- const pointer = path12.join(paths.configDir, `.active-${randomUUID()}`);
4863
- fs12.symlinkSync(relative4, pointer);
4864
- fs12.renameSync(pointer, paths.active);
5975
+ const pointer = path15.join(paths.configDir, `.active-${randomUUID2()}`);
5976
+ fs14.symlinkSync(relative5, pointer);
5977
+ fs14.renameSync(pointer, paths.active);
4865
5978
  syncDirectory(paths.configDir);
4866
5979
  };
4867
5980
  var prepareFiles = (paths, files) => {
@@ -4870,7 +5983,7 @@ var prepareFiles = (paths, files) => {
4870
5983
  throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
4871
5984
  }
4872
5985
  const active = readActive(paths);
4873
- if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs12.statSync(path12.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
5986
+ if (active !== null && allFiles(files).every(([key, content]) => content === active.files[key]) && (fs14.statSync(path15.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
4874
5987
  return active;
4875
5988
  if (identity === null) publishIdentity(paths, files.identity);
4876
5989
  return prepareGeneration(paths, files);
@@ -4883,9 +5996,9 @@ var commitGeneration = (paths, files) => {
4883
5996
  var createStore = (configDir, lock) => {
4884
5997
  const paths = {
4885
5998
  configDir,
4886
- identity: path12.join(configDir, "identity.env"),
4887
- generations: path12.join(configDir, "generations"),
4888
- active: path12.join(configDir, "active")
5999
+ identity: path15.join(configDir, "identity.env"),
6000
+ generations: path15.join(configDir, "generations"),
6001
+ active: path15.join(configDir, "active")
4889
6002
  };
4890
6003
  const whileOwned = (operation) => attempt(() => {
4891
6004
  lock.assertOwned();
@@ -4898,13 +6011,13 @@ var createStore = (configDir, lock) => {
4898
6011
  prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
4899
6012
  activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
4900
6013
  createBackup: (previous, target) => whileOwned(() => {
4901
- const backups = path12.join(configDir, "backups");
4902
- fs12.mkdirSync(backups, { recursive: true, mode: 448 });
4903
- fs12.chmodSync(backups, 448);
4904
- const directory = path12.join(backups, `upgrade-${randomUUID()}`);
4905
- fs12.mkdirSync(directory, { mode: 448 });
6014
+ const backups = path15.join(configDir, "backups");
6015
+ fs14.mkdirSync(backups, { recursive: true, mode: 448 });
6016
+ fs14.chmodSync(backups, 448);
6017
+ const directory = path15.join(backups, `upgrade-${randomUUID2()}`);
6018
+ fs14.mkdirSync(directory, { mode: 448 });
4906
6019
  writeDurable(
4907
- path12.join(directory, "recovery.json"),
6020
+ path15.join(directory, "recovery.json"),
4908
6021
  `${JSON.stringify(
4909
6022
  {
4910
6023
  previousGeneration: previous.directory,
@@ -4920,21 +6033,21 @@ var createStore = (configDir, lock) => {
4920
6033
  syncDirectory(directory);
4921
6034
  syncDirectory(backups);
4922
6035
  syncDirectory(configDir);
4923
- const partialFile = path12.join(directory, "database.sql.partial");
6036
+ const partialFile = path15.join(directory, "database.sql.partial");
4924
6037
  return {
4925
6038
  directory,
4926
6039
  partialFile,
4927
6040
  complete: () => whileOwned(() => {
4928
- if (fs12.statSync(partialFile).size === 0)
6041
+ if (fs14.statSync(partialFile).size === 0)
4929
6042
  throw new ServerStoreError("Database backup is empty; refusing target startup.");
4930
- const fd = fs12.openSync(partialFile, "r");
6043
+ const fd = fs14.openSync(partialFile, "r");
4931
6044
  try {
4932
- fs12.fchmodSync(fd, 384);
4933
- fs12.fsyncSync(fd);
6045
+ fs14.fchmodSync(fd, 384);
6046
+ fs14.fsyncSync(fd);
4934
6047
  } finally {
4935
- fs12.closeSync(fd);
6048
+ fs14.closeSync(fd);
4936
6049
  }
4937
- fs12.renameSync(partialFile, path12.join(directory, "database.sql"));
6050
+ fs14.renameSync(partialFile, path15.join(directory, "database.sql"));
4938
6051
  syncDirectory(directory);
4939
6052
  })
4940
6053
  };
@@ -4943,7 +6056,7 @@ var createStore = (configDir, lock) => {
4943
6056
  };
4944
6057
  var refuseFlatLayout = (configDir) => {
4945
6058
  if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
4946
- (name) => fs12.existsSync(path12.join(configDir, name))
6059
+ (name) => fs14.existsSync(path15.join(configDir, name))
4947
6060
  )) {
4948
6061
  throw new ServerStoreError(
4949
6062
  "Unreleased flat server configuration found. Preserve its credentials and volumes; migrate it to the generation layout before retrying. Setup will not replace this identity."
@@ -4951,7 +6064,7 @@ var refuseFlatLayout = (configDir) => {
4951
6064
  }
4952
6065
  };
4953
6066
  var withServerStore = async (directory, operation, options = {}) => {
4954
- const configDir = path12.resolve(directory);
6067
+ const configDir = path15.resolve(directory);
4955
6068
  let lock;
4956
6069
  let result;
4957
6070
  try {
@@ -4968,9 +6081,9 @@ var withServerStore = async (directory, operation, options = {}) => {
4968
6081
  };
4969
6082
 
4970
6083
  // src/version.ts
4971
- import * as fs13 from "node:fs";
6084
+ import * as fs15 from "node:fs";
4972
6085
  var cliVersion = () => {
4973
- const raw = fs13.readFileSync(new URL("../package.json", import.meta.url), "utf8");
6086
+ const raw = fs15.readFileSync(new URL("../package.json", import.meta.url), "utf8");
4974
6087
  const parsed = JSON.parse(raw);
4975
6088
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
4976
6089
  const version = parsed.version;
@@ -5079,8 +6192,31 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
5079
6192
  "--ssh-port",
5080
6193
  "--docker-socket",
5081
6194
  "--assets-dir",
5082
- "--offline"
6195
+ "--offline",
6196
+ "--edge",
6197
+ "--no-edge",
6198
+ "--exposure",
6199
+ "--tenancy"
5083
6200
  ]);
6201
+ var parseExposure = (value) => {
6202
+ if (!isExposure(value))
6203
+ throw setupError(`--exposure must be one of ${EXPOSURES.join(", ")}, not "${value}".`);
6204
+ return value;
6205
+ };
6206
+ var parseTenancy = (value) => {
6207
+ if (!isTenancy(value))
6208
+ throw setupError(`--tenancy must be one of ${TENANCIES.join(", ")}, not "${value}".`);
6209
+ return value;
6210
+ };
6211
+ var parseEdge = (value) => {
6212
+ const host = parseEdgeHost(value);
6213
+ if (host === null) {
6214
+ throw setupError(
6215
+ `--edge must be a DNS name a certificate can be issued for, such as mend.example.com, not "${value}". An IP address or a single label cannot carry a public certificate.`
6216
+ );
6217
+ }
6218
+ return host;
6219
+ };
5084
6220
  var parseSetupOptions = (args) => {
5085
6221
  const values = /* @__PURE__ */ new Map();
5086
6222
  for (let index = 0; index < args.length; index += 1) {
@@ -5088,8 +6224,8 @@ var parseSetupOptions = (args) => {
5088
6224
  if (flag === void 0) continue;
5089
6225
  if (!flag.startsWith("--")) throw setupError(`Unexpected server setup argument "${flag}".`);
5090
6226
  if (!SETUP_FLAGS.has(flag)) throw setupError(`Unknown server setup option "${flag}".`);
5091
- if (flag === "--offline") {
5092
- if (values.has(flag)) throw setupError("--offline may be supplied only once.");
6227
+ if (flag === "--offline" || flag === "--no-edge") {
6228
+ if (values.has(flag)) throw setupError(`${flag} may be supplied only once.`);
5093
6229
  values.set(flag, ["true"]);
5094
6230
  continue;
5095
6231
  }
@@ -5107,6 +6243,11 @@ var parseSetupOptions = (args) => {
5107
6243
  const appPort = flagValue3("--port");
5108
6244
  const sshPort = flagValue3("--ssh-port");
5109
6245
  const origins = values.get("--origin");
6246
+ const edge = flagValue3("--edge");
6247
+ const exposure = flagValue3("--exposure");
6248
+ const tenancy = flagValue3("--tenancy");
6249
+ if (edge !== void 0 && values.has("--no-edge"))
6250
+ throw setupError("--edge and --no-edge contradict each other.");
5110
6251
  return {
5111
6252
  context: flagValue3("--context"),
5112
6253
  version: version === void 0 ? void 0 : parseVersion(version),
@@ -5117,7 +6258,11 @@ var parseSetupOptions = (args) => {
5117
6258
  sshPort: sshPort === void 0 ? void 0 : parsePort(sshPort, "--ssh-port"),
5118
6259
  dockerSocket: flagValue3("--docker-socket"),
5119
6260
  assetsDir: flagValue3("--assets-dir"),
5120
- offline: values.has("--offline")
6261
+ offline: values.has("--offline"),
6262
+ edge: edge === void 0 ? void 0 : parseEdge(edge),
6263
+ noEdge: values.has("--no-edge"),
6264
+ exposure: exposure === void 0 ? void 0 : parseExposure(exposure),
6265
+ tenancy: tenancy === void 0 ? void 0 : parseTenancy(tenancy)
5121
6266
  };
5122
6267
  };
5123
6268
  var parseUrl = (input, label) => {
@@ -5140,11 +6285,20 @@ var parseHttpOrigin = (input, label) => {
5140
6285
  return parsed.origin;
5141
6286
  };
5142
6287
  var isLoopbackBind = (bind) => bind === "127.0.0.1" || bind === "::1" || bind.startsWith("127.");
5143
- var isLoopbackHost = (hostname7) => hostname7 === "localhost" || hostname7 === "::1" || hostname7 === "[::1]" || hostname7.startsWith("127.");
5144
- var resolveAppUrl = (existing, options, appPort) => {
6288
+ var isLoopbackHost = (hostname8) => hostname8 === "localhost" || hostname8 === "::1" || hostname8 === "[::1]" || hostname8.startsWith("127.");
6289
+ var resolveAppUrl = (existing, options, appPort, edgeHost) => {
6290
+ if (edgeHost !== void 0) {
6291
+ const origin = parseHttpOrigin(options.url ?? `https://${edgeHost}`, "--url");
6292
+ if (origin !== `https://${edgeHost}`) {
6293
+ throw setupError(
6294
+ `With --edge ${edgeHost}, --url must be https://${edgeHost}, the origin the edge serves.`
6295
+ );
6296
+ }
6297
+ return origin;
6298
+ }
5145
6299
  const oldLocalUrl = existing === null ? null : `http://localhost:${existing.appPort}`;
5146
6300
  const portChangedOnLocalhost = options.appPort !== void 0 && existing?.appUrl === oldLocalUrl;
5147
- const fallback = portChangedOnLocalhost ? `http://localhost:${appPort}` : existing?.appUrl ?? `http://localhost:${appPort}`;
6301
+ const fallback = portChangedOnLocalhost || existing?.edgeHost !== void 0 ? `http://localhost:${appPort}` : existing?.appUrl ?? `http://localhost:${appPort}`;
5148
6302
  return parseHttpOrigin(options.url ?? fallback, "--url");
5149
6303
  };
5150
6304
  var checkExposurePair = (bind, appUrl, requireExplicitUrl) => {
@@ -5159,6 +6313,33 @@ var checkExposurePair = (bind, appUrl, requireExplicitUrl) => {
5159
6313
  );
5160
6314
  }
5161
6315
  };
6316
+ var checkPosture = (bind, ports, edgeHost, exposure) => {
6317
+ if (edgeHost !== void 0 && [ports.appPort, ports.sshPort].some((p) => p === 80 || p === 443)) {
6318
+ throw setupError(
6319
+ "With an edge, --port and --ssh-port must not be 80 or 443: the edge publishes both on every interface."
6320
+ );
6321
+ }
6322
+ if (edgeHost !== void 0 && !isLoopbackBind(bind)) {
6323
+ throw setupError(
6324
+ `With an edge, --bind stays on loopback, ${DEFAULT_BIND}: the edge publishes 80 and 443, and Mend's own port is reached through it alone.`
6325
+ );
6326
+ }
6327
+ if (edgeHost !== void 0 && exposure === "loopback") {
6328
+ throw setupError(
6329
+ "--exposure loopback and an edge contradict each other: the edge publishes 80 and 443 on every interface."
6330
+ );
6331
+ }
6332
+ if (exposure === "loopback" && !isLoopbackBind(bind)) {
6333
+ throw setupError(
6334
+ "--exposure loopback contradicts a non-loopback --bind: the port is published beyond this machine."
6335
+ );
6336
+ }
6337
+ if (exposure === "public" && edgeHost === void 0) {
6338
+ throw setupError(
6339
+ "--exposure public needs the edge: add --edge <host>. Without it nothing sets MEND_TRUSTED_PROXIES or an https origin, and the server refuses to start as public."
6340
+ );
6341
+ }
6342
+ };
5162
6343
  var validateExposure = (existing, options) => {
5163
6344
  const appPort = options.appPort ?? existing?.appPort ?? DEFAULT_APP_PORT;
5164
6345
  const sshPort = options.sshPort ?? existing?.sshPort ?? DEFAULT_SSH_PORT;
@@ -5166,19 +6347,33 @@ var validateExposure = (existing, options) => {
5166
6347
  throw setupError("--port and --ssh-port must use different ports.");
5167
6348
  }
5168
6349
  const bind = options.bind ?? existing?.bind ?? DEFAULT_BIND;
5169
- if (net2.isIP(bind) === 0) {
6350
+ if (net3.isIP(bind) === 0) {
5170
6351
  throw setupError(
5171
6352
  "--bind must be a literal IPv4 or IPv6 address, such as 127.0.0.1 or 0.0.0.0."
5172
6353
  );
5173
6354
  }
5174
- const appUrl = resolveAppUrl(existing, options, appPort);
5175
- checkExposurePair(bind, appUrl, options.url === void 0 && existing === null);
6355
+ const edgeHost = options.noEdge ? void 0 : options.edge ?? existing?.edgeHost;
6356
+ const exposure = options.exposure ?? existing?.exposure;
6357
+ const tenancy = options.tenancy ?? existing?.tenancy;
6358
+ checkPosture(bind, { appPort, sshPort }, edgeHost, exposure);
6359
+ const appUrl = resolveAppUrl(existing, options, appPort, edgeHost);
6360
+ if (edgeHost === void 0)
6361
+ checkExposurePair(bind, appUrl, options.url === void 0 && existing === null);
5176
6362
  const requestedOrigins = options.origins?.map((origin) => parseHttpOrigin(origin, "--origin"));
5177
6363
  const inheritedOrigins = existing?.allowedOrigins ?? [];
5178
6364
  const allowedOrigins = [
5179
6365
  ...new Set((requestedOrigins ?? inheritedOrigins).filter((origin) => origin !== appUrl))
5180
6366
  ];
5181
- return { bind, appUrl, allowedOrigins, appPort, sshPort };
6367
+ return {
6368
+ bind,
6369
+ appUrl,
6370
+ allowedOrigins,
6371
+ appPort,
6372
+ sshPort,
6373
+ ...edgeHost === void 0 ? {} : { edgeHost },
6374
+ ...exposure === void 0 ? {} : { exposure },
6375
+ ...tenancy === void 0 ? {} : { tenancy }
6376
+ };
5182
6377
  };
5183
6378
  var parseServerConfig = (raw) => {
5184
6379
  let decoded;
@@ -5204,6 +6399,18 @@ var parseServerConfig = (raw) => {
5204
6399
  if (dockerSocketSource !== "detected" && dockerSocketSource !== "override") {
5205
6400
  throw setupError("Server config is corrupt: dockerSocketSource must be detected or override.");
5206
6401
  }
6402
+ const edgeHost = fields.has("edgeHost") ? requiredString(fields, "edgeHost") : void 0;
6403
+ if (edgeHost !== void 0 && parseEdgeHost(edgeHost) !== edgeHost) {
6404
+ throw setupError("Server config is corrupt: edgeHost must be a lowercase DNS name.");
6405
+ }
6406
+ const exposure = fields.has("exposure") ? requiredString(fields, "exposure") : void 0;
6407
+ if (exposure !== void 0 && !isExposure(exposure)) {
6408
+ throw setupError(`Server config is corrupt: exposure must be one of ${EXPOSURES.join(", ")}.`);
6409
+ }
6410
+ const tenancy = fields.has("tenancy") ? requiredString(fields, "tenancy") : void 0;
6411
+ if (tenancy !== void 0 && !isTenancy(tenancy)) {
6412
+ throw setupError(`Server config is corrupt: tenancy must be one of ${TENANCIES.join(", ")}.`);
6413
+ }
5207
6414
  const config = {
5208
6415
  schemaVersion: requiredInteger(fields, "schemaVersion"),
5209
6416
  assetContract: requiredString(fields, "assetContract"),
@@ -5220,7 +6427,10 @@ var parseServerConfig = (raw) => {
5220
6427
  appPort: requiredInteger(fields, "appPort"),
5221
6428
  sshPort: requiredInteger(fields, "sshPort"),
5222
6429
  ...fields.has("registryPort") ? { registryPort: requiredInteger(fields, "registryPort") } : {},
5223
- ...fields.has("bucket") ? { bucket: requiredBucket(fields) } : {}
6430
+ ...fields.has("bucket") ? { bucket: requiredBucket(fields) } : {},
6431
+ ...edgeHost === void 0 ? {} : { edgeHost },
6432
+ ...exposure === void 0 ? {} : { exposure },
6433
+ ...tenancy === void 0 ? {} : { tenancy }
5224
6434
  };
5225
6435
  if (config.schemaVersion !== CONFIG_SCHEMA_VERSION || config.assetContract !== ASSET_CONTRACT && !LEGACY_ASSET_CONTRACTS.has(config.assetContract)) {
5226
6436
  throw setupError(
@@ -5241,7 +6451,11 @@ var parseServerConfig = (raw) => {
5241
6451
  sshPort: config.sshPort,
5242
6452
  dockerSocket: void 0,
5243
6453
  assetsDir: void 0,
5244
- offline: false
6454
+ offline: false,
6455
+ edge: config.edgeHost,
6456
+ noEdge: false,
6457
+ exposure: config.exposure,
6458
+ tenancy: config.tenancy
5245
6459
  });
5246
6460
  return config;
5247
6461
  };
@@ -5334,10 +6548,8 @@ var parseContextRows = (raw) => {
5334
6548
  return rows;
5335
6549
  };
5336
6550
  var localUnixEndpoint = (endpoint) => endpoint.startsWith("unix:///");
5337
- var commandFailure = (label, output) => {
5338
- const detail = output.stderr.trim() || output.error || `exit ${output.status ?? "unknown"}`;
5339
- return setupError(`${label}: ${detail}`);
5340
- };
6551
+ var outputDetail = (output) => output.stderr.trim() || output.error || `exit ${output.status ?? "unknown"}`;
6552
+ var commandFailure = (label, output) => setupError(`${label}: ${outputDetail(output)}`);
5341
6553
  var selectDockerContext = async (runtime, requested) => {
5342
6554
  if (requested !== void 0) {
5343
6555
  const inspect = await runtime.run("docker", [
@@ -5420,6 +6632,32 @@ var checkDocker = async (runtime, context) => {
5420
6632
  }
5421
6633
  return info.stdout.trim();
5422
6634
  };
6635
+ var hostUserNamespacesLine = async (runtime, context, image) => {
6636
+ const read = HOST_USER_NAMESPACE_FILES.map(
6637
+ (file) => `printf '%s|' "$(cat ${file} 2>/dev/null || echo -)"`
6638
+ ).join("; ");
6639
+ const probe = await runtime.run("docker", [
6640
+ "--context",
6641
+ context,
6642
+ "run",
6643
+ "--rm",
6644
+ "--network",
6645
+ "none",
6646
+ "--entrypoint",
6647
+ "sh",
6648
+ image,
6649
+ "-c",
6650
+ read
6651
+ ]);
6652
+ if (probe.status !== 0) return null;
6653
+ const [restrict, apparmor, clone] = probe.stdout.trim().split("|").map((value) => value.trim() === "-" ? null : value);
6654
+ const observed = hostUserNamespacesOf({
6655
+ apparmorRestrictUnprivilegedUserns: restrict ?? null,
6656
+ apparmorEnabled: apparmor ?? null,
6657
+ unprivilegedUsernsClone: clone ?? null
6658
+ });
6659
+ return observed.allowed ? null : `No session can start on this Docker host yet: its kernel refuses unprivileged user namespaces, which each workspace's rootless Docker service needs. On the host, run: ${hostUserNamespacesFix(observed.setting)}`;
6660
+ };
5423
6661
  var resolveLatestVersion = async (runtime) => {
5424
6662
  runtime.writeLine("Resolving the latest Mend server release");
5425
6663
  const response = await runtime.fetchText(LATEST_RELEASE_URL, 15e3);
@@ -5556,7 +6794,7 @@ var renderGarage = (secrets, config) => {
5556
6794
  var namespaceOf = (config) => config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
5557
6795
  var renderSecrets = (secrets, config) => {
5558
6796
  const sshHost = parseUrl(config.appUrl, "Server config appUrl").hostname.replace(/^\[|\]$/g, "");
5559
- const composeBind = net2.isIP(config.bind) === 6 ? `[${config.bind}]` : config.bind;
6797
+ const composeBind = net3.isIP(config.bind) === 6 ? `[${config.bind}]` : config.bind;
5560
6798
  return [
5561
6799
  `MEND_VERSION=${config.serverVersion}`,
5562
6800
  "MEND_IMAGE_REPOSITORY=ghcr.io/sealant-sh/mend",
@@ -5568,6 +6806,9 @@ var renderSecrets = (secrets, config) => {
5568
6806
  `MEND_SSH_PORT=${config.sshPort}`,
5569
6807
  ...config.registryPort === void 0 ? [] : [`MEND_REGISTRY_PORT=${config.registryPort}`],
5570
6808
  `SEALANT_SSH_HOST=${sshHost}`,
6809
+ // The edge's host and the posture (server-edge.ts): compose.edge.yaml and
6810
+ // compose.posture.yaml read them from here, and only from here.
6811
+ ...postureEnvLines(config),
5571
6812
  "MEND_STORE_VOLUME_NAME=mend-store",
5572
6813
  "MEND_CONTROL_VOLUME_NAME=mend-control",
5573
6814
  ...renderGarage(secrets, config),
@@ -5579,6 +6820,48 @@ var storeValue = (result) => {
5579
6820
  if (result._tag === "error") throw result.error;
5580
6821
  return result.value;
5581
6822
  };
6823
+ var composeTarget = (installation) => ({
6824
+ directory: installation.directory,
6825
+ dockerContext: installation.config.dockerContext,
6826
+ overlays: composeOverlays(installation.config)
6827
+ });
6828
+ var edgeImageOf = (overlay) => {
6829
+ const image = /^ {4}image:[ \t]*(caddy:[^\s#]+)[ \t]*$/m.exec(overlay)?.[1];
6830
+ if (image === void 0)
6831
+ throw setupError("Server generation is corrupt: compose.edge.yaml pins no Caddy image.");
6832
+ return image;
6833
+ };
6834
+ var validateOverlays = (config, files) => {
6835
+ const posture = renderPostureOverlay(config);
6836
+ if (posture === void 0 !== (files.posture === void 0)) {
6837
+ throw setupError(
6838
+ "Server generation is corrupt: compose.posture.yaml does not match the persisted server config."
6839
+ );
6840
+ }
6841
+ for (const line of postureEnvLines(config)) {
6842
+ const key = line.slice(0, line.indexOf("="));
6843
+ if (key !== "MEND_EDGE_HOST" && !(files.posture ?? "").includes(` ${key}: \${${key}`)) {
6844
+ throw setupError(
6845
+ `Server generation is corrupt: compose.posture.yaml does not hand ${key} to Mend.`
6846
+ );
6847
+ }
6848
+ }
6849
+ if (config.edgeHost === void 0 !== (files.edge === void 0) || config.edgeHost === void 0 !== (files.caddyfile === void 0)) {
6850
+ throw setupError(
6851
+ "Server generation is corrupt: compose.edge.yaml and Caddyfile do not match the persisted server config."
6852
+ );
6853
+ }
6854
+ if (files.edge === void 0 || files.caddyfile === void 0) return void 0;
6855
+ if (!files.edge.includes("\n edge:") || !files.edge.includes("MEND_EDGE_HOST") || !files.edge.includes("./Caddyfile:/etc/caddy/Caddyfile")) {
6856
+ throw setupError("Server generation is corrupt: compose.edge.yaml is not the edge overlay.");
6857
+ }
6858
+ if (!files.caddyfile.includes("{$MEND_EDGE_HOST}") || !files.caddyfile.includes("reverse_proxy mend:3105")) {
6859
+ throw setupError(
6860
+ "Server generation is corrupt: the Caddyfile does not route the edge host to Mend."
6861
+ );
6862
+ }
6863
+ return edgeImageOf(files.edge);
6864
+ };
5582
6865
  var readServerInstallation = (store) => {
5583
6866
  try {
5584
6867
  const generation = storeValue(store.readActive());
@@ -5592,7 +6875,15 @@ var readServerInstallation = (store) => {
5592
6875
  }
5593
6876
  validateComposeAsset(generation.files.compose);
5594
6877
  validatePostgresAsset(generation.files.postgresInit);
5595
- return { _tag: "ok", value: { config, directory: generation.directory } };
6878
+ const edgeImage = validateOverlays(config, generation.files);
6879
+ return {
6880
+ _tag: "ok",
6881
+ value: {
6882
+ config,
6883
+ directory: generation.directory,
6884
+ ...edgeImage === void 0 ? {} : { edgeImage }
6885
+ }
6886
+ };
5596
6887
  } catch (cause) {
5597
6888
  return {
5598
6889
  _tag: "error",
@@ -5618,6 +6909,11 @@ var readServerInstallationFacts = async (configDir) => {
5618
6909
  directory: installation.directory,
5619
6910
  config: installation.config,
5620
6911
  compose: generation.files.compose,
6912
+ overlays: [
6913
+ ...generation.files.edge === void 0 ? [] : [{ name: "compose.edge.yaml", content: generation.files.edge }],
6914
+ ...generation.files.caddyfile === void 0 ? [] : [{ name: "Caddyfile", content: generation.files.caddyfile }],
6915
+ ...generation.files.posture === void 0 ? [] : [{ name: "compose.posture.yaml", content: generation.files.posture }]
6916
+ ],
5621
6917
  envKeys: envKeyNames(generation.files.env)
5622
6918
  };
5623
6919
  },
@@ -5626,15 +6922,25 @@ var readServerInstallationFacts = async (configDir) => {
5626
6922
  if (result._tag === "error") throw result.error;
5627
6923
  return result.value;
5628
6924
  };
5629
- var persistSetup = (store, config, secrets, assets) => storeValue(
5630
- store.prepare({
5631
- identity: renderIdentity(secrets),
6925
+ var generationFiles = (config, secrets, assets, identity = renderIdentity(secrets)) => {
6926
+ const posture = renderPostureOverlay(config);
6927
+ return {
6928
+ identity,
5632
6929
  config: `${JSON.stringify(config, null, 2)}
5633
6930
  `,
5634
6931
  env: renderSecrets(secrets, config),
5635
- ...assets
5636
- })
5637
- );
6932
+ ...assets,
6933
+ ...posture === void 0 ? {} : { posture },
6934
+ ...config.edgeHost === void 0 ? {} : { edge: EDGE_COMPOSE_OVERLAY, caddyfile: EDGE_CADDYFILE }
6935
+ };
6936
+ };
6937
+ var persistSetup = (store, config, secrets, assets) => storeValue(store.prepare(generationFiles(config, secrets, assets)));
6938
+ var healthOrigin = (config) => {
6939
+ if (config.edgeHost === void 0) return config.appUrl;
6940
+ const host = net3.isIP(config.bind) === 6 ? `[${config.bind}]` : config.bind;
6941
+ return `http://${host}:${config.appPort}`;
6942
+ };
6943
+ var reachableLine = (config) => config.edgeHost === void 0 ? `Mend ${config.serverVersion} is reachable at ${config.appUrl}` : `Mend ${config.serverVersion} answers at ${healthOrigin(config)} on this machine \xB7 the edge is set up for ${config.appUrl}`;
5638
6944
  var probeHealth = async (runtime, appUrl, expectedVersion) => {
5639
6945
  const healthUrl = `${appUrl}/api/health`;
5640
6946
  let lastFailure = "request did not complete";
@@ -5673,7 +6979,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
5673
6979
  return version;
5674
6980
  };
5675
6981
  var parseDockerSocketPath = (socket) => {
5676
- if (!path13.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
6982
+ if (!path16.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
5677
6983
  throw setupError(
5678
6984
  "--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
5679
6985
  );
@@ -5699,8 +7005,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
5699
7005
  if (options.assetsDir !== void 0) {
5700
7006
  try {
5701
7007
  const [compose2, postgresInit2] = await Promise.all([
5702
- readFile(path13.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
5703
- readFile(path13.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
7008
+ readFile(path16.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
7009
+ readFile(path16.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
5704
7010
  ]);
5705
7011
  validateComposeAsset(compose2);
5706
7012
  validatePostgresAsset(postgresInit2);
@@ -5740,7 +7046,8 @@ var inspectImage = async (runtime, context, image, format, policy) => {
5740
7046
  if (pulled.status !== 0) throw commandFailure(`Could not pull ${image}`, pulled);
5741
7047
  return runtime.run("docker", args);
5742
7048
  };
5743
- var checkLocalImages = async (runtime, config, policy = "local") => {
7049
+ var checkLocalImages = async (runtime, installation, policy = "local") => {
7050
+ const { config } = installation;
5744
7051
  const image = `ghcr.io/sealant-sh/mend:${config.serverVersion}`;
5745
7052
  const mend = await inspectImage(
5746
7053
  runtime,
@@ -5775,25 +7082,34 @@ var checkLocalImages = async (runtime, config, policy = "local") => {
5775
7082
  if (garage.status !== 0)
5776
7083
  throw commandFailure(`Preload ${GARAGE_IMAGE} before continuing`, garage);
5777
7084
  }
7085
+ if (installation.edgeImage !== void 0) {
7086
+ const edge = await inspectImage(
7087
+ runtime,
7088
+ config.dockerContext,
7089
+ installation.edgeImage,
7090
+ "{{.Id}}",
7091
+ policy
7092
+ );
7093
+ if (edge.status !== 0)
7094
+ throw commandFailure(`Preload ${installation.edgeImage} before continuing`, edge);
7095
+ }
5778
7096
  };
5779
7097
  var checkComposeImages = async (runtime, installation) => {
5780
7098
  const output = await runtime.run(
5781
7099
  "docker",
5782
- serverComposeArgs(
5783
- { directory: installation.directory, dockerContext: installation.config.dockerContext },
5784
- ["config", "--images"]
5785
- )
7100
+ serverComposeArgs(composeTarget(installation), ["config", "--images"])
5786
7101
  );
5787
7102
  if (output.status !== 0) throw commandFailure("Docker Compose config failed", output);
5788
7103
  const images = output.stdout.trim().split(/\s+/).toSorted();
5789
7104
  const expected = [
5790
7105
  `ghcr.io/sealant-sh/mend:${installation.config.serverVersion}`,
5791
7106
  "postgres:17-alpine",
5792
- ...installation.config.bucket === "garage" ? [GARAGE_IMAGE] : []
7107
+ ...installation.config.bucket === "garage" ? [GARAGE_IMAGE] : [],
7108
+ ...installation.edgeImage === void 0 ? [] : [installation.edgeImage]
5793
7109
  ].toSorted();
5794
7110
  if (images.join("\n") !== expected.join("\n")) {
5795
7111
  throw setupError(
5796
- `Compose must use only the canonical pinned Mend image, official postgres:17-alpine${installation.config.bucket === "garage" ? ` and ${GARAGE_IMAGE}` : ""}.`
7112
+ `Compose must use only the canonical pinned Mend image, official postgres:17-alpine${installation.config.bucket === "garage" ? ` and ${GARAGE_IMAGE}` : ""}${installation.edgeImage === void 0 ? "" : ` and the edge's ${installation.edgeImage}`}.`
5797
7113
  );
5798
7114
  }
5799
7115
  };
@@ -5802,10 +7118,15 @@ var initGarage = async (runtime, installation, secrets) => {
5802
7118
  const garage = garageSecrets(secrets);
5803
7119
  const exec = (args) => runtime.run(
5804
7120
  "docker",
5805
- serverComposeArgs(
5806
- { directory: installation.directory, dockerContext: installation.config.dockerContext },
5807
- ["exec", "-T", "garage", "/garage", "-c", "/etc/garage.toml", ...args]
5808
- ),
7121
+ serverComposeArgs(composeTarget(installation), [
7122
+ "exec",
7123
+ "-T",
7124
+ "garage",
7125
+ "/garage",
7126
+ "-c",
7127
+ "/etc/garage.toml",
7128
+ ...args
7129
+ ]),
5809
7130
  { timeoutMs: serverProcessDeadlines.ordinary }
5810
7131
  );
5811
7132
  const status = await exec(["status"]);
@@ -5827,19 +7148,79 @@ var initGarage = async (runtime, installation, secrets) => {
5827
7148
  "--key",
5828
7149
  GARAGE_BUCKET
5829
7150
  ]);
5830
- const info = await exec(["bucket", "info", GARAGE_BUCKET]);
5831
- if (info.status !== 0 || info.error !== void 0 || !info.stdout.includes(garage.keyId))
5832
- throw commandFailure(`Garage bucket ${GARAGE_BUCKET} is not readable by Mend's key`, info);
5833
- runtime.writeLine(`Capture store bucket ${GARAGE_BUCKET} is laid out in Garage`);
7151
+ const info = await exec(["bucket", "info", GARAGE_BUCKET]);
7152
+ if (info.status !== 0 || info.error !== void 0 || !info.stdout.includes(garage.keyId))
7153
+ throw commandFailure(`Garage bucket ${GARAGE_BUCKET} is not readable by Mend's key`, info);
7154
+ runtime.writeLine(`Capture store bucket ${GARAGE_BUCKET} is laid out in Garage`);
7155
+ };
7156
+ var startingNotice = (runtime, version) => runtime.writeLine(
7157
+ `Starting Mend ${version} containers; Docker waits up to ${serverProcessDeadlines.composeWaitSeconds}s for them to report healthy`
7158
+ );
7159
+ var removeStrayEdge = async (runtime, installation) => {
7160
+ if (installation.config.edgeHost !== void 0) return;
7161
+ const context = installation.config.dockerContext;
7162
+ const listed = await runtime.run("docker", [
7163
+ "--context",
7164
+ context,
7165
+ "container",
7166
+ "ls",
7167
+ "--all",
7168
+ "--filter",
7169
+ "label=com.docker.compose.project=mend",
7170
+ "--filter",
7171
+ "label=com.docker.compose.service=edge",
7172
+ "--format",
7173
+ '{{.Names}} {{.Label "com.docker.compose.project.working_dir"}}'
7174
+ ]);
7175
+ if (listed.status !== 0 || listed.error !== void 0) {
7176
+ runtime.writeLine(
7177
+ `Could not list this project's containers: ${outputDetail(listed)}. An edge container left by an earlier generation, if any, stays until the next start. mend server status shows it.`
7178
+ );
7179
+ return;
7180
+ }
7181
+ const generations = path16.dirname(installation.directory);
7182
+ let resolvedGenerations = generations;
7183
+ try {
7184
+ resolvedGenerations = fs16.realpathSync(generations);
7185
+ } catch {
7186
+ resolvedGenerations = generations;
7187
+ }
7188
+ const own = (workingDir) => [generations, resolvedGenerations].some((root) => workingDir.startsWith(`${root}${path16.sep}`));
7189
+ const rows = listed.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "").map((line) => {
7190
+ const [name = "", workingDir = ""] = line.split(" ");
7191
+ return { name, workingDir };
7192
+ }).filter((row) => row.name !== "");
7193
+ for (const row of rows.filter((candidate) => !own(candidate.workingDir))) {
7194
+ runtime.writeLine(
7195
+ `Edge container ${row.name} was started from ${row.workingDir || "an unknown directory"}, not from this installation's generations. It stays.`
7196
+ );
7197
+ }
7198
+ const names = rows.filter((row) => own(row.workingDir)).map((row) => row.name);
7199
+ if (names.length === 0) return;
7200
+ const removed = await runtime.run("docker", [
7201
+ "--context",
7202
+ context,
7203
+ "container",
7204
+ "rm",
7205
+ "--force",
7206
+ ...names
7207
+ ]);
7208
+ if (removed.status !== 0 || removed.error !== void 0) {
7209
+ runtime.writeLine(
7210
+ `Could not remove the edge container ${names.join(", ")}: ${outputDetail(removed)}. It may still hold 80 and 443. To remove it: docker --context ${context} container rm --force ${names.join(" ")}`
7211
+ );
7212
+ return;
7213
+ }
7214
+ runtime.writeLine(
7215
+ `Removed the edge container ${names.join(", ")}, which this generation does not run.`
7216
+ );
5834
7217
  };
5835
- var startingNotice = (runtime, version) => runtime.writeLine(
5836
- `Starting Mend ${version} containers; Docker waits up to ${serverProcessDeadlines.composeWaitSeconds}s for them to report healthy`
5837
- );
5838
- var startCompose = async (runtime, config, generation) => {
5839
- startingNotice(runtime, config.serverVersion);
7218
+ var startCompose = async (runtime, installation) => {
7219
+ await removeStrayEdge(runtime, installation);
7220
+ startingNotice(runtime, installation.config.serverVersion);
5840
7221
  const compose = await runtime.run(
5841
7222
  "docker",
5842
- serverComposeArgs({ directory: generation.directory, dockerContext: config.dockerContext }, [
7223
+ serverComposeArgs(composeTarget(installation), [
5843
7224
  "up",
5844
7225
  "-d",
5845
7226
  "--wait",
@@ -5853,6 +7234,7 @@ var startCompose = async (runtime, config, generation) => {
5853
7234
  );
5854
7235
  if (compose.status !== 0) throw commandFailure("Mend containers did not start", compose);
5855
7236
  };
7237
+ var edgeStartedLine = (config) => config.edgeHost === void 0 ? null : `The edge for ${config.edgeHost} is up on 80 and 443. Caddy asks for its certificate once ${config.edgeHost} resolves to this machine and both ports reach it from the Internet. mend server status says whether it holds one, and mend server logs shows what Caddy tried.`;
5856
7238
  var setupServer = async (args, runtime, store) => {
5857
7239
  if (runtime.platform !== "linux" && runtime.platform !== "darwin") {
5858
7240
  throw setupError(`mend server setup supports Linux and macOS, not ${runtime.platform}.`);
@@ -5909,24 +7291,47 @@ var setupServer = async (args, runtime, store) => {
5909
7291
  ...configWithoutBucket,
5910
7292
  ...bucket === void 0 ? {} : { bucket }
5911
7293
  };
7294
+ if (existing === null && (config.edgeHost !== void 0 || config.exposure === "public")) {
7295
+ throw setupError(
7296
+ `A fresh install cannot start with the edge or as public: until the first account exists, registration is open to whoever reaches the origin first, and the server refuses MEND_EXPOSURE=public without an operator account. Run mend server setup without --edge and --exposure public, create the first account at http://localhost:${config.appPort}, then run mend server setup --edge <host> and declare the posture.`
7297
+ );
7298
+ }
5912
7299
  const secrets = savedSecrets ?? createSecrets(runtime);
5913
7300
  const generation = persistSetup(store, config, secrets, assets);
5914
- await checkComposeImages(runtime, { directory: generation.directory, config });
7301
+ const installation = {
7302
+ directory: generation.directory,
7303
+ config,
7304
+ ...generation.files.edge === void 0 ? {} : { edgeImage: edgeImageOf(generation.files.edge) }
7305
+ };
7306
+ await checkComposeImages(runtime, installation);
5915
7307
  const ownership2 = await claimServerDockerVolumes(runtime, {
5916
7308
  dockerContext: config.dockerContext,
5917
7309
  identityBytes: Buffer.from(generation.files.identity),
5918
7310
  namespace: namespaceOf(config)
5919
7311
  });
5920
7312
  if (ownership2._tag === "error") throw setupError(ownership2.error.message);
5921
- await checkLocalImages(runtime, config, options.offline ? "local" : "pull-missing");
7313
+ await checkLocalImages(runtime, installation, options.offline ? "local" : "pull-missing");
7314
+ const userNamespaces = await hostUserNamespacesLine(
7315
+ runtime,
7316
+ config.dockerContext,
7317
+ `ghcr.io/sealant-sh/mend:${config.serverVersion}`
7318
+ );
5922
7319
  storeValue(store.activate(generation));
5923
- await startCompose(runtime, config, generation);
5924
- await initGarage(runtime, { directory: generation.directory, config }, secrets);
5925
- await probeHealth(runtime, config.appUrl, config.serverVersion);
5926
- runtime.writeLine(`Mend ${config.serverVersion} is reachable at ${config.appUrl}`);
7320
+ await startCompose(runtime, installation);
7321
+ await initGarage(runtime, installation, secrets);
7322
+ await probeHealth(runtime, healthOrigin(config), config.serverVersion);
7323
+ runtime.writeLine(reachableLine(config));
7324
+ const edgeStarted = edgeStartedLine(config);
7325
+ if (edgeStarted !== null) runtime.writeLine(edgeStarted);
7326
+ if (existing?.config.edgeHost !== void 0 && config.edgeHost === void 0) {
7327
+ runtime.writeLine(
7328
+ `The edge for ${existing.config.edgeHost} is gone. Its certificate volumes stay until you remove them: docker --context ${config.dockerContext} volume rm mend_mend-edge-data mend_mend-edge-config`
7329
+ );
7330
+ }
5927
7331
  runtime.writeLine(
5928
7332
  `Open ${config.appUrl}, create the first account, then run: mend login --url ${config.appUrl}`
5929
7333
  );
7334
+ if (userNamespaces !== null) runtime.writeLine(userNamespaces);
5930
7335
  };
5931
7336
  var serverVersionParts = (version) => {
5932
7337
  const separator = version.indexOf("-");
@@ -5970,7 +7375,7 @@ var composeCommand = async (runtime, installation, args) => {
5970
7375
  const output = await runtime.run(
5971
7376
  "docker",
5972
7377
  serverComposeArgs(
5973
- { directory: installation.directory, dockerContext: installation.config.dockerContext },
7378
+ composeTarget(installation),
5974
7379
  args.flatMap(
5975
7380
  (arg) => arg === "--wait" ? [arg, "--wait-timeout", String(serverProcessDeadlines.composeWaitSeconds)] : [arg]
5976
7381
  )
@@ -5987,6 +7392,7 @@ var interruptionNotice = (runtime) => runtime.writeLine(
5987
7392
  "Connections will be interrupted. Workspace containers and data are retained, but active work can lose connectivity and may need reconnection. Mend does not stop workspace containers."
5988
7393
  );
5989
7394
  var startInstallation = async (runtime, installation, secrets) => {
7395
+ await removeStrayEdge(runtime, installation);
5990
7396
  startingNotice(runtime, installation.config.serverVersion);
5991
7397
  await composeCommand(runtime, installation, [
5992
7398
  "up",
@@ -5997,33 +7403,160 @@ var startInstallation = async (runtime, installation, secrets) => {
5997
7403
  "--no-build"
5998
7404
  ]);
5999
7405
  await initGarage(runtime, installation, secrets);
6000
- await probeHealth(runtime, installation.config.appUrl, installation.config.serverVersion);
6001
- runtime.writeLine(
6002
- `Mend ${installation.config.serverVersion} is reachable at ${installation.config.appUrl}`
6003
- );
7406
+ await probeHealth(runtime, healthOrigin(installation.config), installation.config.serverVersion);
7407
+ runtime.writeLine(reachableLine(installation.config));
6004
7408
  };
6005
7409
  var parseUpgradeOptions = (args) => {
6006
- for (let index = 0; index < args.length; index += 1) {
6007
- const flag = args[index];
7410
+ const fromPreview = args.includes("--from-preview");
7411
+ const rest = args.filter((arg) => arg !== "--from-preview");
7412
+ for (let index = 0; index < rest.length; index += 1) {
7413
+ const flag = rest[index];
6008
7414
  if (flag === "--offline") continue;
6009
7415
  if (flag !== "--version" && flag !== "--assets-dir")
6010
7416
  throw setupError(`Unknown server upgrade option "${flag}".`);
6011
7417
  index += 1;
6012
7418
  }
6013
- const options = parseSetupOptions(args);
7419
+ const options = parseSetupOptions(rest);
6014
7420
  if (options.version === void 0)
6015
7421
  throw setupError(
6016
7422
  "Upgrade requires --version TARGET. Use --version latest only to request the latest release explicitly."
6017
7423
  );
6018
- return options;
7424
+ return { ...options, fromPreview };
7425
+ };
7426
+ var LEGACY_PREVIEW = /^(\d+\.\d+\.\d+)-preview\.(0|[1-9]\d*)$/;
7427
+ var NEXT_BUILD = /^(\d+\.\d+\.\d+)-next\.(0|[1-9]\d*)(\.preview\.[1-9]\d*)?$/;
7428
+ var isPreviewToNext = (from, to) => {
7429
+ const preview = LEGACY_PREVIEW.exec(from);
7430
+ const next = NEXT_BUILD.exec(to);
7431
+ return preview !== null && next !== null && preview[1] === next[1];
7432
+ };
7433
+ var drizzleMillis = (folder) => {
7434
+ const at = (start, end) => Number(folder.slice(start, end));
7435
+ return String(Date.UTC(at(0, 4), at(4, 6) - 1, at(6, 8), at(8, 10), at(10, 12), at(12, 14)));
7436
+ };
7437
+ var mendKey = (migration) => `${String(migration.id).padStart(4, "0")}_${migration.name}`;
7438
+ var migrationProblems = (applied, manifest) => {
7439
+ const mend = /* @__PURE__ */ new Map();
7440
+ const sealant = /* @__PURE__ */ new Map();
7441
+ const sealantTimes = /* @__PURE__ */ new Map();
7442
+ for (const line of manifest.split("\n")) {
7443
+ const [kind, entry, hash = ""] = line.trim().split(/\s+/);
7444
+ if (entry === void 0) continue;
7445
+ if (kind === "mend") {
7446
+ const match = /^(\d+)_(.+)$/.exec(entry);
7447
+ if (match?.[1] !== void 0 && match[2] !== void 0) mend.set(Number(match[1]), match[2]);
7448
+ }
7449
+ if (kind === "sealant") {
7450
+ sealant.set(entry, hash);
7451
+ sealantTimes.set(drizzleMillis(entry), hash);
7452
+ }
7453
+ }
7454
+ const problems = [];
7455
+ for (const migration of applied.mend) {
7456
+ if (mend.get(migration.id) !== migration.name)
7457
+ problems.push(`mend ${mendKey(migration)} is applied here and not in the target`);
7458
+ }
7459
+ const appliedIds = new Set(applied.mend.map((migration) => migration.id));
7460
+ const highest = Math.max(0, ...appliedIds);
7461
+ for (const [id, name] of [...mend].toSorted(([a], [b]) => a - b)) {
7462
+ if (id <= highest && !appliedIds.has(id))
7463
+ problems.push(
7464
+ `mend ${mendKey({ id, name })} would never run: this server already applied ${highest}`
7465
+ );
7466
+ }
7467
+ for (const row of applied.sealant) {
7468
+ const label = row.name === "" ? `(created ${row.createdAt})` : row.name;
7469
+ const hash = row.name === "" ? sealantTimes.get(row.createdAt) : sealant.get(row.name);
7470
+ if (hash === void 0) problems.push(`sealant ${label} is applied here and not in the target`);
7471
+ else if (hash !== row.hash)
7472
+ problems.push(`sealant ${label} changed after this server applied it`);
7473
+ }
7474
+ return problems;
7475
+ };
7476
+ var psqlRows = async (runtime, installation, database, query) => {
7477
+ const output = await composeCommand(runtime, installation, [
7478
+ "exec",
7479
+ "-T",
7480
+ "postgres",
7481
+ "psql",
7482
+ "--username=postgres",
7483
+ `--dbname=${database}`,
7484
+ "--no-align",
7485
+ "--tuples-only",
7486
+ "--field-separator=|",
7487
+ "--command",
7488
+ query
7489
+ ]);
7490
+ return output.stdout.split("\n").filter((line) => line.trim().length > 0);
7491
+ };
7492
+ var checkPreviewMigrations = async (runtime, existing, target) => {
7493
+ const image = `ghcr.io/sealant-sh/mend:${target.serverVersion}`;
7494
+ const listed = await runtime.run(
7495
+ "docker",
7496
+ [
7497
+ "--context",
7498
+ target.dockerContext,
7499
+ "run",
7500
+ "--rm",
7501
+ "--entrypoint",
7502
+ "cat",
7503
+ image,
7504
+ "/app/migrations.txt"
7505
+ ],
7506
+ { timeoutMs: serverProcessDeadlines.ordinary }
7507
+ );
7508
+ if (listed.status !== 0 || listed.error !== void 0 || listed.stdout.trim() === "")
7509
+ throw setupError(
7510
+ `${image} does not list its migrations (/app/migrations.txt), so --from-preview cannot check them. Nothing was changed.`
7511
+ );
7512
+ await composeCommand(runtime, existing, [
7513
+ "up",
7514
+ "-d",
7515
+ "--wait",
7516
+ "--pull",
7517
+ "never",
7518
+ "--no-build",
7519
+ "postgres"
7520
+ ]);
7521
+ const mend = (await psqlRows(
7522
+ runtime,
7523
+ existing,
7524
+ "mend",
7525
+ "select migration_id, name from mend_migrations order by migration_id"
7526
+ )).map((line) => {
7527
+ const [id = "", name = ""] = line.split("|");
7528
+ return { id: Number(id), name };
7529
+ });
7530
+ const sealant = (await psqlRows(
7531
+ runtime,
7532
+ existing,
7533
+ "sealant_control_plane",
7534
+ "select coalesce(name, ''), created_at, hash from drizzle.__drizzle_migrations order by id"
7535
+ )).map((line) => {
7536
+ const [name = "", createdAt = "", hash = ""] = line.split("|");
7537
+ return { name, createdAt, hash };
7538
+ });
7539
+ const problems = migrationProblems({ mend, sealant }, listed.stdout);
7540
+ if (problems.length > 0)
7541
+ throw setupError(
7542
+ `${image} cannot take this server's databases (${problems.length}): ${problems.join("; ")}. Choose a build that contains every migration this server applied. Nothing was changed.`
7543
+ );
7544
+ runtime.writeLine(
7545
+ `${image} carries all ${mend.length + sealant.length} migrations this server applied.`
7546
+ );
6019
7547
  };
6020
7548
  var upgradeServer = async (args, runtime, store, existing) => {
6021
7549
  const options = parseUpgradeOptions(args);
6022
7550
  const version = await resolveServerVersion(runtime, options, existing.config);
6023
7551
  const order = compareServerVersions(version, existing.config.serverVersion);
6024
- if (order < 0)
7552
+ const previewToNext = isPreviewToNext(existing.config.serverVersion, version);
7553
+ if (options.fromPreview && !previewToNext)
7554
+ throw setupError(
7555
+ "--from-preview moves a server on X.Y.Z-preview.K to a next build or a new-style preview of the same version (X.Y.Z-next.N, X.Y.Z-next.N.preview.R), and nothing else."
7556
+ );
7557
+ if (order < 0 && !options.fromPreview)
6025
7558
  throw setupError(
6026
- `Refusing downgrade from ${existing.config.serverVersion} to ${version}. Database migrations may not be reversible.`
7559
+ `Refusing downgrade from ${existing.config.serverVersion} to ${version}. Database migrations may not be reversible.${previewToNext ? ` ${existing.config.serverVersion} is a preview numbered before the next channel. To move to the next channel once, run mend server upgrade --version ${version} --from-preview; it first checks that ${version} carries every migration this server applied.` : ""}`
6027
7560
  );
6028
7561
  if (order === 0) {
6029
7562
  runtime.writeLine(
@@ -6047,20 +7580,20 @@ var upgradeServer = async (args, runtime, store, existing) => {
6047
7580
  ...bucket === void 0 ? {} : { bucket }
6048
7581
  };
6049
7582
  const secrets = parseSecrets(previous.files.identity);
6050
- const files = {
6051
- identity: previous.files.identity,
6052
- config: `${JSON.stringify(config, null, 2)}
6053
- `,
6054
- env: renderSecrets(secrets, config),
6055
- ...assets
6056
- };
7583
+ const files = generationFiles(config, secrets, assets, previous.files.identity);
6057
7584
  const parsed = parseServerConfig(files.config);
6058
7585
  if (renderSecrets(parseSecrets(files.env), parsed) !== files.env)
6059
7586
  throw setupError("Invalid upgrade configuration.");
6060
- await checkLocalImages(runtime, config, options.offline ? "local" : "pull-missing");
7587
+ const edgeImage = files.edge === void 0 ? {} : { edgeImage: edgeImageOf(files.edge) };
7588
+ await checkLocalImages(
7589
+ runtime,
7590
+ { config, ...edgeImage },
7591
+ options.offline ? "local" : "pull-missing"
7592
+ );
6061
7593
  await checkComposeImages(runtime, existing);
7594
+ if (options.fromPreview) await checkPreviewMigrations(runtime, existing, config);
6062
7595
  const target = storeValue(store.prepare(files));
6063
- const installation = { directory: target.directory, config };
7596
+ const installation = { directory: target.directory, config, ...edgeImage };
6064
7597
  await checkComposeImages(runtime, installation);
6065
7598
  if (config.bucket === "garage" && existing.config.bucket !== "garage") {
6066
7599
  const ownership2 = await claimServerDockerVolumes(runtime, {
@@ -6095,7 +7628,7 @@ var upgradeServer = async (args, runtime, store, existing) => {
6095
7628
  ]);
6096
7629
  const dumped = await runtime.run(
6097
7630
  "docker",
6098
- serverComposeArgs({ directory: previous.directory, dockerContext: config.dockerContext }, [
7631
+ serverComposeArgs(composeTarget(existing), [
6099
7632
  "exec",
6100
7633
  "-T",
6101
7634
  "postgres",
@@ -6137,11 +7670,112 @@ var upgradeServer = async (args, runtime, store, existing) => {
6137
7670
  }
6138
7671
  runtime.writeLine(`Upgraded to ${version}. Retained database backup: ${backup.directory}`);
6139
7672
  };
6140
- var serverStatus = async (runtime, installation) => {
6141
- runtime.writeLine(
6142
- `Pinned Mend ${installation.config.serverVersion} at ${installation.config.appUrl}`
7673
+ var observeEdgeCertificate = async (runtime, installation, host) => {
7674
+ const listed = await runtime.run(
7675
+ "docker",
7676
+ serverComposeArgs(composeTarget(installation), [
7677
+ "exec",
7678
+ "-T",
7679
+ "edge",
7680
+ "sh",
7681
+ "-c",
7682
+ `ls /data/caddy/certificates/*/${host}/${host}.crt`
7683
+ ]),
7684
+ { timeoutMs: serverProcessDeadlines.ordinary }
6143
7685
  );
7686
+ if (listed.error !== void 0) return { kind: "unavailable", reason: listed.error };
7687
+ if (listed.status !== 0) {
7688
+ return /No such file or directory/.test(listed.stderr) ? { kind: "none" } : {
7689
+ kind: "unavailable",
7690
+ reason: listed.stderr.trim() || `exit ${listed.status ?? "unknown"}`
7691
+ };
7692
+ }
7693
+ const file = listed.stdout.trim().split("\n")[0];
7694
+ return file === void 0 || file === "" ? { kind: "none" } : { kind: "observed", file };
7695
+ };
7696
+ var operatorReportLines = async (runtime, installation) => {
7697
+ const login2 = runtime.readLogin?.(runtime.configDir) ?? null;
7698
+ if (login2 === null) {
7699
+ return [
7700
+ `gate items \xB7 every item with its detail needs the operator's sign-in on this machine: mend login --url ${installation.config.appUrl}, then mend server status again, or mend operator gate and mend operator exposure`
7701
+ ];
7702
+ }
7703
+ let loginOrigin;
7704
+ try {
7705
+ loginOrigin = new URL(login2.url).origin;
7706
+ } catch {
7707
+ return [`gate items \xB7 the saved sign-in's URL "${login2.url}" is not a URL \xB7 not read`];
7708
+ }
7709
+ if (loginOrigin !== installation.config.appUrl) {
7710
+ return [
7711
+ `gate items \xB7 this machine is signed in to ${loginOrigin}, not ${installation.config.appUrl} \xB7 not read`
7712
+ ];
7713
+ }
7714
+ const headers = { Authorization: `Bearer ${login2.token}` };
7715
+ const read = async (route) => {
7716
+ const response = await runtime.fetchText(
7717
+ `${healthOrigin(installation.config)}/api${route}`,
7718
+ 5e3,
7719
+ headers
7720
+ );
7721
+ if (response.error !== void 0) return `${route} \xB7 not read: ${response.error}`;
7722
+ if (response.status === 404)
7723
+ return `${route} \xB7 404: the signed-in account does not hold the operator role`;
7724
+ if (response.status < 200 || response.status >= 300)
7725
+ return `${route} \xB7 not read: HTTP ${response.status}`;
7726
+ try {
7727
+ return JSON.parse(response.body);
7728
+ } catch {
7729
+ return `${route} \xB7 not read: the answer is not JSON`;
7730
+ }
7731
+ };
7732
+ const lines = [];
7733
+ const gate = await read("/operator/gate");
7734
+ const gateItems = Array.isArray(gate) ? gate.flatMap(gateItemOf) : null;
7735
+ if (typeof gate === "string") lines.push(`gate items \xB7 ${gate}`);
7736
+ else if (gateItems === null || gateItems.length === 0)
7737
+ lines.push("gate items \xB7 /operator/gate answered in a shape this CLI does not read");
7738
+ else lines.push("multi mode gate, as the server reports each item:", ...renderGate(gateItems));
7739
+ const exposure = await read("/operator/exposure");
7740
+ const report = exposureReportOf(exposure);
7741
+ if (typeof exposure === "string") lines.push(`gate items \xB7 ${exposure}`);
7742
+ else if (report === null)
7743
+ lines.push("gate items \xB7 /operator/exposure answered in a shape this CLI does not read");
7744
+ else
7745
+ lines.push("public exposure gate, as the server reports each item:", ...renderExposure(report));
7746
+ return lines;
7747
+ };
7748
+ var gateItemOf = (value) => {
7749
+ const fields = ownFields(value);
7750
+ const id = fields?.get("id");
7751
+ const ok = fields?.get("ok");
7752
+ const detail = fields?.get("detail");
7753
+ const fix = fields?.get("fix");
7754
+ return typeof id === "string" && typeof ok === "boolean" && typeof detail === "string" && (fix === null || typeof fix === "string") ? [{ id, ok, detail, fix: fix ?? null }] : [];
7755
+ };
7756
+ var isEstablished = (value) => value === "observed" || value === "carried" || value === "declared" || value === "open";
7757
+ var exposureItemOf = (value) => {
7758
+ const fields = ownFields(value);
7759
+ const id = fields?.get("id");
7760
+ const established = fields?.get("established");
7761
+ const detail = fields?.get("detail");
7762
+ const fix = fields?.get("fix");
7763
+ const blocksStart = fields?.get("blocksStart");
7764
+ return typeof id === "string" && isEstablished(established) && typeof detail === "string" && (fix === null || typeof fix === "string") && typeof blocksStart === "boolean" ? [{ id, established, detail, fix: fix ?? null, blocksStart }] : [];
7765
+ };
7766
+ var exposureReportOf = (value) => {
7767
+ const fields = ownFields(value);
7768
+ const declared = fields?.get("declared");
7769
+ const items = fields?.get("items");
7770
+ if (typeof declared !== "string" || !isExposure(declared) || !Array.isArray(items)) return null;
7771
+ const parsed = items.flatMap(exposureItemOf);
7772
+ return parsed.length === items.length && parsed.length > 0 ? { declared, items: parsed } : null;
7773
+ };
7774
+ var serverStatus = async (runtime, installation) => {
7775
+ const { config } = installation;
7776
+ runtime.writeLine(`Pinned Mend ${config.serverVersion} at ${config.appUrl}`);
6144
7777
  runtime.writeLine(`Active generation: ${installation.directory}`);
7778
+ for (const line of declaredPostureLines(config)) runtime.writeLine(line);
6145
7779
  const state = await composeCommand(runtime, installation, ["ps", "--all"]);
6146
7780
  runtime.writeLine(state.stdout.trim() || "No Compose containers found. Run mend server start.");
6147
7781
  const running = await composeCommand(runtime, installation, [
@@ -6150,24 +7784,36 @@ var serverStatus = async (runtime, installation) => {
6150
7784
  "running",
6151
7785
  "--services"
6152
7786
  ]);
6153
- if (!running.stdout.trim().split(/\s+/).includes("mend")) {
7787
+ const runningServices = running.stdout.trim().split(/\s+/);
7788
+ if (config.edgeHost !== void 0) {
7789
+ const edgeRunning = runningServices.includes("edge");
7790
+ runtime.writeLine(
7791
+ observedEdgeLine(config.edgeHost, {
7792
+ running: edgeRunning,
7793
+ certificate: edgeRunning ? await observeEdgeCertificate(runtime, installation, config.edgeHost) : { kind: "unavailable", reason: "the edge is not running, so its data was not read" }
7794
+ })
7795
+ );
7796
+ }
7797
+ if (!runningServices.includes("mend")) {
6154
7798
  runtime.writeLine("Mend is stopped. No health claim was made.");
6155
7799
  return;
6156
7800
  }
6157
- const response = await runtime.fetchText(`${installation.config.appUrl}/api/health`, 2e3);
7801
+ const response = await runtime.fetchText(`${healthOrigin(config)}/api/health`, 2e3);
6158
7802
  let fields = null;
7803
+ let body = null;
6159
7804
  try {
6160
- fields = ownFields(JSON.parse(response.body));
7805
+ body = JSON.parse(response.body);
7806
+ fields = ownFields(body);
6161
7807
  } catch {
6162
7808
  }
6163
- if (response.error !== void 0 || response.status < 200 || response.status >= 300 || fields?.get("status") !== "ok" || fields.get("version") !== installation.config.serverVersion) {
7809
+ if (response.error !== void 0 || response.status < 200 || response.status >= 300 || fields?.get("status") !== "ok" || fields.get("version") !== config.serverVersion) {
6164
7810
  throw setupError(
6165
- `Mend is running but exact-version health for ${installation.config.serverVersion} was not observed. Check mend server logs --tail 100.`
7811
+ `Mend is running but exact-version health for ${config.serverVersion} was not observed. Check mend server logs --tail 100.`
6166
7812
  );
6167
7813
  }
6168
- runtime.writeLine(
6169
- `Mend ${installation.config.serverVersion} is reachable at ${installation.config.appUrl}`
6170
- );
7814
+ runtime.writeLine(reachableLine(config));
7815
+ for (const line of observedPostureLines(config, healthPosture(body))) runtime.writeLine(line);
7816
+ for (const line of await operatorReportLines(runtime, installation)) runtime.writeLine(line);
6171
7817
  };
6172
7818
  var manageServer = async (command, args, runtime, store) => {
6173
7819
  const installation = storeValue(readServerInstallation(store));
@@ -6212,12 +7858,18 @@ var manageServer = async (command, args, runtime, store) => {
6212
7858
  if (command === "stop") {
6213
7859
  interruptionNotice(runtime);
6214
7860
  await composeCommand(runtime, installation, ["stop", "--timeout", "30"]);
7861
+ const stopped = [
7862
+ "Mend",
7863
+ "Postgres",
7864
+ ...installation.config.bucket === "garage" ? ["Garage"] : [],
7865
+ ...installation.config.edgeHost === void 0 ? [] : ["the edge"]
7866
+ ];
6215
7867
  runtime.writeLine(
6216
- `Mend, Postgres${installation.config.bucket === "garage" ? " and Garage" : ""} stopped. Volumes, configuration and workspace containers are retained.`
7868
+ `${stopped.slice(0, -1).join(", ")} and ${stopped.at(-1)} stopped. Volumes, configuration and workspace containers are retained.`
6217
7869
  );
6218
7870
  return;
6219
7871
  }
6220
- await checkLocalImages(runtime, installation.config);
7872
+ await checkLocalImages(runtime, installation);
6221
7873
  if (command === "restart") {
6222
7874
  interruptionNotice(runtime);
6223
7875
  await composeCommand(runtime, installation, ["stop", "--timeout", "30", "mend"]);
@@ -6226,16 +7878,19 @@ var manageServer = async (command, args, runtime, store) => {
6226
7878
  };
6227
7879
  var nodeServerRuntime = () => {
6228
7880
  const environment = { ...process.env };
6229
- const home = os8.homedir();
6230
- const configHome = environment["XDG_CONFIG_HOME"] ?? path13.join(home, ".config");
7881
+ const home = os10.homedir();
7882
+ const configHome = environment["XDG_CONFIG_HOME"] ?? path16.join(home, ".config");
6231
7883
  return {
6232
- configDir: path13.join(configHome, "mend"),
7884
+ configDir: path16.join(configHome, "mend"),
6233
7885
  platform: process.platform,
6234
7886
  cliVersion: cliVersion(),
6235
7887
  run: (command, args, options) => runServerProcess(command, args, environment, options),
6236
- fetchText: async (url, timeoutMs) => {
7888
+ fetchText: async (url, timeoutMs, headers) => {
6237
7889
  try {
6238
- const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
7890
+ const response = await fetch(url, {
7891
+ signal: AbortSignal.timeout(timeoutMs),
7892
+ ...headers === void 0 ? {} : { headers }
7893
+ });
6239
7894
  return { status: response.status, body: await response.text() };
6240
7895
  } catch (cause) {
6241
7896
  return {
@@ -6246,12 +7901,26 @@ var nodeServerRuntime = () => {
6246
7901
  }
6247
7902
  },
6248
7903
  randomBytes,
6249
- sleep: (milliseconds) => new Promise((resolve7) => setTimeout(resolve7, milliseconds)),
7904
+ sleep: (milliseconds) => new Promise((resolve8) => setTimeout(resolve8, milliseconds)),
6250
7905
  writeLine: (line) => process.stdout.write(`${line}
6251
7906
  `),
6252
- dockerDaemonFacts: hostDockerDaemonFacts
7907
+ dockerDaemonFacts: hostDockerDaemonFacts,
7908
+ readLogin: (configDir) => savedLogin(configDir, environment)
6253
7909
  };
6254
7910
  };
7911
+ var savedLogin = (configDir, environment) => {
7912
+ let saved = null;
7913
+ try {
7914
+ saved = ownFields(JSON.parse(fs16.readFileSync(path16.join(configDir, "cli.json"), "utf8")));
7915
+ } catch {
7916
+ saved = null;
7917
+ }
7918
+ const savedUrl = saved?.get("url");
7919
+ const savedToken = saved?.get("token");
7920
+ const url = environment["MEND_URL"] ?? (typeof savedUrl === "string" ? savedUrl : null);
7921
+ const token = environment["MEND_TOKEN"] ?? (typeof savedToken === "string" ? savedToken : null);
7922
+ return url === null || token === null || token === "" ? null : { url, token };
7923
+ };
6255
7924
  var serverCommand = async (args, runtime = nodeServerRuntime()) => {
6256
7925
  const [command, ...rest] = args;
6257
7926
  if (command === void 0 || !["setup", "status", "start", "stop", "restart", "logs", "upgrade"].includes(command)) {
@@ -6456,7 +8125,7 @@ var renderMendToml = (proposals) => {
6456
8125
  };
6457
8126
 
6458
8127
  // src/service-tunnels.ts
6459
- import * as net3 from "node:net";
8128
+ import * as net4 from "node:net";
6460
8129
  var attachTunnelTargets = (services, sessionId) => services.filter(
6461
8130
  (service) => service.sessionId === sessionId && service.protocol === "tcp" && service.browserScheme !== null
6462
8131
  );
@@ -6497,7 +8166,7 @@ var pumpConnection = (socket, url) => {
6497
8166
  () => socket.destroy()
6498
8167
  );
6499
8168
  };
6500
- var listenLocal = (server, port, fallback) => new Promise((resolve7, reject) => {
8169
+ var listenLocal = (server, port, fallback) => new Promise((resolve8, reject) => {
6501
8170
  const attempt2 = (candidate, lastTry) => {
6502
8171
  const onError = (error) => {
6503
8172
  if (!lastTry) {
@@ -6510,7 +8179,7 @@ var listenLocal = (server, port, fallback) => new Promise((resolve7, reject) =>
6510
8179
  server.listen(candidate, "127.0.0.1", () => {
6511
8180
  server.removeListener("error", onError);
6512
8181
  const address = server.address();
6513
- resolve7(typeof address === "object" && address !== null ? address.port : candidate);
8182
+ resolve8(typeof address === "object" && address !== null ? address.port : candidate);
6514
8183
  });
6515
8184
  };
6516
8185
  attempt2(port, !fallback || port === 0);
@@ -6539,7 +8208,7 @@ var serverEvents = (config) => async (handlers, signal) => {
6539
8208
  if (signal.aborted) return;
6540
8209
  }
6541
8210
  connected = true;
6542
- await new Promise((resolve7) => setTimeout(resolve7, 2e3));
8211
+ await new Promise((resolve8) => setTimeout(resolve8, 2e3));
6543
8212
  }
6544
8213
  };
6545
8214
  var createServiceTunnels = (options) => {
@@ -6568,7 +8237,7 @@ var createServiceTunnels = (options) => {
6568
8237
  };
6569
8238
  const openFor = async (service) => {
6570
8239
  const sockets = /* @__PURE__ */ new Set();
6571
- const server = net3.createServer((socket) => {
8240
+ const server = net4.createServer((socket) => {
6572
8241
  sockets.add(socket);
6573
8242
  socket.on("close", () => sockets.delete(socket));
6574
8243
  pumpConnection(socket, options.tunnelUrl(service.id));
@@ -6682,8 +8351,8 @@ var createServiceTunnels = (options) => {
6682
8351
  };
6683
8352
 
6684
8353
  // src/skills.ts
6685
- import * as fs14 from "node:fs";
6686
- import * as path14 from "node:path";
8354
+ import * as fs17 from "node:fs";
8355
+ import * as path17 from "node:path";
6687
8356
  var DEFAULT_SKILLS_DIR = ".agents/skills";
6688
8357
  var MAX_SKILL_FILES = 64;
6689
8358
  var MAX_SKILL_FILE_BYTES = 512 * 1024;
@@ -6706,12 +8375,12 @@ var readFrontmatterDescription = (contents) => {
6706
8375
  var walkFiles = (root) => {
6707
8376
  const found = [];
6708
8377
  const visit = (dir) => {
6709
- for (const entry of fs14.readdirSync(dir, { withFileTypes: true })) {
8378
+ for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
6710
8379
  if (IGNORED_DIRS.has(entry.name)) continue;
6711
- const absolute = path14.join(dir, entry.name);
8380
+ const absolute = path17.join(dir, entry.name);
6712
8381
  let stat;
6713
8382
  try {
6714
- stat = fs14.statSync(absolute);
8383
+ stat = fs17.statSync(absolute);
6715
8384
  } catch {
6716
8385
  continue;
6717
8386
  }
@@ -6725,7 +8394,7 @@ var walkFiles = (root) => {
6725
8394
  var scanSkillLibrary = (root) => {
6726
8395
  let entries;
6727
8396
  try {
6728
- entries = fs14.readdirSync(root, { withFileTypes: true });
8397
+ entries = fs17.readdirSync(root, { withFileTypes: true });
6729
8398
  } catch {
6730
8399
  return { error: `${root} is not a readable directory` };
6731
8400
  }
@@ -6733,32 +8402,32 @@ var scanSkillLibrary = (root) => {
6733
8402
  const notes = [];
6734
8403
  for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
6735
8404
  if (entry.name.startsWith(".")) continue;
6736
- const skillDir = path14.join(root, entry.name);
8405
+ const skillDir = path17.join(root, entry.name);
6737
8406
  let stat;
6738
8407
  try {
6739
- stat = fs14.statSync(skillDir);
8408
+ stat = fs17.statSync(skillDir);
6740
8409
  } catch {
6741
8410
  continue;
6742
8411
  }
6743
8412
  if (!stat.isDirectory()) continue;
6744
- if (!fs14.existsSync(path14.join(skillDir, "SKILL.md"))) {
8413
+ if (!fs17.existsSync(path17.join(skillDir, "SKILL.md"))) {
6745
8414
  notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
6746
8415
  continue;
6747
8416
  }
6748
8417
  const files = [];
6749
8418
  let bytes = 0;
6750
8419
  for (const absolute of walkFiles(skillDir)) {
6751
- const relative4 = path14.relative(skillDir, absolute).split(path14.sep).join("/");
6752
- const buffer = fs14.readFileSync(absolute);
8420
+ const relative5 = path17.relative(skillDir, absolute).split(path17.sep).join("/");
8421
+ const buffer = fs17.readFileSync(absolute);
6753
8422
  if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
6754
- notes.push({ skill: entry.name, message: `${relative4} is over 512KB \u2014 skipped` });
8423
+ notes.push({ skill: entry.name, message: `${relative5} is over 512KB \u2014 skipped` });
6755
8424
  continue;
6756
8425
  }
6757
8426
  if (!isTextBuffer(buffer)) {
6758
- notes.push({ skill: entry.name, message: `${relative4} is binary \u2014 skipped` });
8427
+ notes.push({ skill: entry.name, message: `${relative5} is binary \u2014 skipped` });
6759
8428
  continue;
6760
8429
  }
6761
- files.push({ path: relative4, contents: buffer.toString("utf8") });
8430
+ files.push({ path: relative5, contents: buffer.toString("utf8") });
6762
8431
  bytes += buffer.byteLength;
6763
8432
  }
6764
8433
  if (files.length > MAX_SKILL_FILES) {
@@ -6784,16 +8453,16 @@ var scanSkillLibrary = (root) => {
6784
8453
  };
6785
8454
 
6786
8455
  // src/ssh-setup.ts
6787
- import * as os10 from "node:os";
6788
- import * as path16 from "node:path";
8456
+ import * as os12 from "node:os";
8457
+ import * as path19 from "node:path";
6789
8458
 
6790
8459
  // ../../packages/workspace-ssh/src/index.ts
6791
8460
  import { spawnSync as spawnSync5 } from "node:child_process";
6792
8461
  import { createHash as createHash3 } from "node:crypto";
6793
- import * as fs15 from "node:fs";
6794
- import { isIP as isIP2 } from "node:net";
6795
- import * as os9 from "node:os";
6796
- import * as path15 from "node:path";
8462
+ import * as fs18 from "node:fs";
8463
+ import { isIP as isIP3 } from "node:net";
8464
+ import * as os11 from "node:os";
8465
+ import * as path18 from "node:path";
6797
8466
  var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
6798
8467
  var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
6799
8468
  var WorkspaceSshError = class extends Error {
@@ -6810,16 +8479,16 @@ var WorkspaceSshError = class extends Error {
6810
8479
  };
6811
8480
  var success = (value) => ({ ok: true, value });
6812
8481
  var failure = (error) => ({ ok: false, error });
6813
- var stripIpv6Brackets = (hostname7) => hostname7.startsWith("[") && hostname7.endsWith("]") ? hostname7.slice(1, -1) : hostname7;
8482
+ var stripIpv6Brackets = (hostname8) => hostname8.startsWith("[") && hostname8.endsWith("]") ? hostname8.slice(1, -1) : hostname8;
6814
8483
  var hashIdentity = (identity) => createHash3("sha256").update(identity).digest("hex").slice(0, 24);
6815
- var aliasLabel = (hostname7) => {
6816
- const label = hostname7.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
8484
+ var aliasLabel = (hostname8) => {
8485
+ const label = hostname8.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
6817
8486
  return label === "" ? "server" : label;
6818
8487
  };
6819
8488
  var parseHostOverride = (raw) => {
6820
- const hostname7 = raw.trim();
6821
- if (hostname7 === "") return failure(new WorkspaceSshError("target", "SSH hostname is empty."));
6822
- if (hostname7.includes("://") || /[\s/@]/.test(hostname7)) {
8489
+ const hostname8 = raw.trim();
8490
+ if (hostname8 === "") return failure(new WorkspaceSshError("target", "SSH hostname is empty."));
8491
+ if (hostname8.includes("://") || /[\s/@]/.test(hostname8)) {
6823
8492
  return failure(
6824
8493
  new WorkspaceSshError(
6825
8494
  "target",
@@ -6827,7 +8496,7 @@ var parseHostOverride = (raw) => {
6827
8496
  )
6828
8497
  );
6829
8498
  }
6830
- if (/^[^:]+:\d+$/.test(hostname7)) {
8499
+ if (/^[^:]+:\d+$/.test(hostname8)) {
6831
8500
  return failure(
6832
8501
  new WorkspaceSshError(
6833
8502
  "target",
@@ -6835,11 +8504,11 @@ var parseHostOverride = (raw) => {
6835
8504
  )
6836
8505
  );
6837
8506
  }
6838
- if ((hostname7.startsWith("[") || hostname7.endsWith("]")) && !/^\[[^\]]+\]$/.test(hostname7)) {
8507
+ if ((hostname8.startsWith("[") || hostname8.endsWith("]")) && !/^\[[^\]]+\]$/.test(hostname8)) {
6839
8508
  return failure(new WorkspaceSshError("target", "SSH hostname contains invalid IPv6 brackets."));
6840
8509
  }
6841
- const unbracketed = stripIpv6Brackets(hostname7);
6842
- if ((hostname7.startsWith("[") || unbracketed.includes(":")) && isIP2(unbracketed) !== 6) {
8510
+ const unbracketed = stripIpv6Brackets(hostname8);
8511
+ if ((hostname8.startsWith("[") || unbracketed.includes(":")) && isIP3(unbracketed) !== 6) {
6843
8512
  return failure(
6844
8513
  new WorkspaceSshError("target", "SSH hostname contains an invalid IPv6 address.")
6845
8514
  );
@@ -6880,8 +8549,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
6880
8549
  var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
6881
8550
  var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
6882
8551
  var parseIdentityPath = (value) => {
6883
- const resolved = path15.resolve(
6884
- value.startsWith("~/") ? path15.join(os9.homedir(), value.slice(2)) : value
8552
+ const resolved = path18.resolve(
8553
+ value.startsWith("~/") ? path18.join(os11.homedir(), value.slice(2)) : value
6885
8554
  );
6886
8555
  if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
6887
8556
  return failure(
@@ -7037,7 +8706,7 @@ var agentKeys = () => {
7037
8706
  };
7038
8707
  var readText = (file) => {
7039
8708
  try {
7040
- return success(fs15.readFileSync(file, "utf8"));
8709
+ return success(fs18.readFileSync(file, "utf8"));
7041
8710
  } catch (cause) {
7042
8711
  const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
7043
8712
  return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
@@ -7095,19 +8764,19 @@ var readRequiredKey = (keyPath, source) => {
7095
8764
  var readOptionalKey = (keyPath) => {
7096
8765
  const parsed = parseIdentityPath(keyPath);
7097
8766
  if (!parsed.ok) return parsed;
7098
- if (!fs15.existsSync(parsed.value) && !fs15.existsSync(publicKeyPath(parsed.value)))
8767
+ if (!fs18.existsSync(parsed.value) && !fs18.existsSync(publicKeyPath(parsed.value)))
7099
8768
  return success(null);
7100
8769
  return readRequiredKey(parsed.value, "existing");
7101
8770
  };
7102
8771
  var pinAgentKey = (configHome, publicKey) => {
7103
- const identityFile = path15.resolve(
8772
+ const identityFile = path18.resolve(
7104
8773
  configHome,
7105
8774
  "ssh",
7106
8775
  `agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
7107
8776
  );
7108
8777
  try {
7109
- fs15.mkdirSync(path15.dirname(identityFile), { recursive: true, mode: 448 });
7110
- fs15.writeFileSync(identityFile, `${publicKey}
8778
+ fs18.mkdirSync(path18.dirname(identityFile), { recursive: true, mode: 448 });
8779
+ fs18.writeFileSync(identityFile, `${publicKey}
7111
8780
  `, { mode: 384 });
7112
8781
  return success({ publicKey, identityFile, source: "agent" });
7113
8782
  } catch (cause) {
@@ -7118,15 +8787,15 @@ var pinAgentKey = (configHome, publicKey) => {
7118
8787
  };
7119
8788
  var generateDedicatedKey = (privatePath) => {
7120
8789
  try {
7121
- fs15.mkdirSync(path15.dirname(privatePath), { recursive: true, mode: 448 });
8790
+ fs18.mkdirSync(path18.dirname(privatePath), { recursive: true, mode: 448 });
7122
8791
  } catch (cause) {
7123
8792
  return failure(
7124
- new WorkspaceSshError("key", `Could not create ${path15.dirname(privatePath)}.`, cause)
8793
+ new WorkspaceSshError("key", `Could not create ${path18.dirname(privatePath)}.`, cause)
7125
8794
  );
7126
8795
  }
7127
8796
  const generated = spawnSync5(
7128
8797
  "ssh-keygen",
7129
- ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os9.hostname()}`],
8798
+ ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os11.hostname()}`],
7130
8799
  { encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
7131
8800
  );
7132
8801
  if (generated.status !== 0) {
@@ -7150,7 +8819,7 @@ var generateDedicatedKey = (privatePath) => {
7150
8819
  return fresh;
7151
8820
  };
7152
8821
  var dedicatedKey = (configHome, create) => {
7153
- const privatePath = path15.join(configHome, "ssh", "id_ed25519");
8822
+ const privatePath = path18.join(configHome, "ssh", "id_ed25519");
7154
8823
  const existing = readOptionalKey(privatePath);
7155
8824
  if (existing.ok === false) return failure(existing.error);
7156
8825
  if (existing.value !== null || !create) return existing;
@@ -7198,10 +8867,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
7198
8867
  const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
7199
8868
  if (reconciled.ok === false) return failure(reconciled.error);
7200
8869
  try {
7201
- fs15.mkdirSync(path15.dirname(configFile), { recursive: true, mode: 448 });
7202
- fs15.chmodSync(path15.dirname(configFile), 448);
7203
- fs15.writeFileSync(configFile, reconciled.value, { mode: 384 });
7204
- fs15.chmodSync(configFile, 384);
8870
+ fs18.mkdirSync(path18.dirname(configFile), { recursive: true, mode: 448 });
8871
+ fs18.chmodSync(path18.dirname(configFile), 448);
8872
+ fs18.writeFileSync(configFile, reconciled.value, { mode: 384 });
8873
+ fs18.chmodSync(configFile, 384);
7205
8874
  return success(void 0);
7206
8875
  } catch (cause) {
7207
8876
  return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
@@ -7214,7 +8883,7 @@ var dim5 = ansi("2");
7214
8883
  var green5 = ansi("32");
7215
8884
  var warn = ansi("33");
7216
8885
  var say5 = (line) => console.log(line);
7217
- var sshConfigPath = () => path16.join(os10.homedir(), ".ssh", "config");
8886
+ var sshConfigPath = () => path19.join(os12.homedir(), ".ssh", "config");
7218
8887
  var flagValue2 = (args, flag) => {
7219
8888
  const index = args.indexOf(flag);
7220
8889
  return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
@@ -7284,7 +8953,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
7284
8953
  if (picked.value === null) return showFailure("No workspace SSH key is available.");
7285
8954
  const registered = await api2("POST", "/workspace-ssh/keys", {
7286
8955
  publicKey: picked.value.publicKey,
7287
- name: os10.hostname()
8956
+ name: os12.hostname()
7288
8957
  });
7289
8958
  const sourceLabel = {
7290
8959
  explicit: "from --key",
@@ -7326,8 +8995,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
7326
8995
  };
7327
8996
 
7328
8997
  // src/uninstall.ts
7329
- import * as fs16 from "node:fs";
7330
- import * as path17 from "node:path";
8998
+ import * as fs19 from "node:fs";
8999
+ import * as path20 from "node:path";
7331
9000
  var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
7332
9001
  var parseUninstallArgs = (args) => {
7333
9002
  let scope = null;
@@ -7349,14 +9018,14 @@ var parseUninstallArgs = (args) => {
7349
9018
  var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
7350
9019
  var countEntries = (directory) => {
7351
9020
  try {
7352
- return fs16.readdirSync(directory).length;
9021
+ return fs19.readdirSync(directory).length;
7353
9022
  } catch {
7354
9023
  return 0;
7355
9024
  }
7356
9025
  };
7357
9026
  var exists = (file) => {
7358
9027
  try {
7359
- fs16.lstatSync(file);
9028
+ fs19.lstatSync(file);
7360
9029
  return true;
7361
9030
  } catch {
7362
9031
  return false;
@@ -7365,7 +9034,7 @@ var exists = (file) => {
7365
9034
  var managedBlockCount = (sshConfigFile) => {
7366
9035
  let config;
7367
9036
  try {
7368
- config = fs16.readFileSync(sshConfigFile, "utf8");
9037
+ config = fs19.readFileSync(sshConfigFile, "utf8");
7369
9038
  } catch {
7370
9039
  return 0;
7371
9040
  }
@@ -7389,27 +9058,29 @@ var describeUninstall = async (runtime, scope) => {
7389
9058
  if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
7390
9059
  server = "none";
7391
9060
  } else if (read.value === null) {
7392
- server = exists(path17.join(configDir, "identity.env")) ? {
9061
+ server = exists(path20.join(configDir, "identity.env")) ? {
7393
9062
  version: "(no active generation)",
7394
9063
  appUrl: "",
7395
9064
  dockerContext: "",
7396
- generations: countEntries(path17.join(configDir, "generations")),
7397
- backups: countEntries(path17.join(configDir, "backups"))
9065
+ edgeHost: null,
9066
+ generations: countEntries(path20.join(configDir, "generations")),
9067
+ backups: countEntries(path20.join(configDir, "backups"))
7398
9068
  } : "none";
7399
9069
  } else {
7400
9070
  server = {
7401
9071
  version: read.value.config.serverVersion,
7402
9072
  appUrl: read.value.config.appUrl,
7403
9073
  dockerContext: read.value.config.dockerContext,
7404
- generations: countEntries(path17.join(configDir, "generations")),
7405
- backups: countEntries(path17.join(configDir, "backups"))
9074
+ edgeHost: read.value.config.edgeHost ?? null,
9075
+ generations: countEntries(path20.join(configDir, "generations")),
9076
+ backups: countEntries(path20.join(configDir, "backups"))
7406
9077
  };
7407
9078
  }
7408
9079
  }
7409
9080
  let home = null;
7410
9081
  if (scope !== "server") {
7411
- const cliConfig = path17.join(runtime.cliHome, "cli.json");
7412
- const sshDirectory = path17.join(runtime.cliHome, "ssh");
9082
+ const cliConfig = path20.join(runtime.cliHome, "cli.json");
9083
+ const sshDirectory = path20.join(runtime.cliHome, "ssh");
7413
9084
  home = {
7414
9085
  cliConfig: exists(cliConfig) ? cliConfig : null,
7415
9086
  sshDirectory: exists(sshDirectory) ? sshDirectory : null,
@@ -7425,13 +9096,15 @@ var planLines = (plan, configDir) => {
7425
9096
  if (plan.server === "none") {
7426
9097
  lines.push(`server none installed under ${configDir}`);
7427
9098
  } else if (plan.server !== null) {
7428
- const { version, appUrl, dockerContext, generations, backups } = plan.server;
9099
+ const { version, appUrl, dockerContext, edgeHost, generations, backups } = plan.server;
7429
9100
  lines.push(
7430
9101
  `server Mend ${version}${appUrl === "" ? "" : ` at ${appUrl}`}${dockerContext === "" ? "" : ` \xB7 docker context ${dockerContext}`}`
7431
9102
  );
7432
9103
  if (dockerContext !== "") {
9104
+ const edge = edgeHost === null ? [] : ["edge"];
9105
+ const edgeVolumes = edgeHost === null ? [] : ["mend-edge-data", "mend-edge-config"];
7433
9106
  lines.push(
7434
- ` containers mend, postgres, garage \xB7 volumes ${[MEND_DOCKER_NAMESPACE_WITH_GARAGE.store, ...secondaryVolumesOf(MEND_DOCKER_NAMESPACE_WITH_GARAGE), "mend-config", "mend-ssh", "mend-postgres"].join(", ")} \xB7 image ghcr.io/sealant-sh/mend:${version}`
9107
+ ` containers ${["mend", "postgres", "garage", ...edge].join(", ")} \xB7 volumes ${[MEND_DOCKER_NAMESPACE_WITH_GARAGE.store, ...secondaryVolumesOf(MEND_DOCKER_NAMESPACE_WITH_GARAGE), "mend-config", "mend-ssh", "mend-postgres", ...edgeVolumes].join(", ")} \xB7 image ghcr.io/sealant-sh/mend:${version}${edgeHost === null ? "" : ` \xB7 the edge for ${edgeHost}`}`
7435
9108
  );
7436
9109
  }
7437
9110
  lines.push(
@@ -7473,13 +9146,14 @@ var removeServer = async (runtime, failures, leftovers) => {
7473
9146
  const context = installation.config.dockerContext;
7474
9147
  const down = await server.run(
7475
9148
  "docker",
7476
- serverComposeArgs({ directory: installation.directory, dockerContext: context }, [
7477
- "down",
7478
- "--volumes",
7479
- "--remove-orphans",
7480
- "--timeout",
7481
- "30"
7482
- ]),
9149
+ serverComposeArgs(
9150
+ {
9151
+ directory: installation.directory,
9152
+ dockerContext: context,
9153
+ overlays: composeOverlays(installation.config)
9154
+ },
9155
+ ["down", "--volumes", "--remove-orphans", "--timeout", "30"]
9156
+ ),
7483
9157
  { timeoutMs: serverProcessDeadlines.stop }
7484
9158
  );
7485
9159
  if (down.status !== 0) {
@@ -7541,7 +9215,7 @@ var removeServer = async (runtime, failures, leftovers) => {
7541
9215
  }
7542
9216
  }
7543
9217
  for (const name of SERVER_FILES) {
7544
- fs16.rmSync(path17.join(configDir, name), { recursive: true, force: true });
9218
+ fs19.rmSync(path20.join(configDir, name), { recursive: true, force: true });
7545
9219
  }
7546
9220
  server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
7547
9221
  },
@@ -7552,20 +9226,20 @@ var removeServer = async (runtime, failures, leftovers) => {
7552
9226
  var removeHome = async (runtime, plan, failures, leftovers) => {
7553
9227
  const { server } = runtime;
7554
9228
  if (plan.cliConfig !== null) {
7555
- fs16.rmSync(plan.cliConfig, { force: true });
9229
+ fs19.rmSync(plan.cliConfig, { force: true });
7556
9230
  server.writeLine(`removed ${plan.cliConfig}`);
7557
9231
  }
7558
9232
  if (plan.sshDirectory !== null) {
7559
- fs16.rmSync(plan.sshDirectory, { recursive: true, force: true });
9233
+ fs19.rmSync(plan.sshDirectory, { recursive: true, force: true });
7560
9234
  server.writeLine(`removed ${plan.sshDirectory}`);
7561
9235
  }
7562
9236
  if (plan.managedSshBlocks > 0) {
7563
9237
  try {
7564
- const before = fs16.readFileSync(runtime.sshConfigFile, "utf8");
9238
+ const before = fs19.readFileSync(runtime.sshConfigFile, "utf8");
7565
9239
  const stripped = stripManagedWorkspaceSshBlocks(before);
7566
9240
  if (!stripped.ok) throw stripped.error;
7567
9241
  if (stripped.value.removed > 0) {
7568
- fs16.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
9242
+ fs19.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
7569
9243
  server.writeLine(
7570
9244
  `removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
7571
9245
  );
@@ -7578,12 +9252,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
7578
9252
  }
7579
9253
  let remaining = [];
7580
9254
  try {
7581
- remaining = fs16.readdirSync(runtime.cliHome);
9255
+ remaining = fs19.readdirSync(runtime.cliHome);
7582
9256
  } catch {
7583
9257
  return;
7584
9258
  }
7585
9259
  if (remaining.length === 0) {
7586
- fs16.rmdirSync(runtime.cliHome);
9260
+ fs19.rmdirSync(runtime.cliHome);
7587
9261
  server.writeLine(`removed ${runtime.cliHome}`);
7588
9262
  } else {
7589
9263
  leftovers.push(
@@ -7644,22 +9318,103 @@ var upgradeUrl = async (input) => {
7644
9318
  return url;
7645
9319
  };
7646
9320
 
9321
+ // src/worktree-remove.ts
9322
+ var parseWorktreesRmArgs = (args) => {
9323
+ let name = null;
9324
+ let force = false;
9325
+ let project2 = null;
9326
+ for (let index = 0; index < args.length; index += 1) {
9327
+ const arg = args[index];
9328
+ if (arg === void 0) continue;
9329
+ if (arg === "--force") {
9330
+ force = true;
9331
+ continue;
9332
+ }
9333
+ if (arg === "--project") {
9334
+ const value = args[index + 1];
9335
+ if (value === void 0 || value.startsWith("--")) return { error: "--project needs a name" };
9336
+ project2 = value;
9337
+ index += 1;
9338
+ continue;
9339
+ }
9340
+ if (arg.startsWith("--"))
9341
+ return { error: `unknown option ${arg} \xB7 ${usageOf("worktrees rm")}` };
9342
+ if (name !== null) return { error: `one worktree at a time \xB7 ${usageOf("worktrees rm")}` };
9343
+ name = arg;
9344
+ }
9345
+ if (name === null) return { error: usageOf("worktrees rm") };
9346
+ return { args: { name, force, project: project2 } };
9347
+ };
9348
+ var plural2 = (count, one, many) => `${count} ${count === 1 ? one : many}`;
9349
+ var pickWorktree = (worktrees, name, liveStatuses) => {
9350
+ const matches = worktrees.filter((worktree2) => worktree2.name === name);
9351
+ const [worktree] = matches;
9352
+ if (worktree === void 0)
9353
+ return { error: `no worktree named ${name} \xB7 mend worktrees lists them` };
9354
+ if (matches.length > 1) {
9355
+ return {
9356
+ error: `${name} names a worktree in ${matches.length} projects: ${matches.map((match) => match.projectName).join(", ")} \xB7 say which with --project`
9357
+ };
9358
+ }
9359
+ const id = worktree.id;
9360
+ if (id === null) {
9361
+ return { error: `this server predates worktrees \xB7 mend sessions lists what it holds` };
9362
+ }
9363
+ const live = worktree.sessions.filter((session) => liveStatuses.has(session.status));
9364
+ if (live.length > 0) {
9365
+ return {
9366
+ error: `${plural2(live.length, "session", "sessions")} live in ${name} \xB7 stop ${live.length === 1 ? "it" : "them"} first: ${live.map((session) => `mend stop ${session.id.slice(0, 8)}`).join(" \xB7 ")}`
9367
+ };
9368
+ }
9369
+ return { worktree: { ...worktree, id } };
9370
+ };
9371
+ var removalRoute = (id, force) => `/worktrees/${encodeURIComponent(id)}${force ? "?force=true" : ""}`;
9372
+ var refusalLines = (words, args) => words.includes(WORKTREE_REMOVAL_FORCE_HINT) ? [
9373
+ words,
9374
+ ` mend worktrees rm ${args.name}${args.project === null ? "" : ` --project ${args.project}`} --force removes it anyway`
9375
+ ] : [words];
9376
+ var removedLine = (name) => `removed \xB7 ${name} \xB7 its sessions, change, checkpoints and review went with it`;
9377
+ var worktreesRmCommand = async (deps, argv) => {
9378
+ const parsed = parseWorktreesRmArgs(argv);
9379
+ if ("error" in parsed) return deps.fail(parsed.error);
9380
+ const { args } = parsed;
9381
+ const picked = pickWorktree(await deps.listWorktrees(args.project), args.name, deps.liveStatuses);
9382
+ if ("error" in picked) return deps.fail(picked.error);
9383
+ const { worktree } = picked;
9384
+ let report;
9385
+ try {
9386
+ report = await deps.request("DELETE", removalRoute(worktree.id, args.force));
9387
+ } catch (error) {
9388
+ if (error instanceof MendRequestError && error.status === 422) {
9389
+ return deps.fail(refusalLines(error.message, args).join("\n"));
9390
+ }
9391
+ return deps.fail(error instanceof Error ? error.message : String(error));
9392
+ }
9393
+ if (!report.removed) {
9394
+ return deps.fail(
9395
+ `not removed \xB7 ${args.name}${report.leftover === null ? "" : ` \xB7 ${report.leftover}`}`
9396
+ );
9397
+ }
9398
+ deps.say(removedLine(args.name));
9399
+ if (report.leftover !== null) deps.say(`left behind \xB7 ${report.leftover}`);
9400
+ };
9401
+
7647
9402
  // src/main.ts
7648
9403
  var mendCliHome = () => {
7649
9404
  const xdg = process.env["XDG_CONFIG_HOME"];
7650
- const preferred = path18.join(
7651
- xdg === void 0 || xdg === "" ? path18.join(os11.homedir(), ".config") : xdg,
9405
+ const preferred = path21.join(
9406
+ xdg === void 0 || xdg === "" ? path21.join(os13.homedir(), ".config") : xdg,
7652
9407
  "mend"
7653
9408
  );
7654
- const legacy = path18.join(os11.homedir(), ".mend");
7655
- return !fs17.existsSync(preferred) && fs17.existsSync(legacy) ? legacy : preferred;
9409
+ const legacy = path21.join(os13.homedir(), ".mend");
9410
+ return !fs20.existsSync(preferred) && fs20.existsSync(legacy) ? legacy : preferred;
7656
9411
  };
7657
- var CONFIG_PATH = path18.join(mendCliHome(), "cli.json");
9412
+ var CONFIG_PATH = path21.join(mendCliHome(), "cli.json");
7658
9413
  var loadConfig = () => {
7659
9414
  let fileConfig = {};
7660
- if (fs17.existsSync(CONFIG_PATH)) {
9415
+ if (fs20.existsSync(CONFIG_PATH)) {
7661
9416
  try {
7662
- fileConfig = JSON.parse(fs17.readFileSync(CONFIG_PATH, "utf8"));
9417
+ fileConfig = JSON.parse(fs20.readFileSync(CONFIG_PATH, "utf8"));
7663
9418
  } catch {
7664
9419
  fail6(`could not parse ${CONFIG_PATH}`);
7665
9420
  }
@@ -7849,10 +9604,10 @@ var withSpinner = async (label, work) => {
7849
9604
  var followStarting = async (config, sessionId, label, start) => {
7850
9605
  let line = label;
7851
9606
  const timers = /* @__PURE__ */ new Set();
7852
- const sleep3 = (ms) => new Promise((resolve7) => {
9607
+ const sleep3 = (ms) => new Promise((resolve8) => {
7853
9608
  const timer = setTimeout(() => {
7854
9609
  timers.delete(timer);
7855
- resolve7();
9610
+ resolve8();
7856
9611
  }, ms);
7857
9612
  timers.add(timer);
7858
9613
  });
@@ -7911,7 +9666,7 @@ var adopt = async (config, args) => {
7911
9666
  }
7912
9667
  const sourceIssue = repositoryCloneUrlIssue(source);
7913
9668
  if (sourceIssue !== null) return fail6(sourceIssue);
7914
- const projectName = name ?? normalizeProjectName(path18.basename(source, ".git"));
9669
+ const projectName = name ?? normalizeProjectName(path21.basename(source, ".git"));
7915
9670
  const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
7916
9671
  if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
7917
9672
  return fail6(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
@@ -7935,6 +9690,14 @@ var adopt = async (config, args) => {
7935
9690
  say6(
7936
9691
  `${dim6(" sessions start with:")} mend codex ${dim6("(from anywhere \u2014")} --project ${project2.name}${dim6(")")}`
7937
9692
  );
9693
+ const repoRoot = positional[0] === void 0 ? cwdFacts(process.cwd()).repoRoot : null;
9694
+ const memory = repoRoot === null ? null : claudeMemoryDirFor(repoRoot);
9695
+ if (memory !== null) {
9696
+ const count = scanClaudeMemory(memory).files.length;
9697
+ say6(
9698
+ `${dim6(" claude memory")} ${count} file${count === 1 ? "" : "s"} on this machine ${dim6("\u2192 mend memory import")}`
9699
+ );
9700
+ }
7938
9701
  };
7939
9702
  var findProject = async (config, explicit, adoptCwd = false) => {
7940
9703
  const projects = await api(config, "GET", "/projects");
@@ -7960,7 +9723,7 @@ var findProject = async (config, explicit, adoptCwd = false) => {
7960
9723
  const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
7961
9724
  if (sourceIssue !== null) return fail6(sourceIssue);
7962
9725
  const adopted = await api(config, "POST", "/projects", {
7963
- name: normalizeProjectName(path18.basename(facts.repoRoot)),
9726
+ name: normalizeProjectName(path21.basename(facts.repoRoot)),
7964
9727
  source: facts.originUrl
7965
9728
  });
7966
9729
  say6(`${green6("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim6(adopted.storePath)}`);
@@ -8086,13 +9849,13 @@ var launch = async (config, harness, args) => {
8086
9849
  if (harness === "run") {
8087
9850
  return supervisedRun(config, session, argv);
8088
9851
  }
8089
- const launchBody = structured ? {
9852
+ const launchBody = {
8090
9853
  ...parsed.prompt === null ? {} : { prompt: parsed.prompt },
8091
9854
  ...parsed.model === null ? {} : { model: parsed.model },
8092
9855
  ...parsed.effort === null ? {} : { effort: parsed.effort },
8093
9856
  ...parsed.ask ? { permissionMode: "ask" } : {},
8094
9857
  ...parsed.fast ? { speed: "fast" } : {}
8095
- } : { argv };
9858
+ };
8096
9859
  await startAndFollow(
8097
9860
  config,
8098
9861
  session.id,
@@ -8147,6 +9910,7 @@ var takeTerminal = () => {
8147
9910
  }
8148
9911
  return { raw: true };
8149
9912
  };
9913
+ var WATCH_READ_GRACE_MS = 3e3;
8150
9914
  var terminalSize = () => ({
8151
9915
  cols: process.stdout.columns || 80,
8152
9916
  rows: process.stdout.rows || 24
@@ -8161,12 +9925,13 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
8161
9925
  connecting.abort();
8162
9926
  };
8163
9927
  let ws = null;
9928
+ let watching = false;
8164
9929
  let detached = false;
8165
9930
  let sawEnd = false;
8166
9931
  let interrupted = false;
8167
9932
  let finishAttachment;
8168
- const finished = new Promise((resolve7) => {
8169
- finishAttachment = resolve7;
9933
+ const finished = new Promise((resolve8) => {
9934
+ finishAttachment = resolve8;
8170
9935
  });
8171
9936
  const onSignal = () => {
8172
9937
  interrupted = true;
@@ -8176,7 +9941,7 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
8176
9941
  };
8177
9942
  const signals = ["SIGHUP", "SIGINT", "SIGTERM"];
8178
9943
  const sendResize = (size = terminalSize()) => {
8179
- if (ws === null || ws.readyState !== WebSocket.OPEN) return;
9944
+ if (watching || ws === null || ws.readyState !== WebSocket.OPEN) return;
8180
9945
  ws.send(JSON.stringify({ t: "resize", cols: size.cols, rows: size.rows }));
8181
9946
  };
8182
9947
  const onWinch = () => sendResize();
@@ -8211,6 +9976,13 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
8211
9976
  else if (data.includes(3)) abort("interrupted");
8212
9977
  return;
8213
9978
  }
9979
+ if (watching) {
9980
+ if (watchKey(data, detachKeyEnabled) === null) return;
9981
+ detached = true;
9982
+ ws.close();
9983
+ finishAttachment?.();
9984
+ return;
9985
+ }
8214
9986
  if (detachKeyEnabled && isDetachChunk(data)) {
8215
9987
  detached = true;
8216
9988
  ws.close();
@@ -8258,12 +10030,12 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
8258
10030
  if (connecting.signal.aborted) return "aborted";
8259
10031
  const socket = new WebSocket(url);
8260
10032
  socket.binaryType = "arraybuffer";
8261
- const opened = await new Promise((resolve7) => {
8262
- const onAbort = () => resolve7(false);
10033
+ const opened = await new Promise((resolve8) => {
10034
+ const onAbort = () => resolve8(false);
8263
10035
  connecting.signal.addEventListener("abort", onAbort, { once: true });
8264
10036
  const settle = (value) => {
8265
10037
  connecting.signal.removeEventListener("abort", onAbort);
8266
- resolve7(value);
10038
+ resolve8(value);
8267
10039
  };
8268
10040
  socket.addEventListener("open", () => settle(true), { once: true });
8269
10041
  socket.addEventListener("error", () => settle(false), { once: true });
@@ -8272,6 +10044,7 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
8272
10044
  socket.close();
8273
10045
  return connecting.signal.aborted ? "aborted" : "unavailable";
8274
10046
  };
10047
+ const detailRead = interactive ? request(config, "GET", `/sessions/${sessionId}`).catch(() => null) : Promise.resolve(null);
8275
10048
  const started = Date.now();
8276
10049
  let connectingShown = false;
8277
10050
  const connectingTimer = setInterval(() => {
@@ -8292,6 +10065,24 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
8292
10065
  process.stdin.on("data", onKeys);
8293
10066
  process.stdin.resume();
8294
10067
  }
10068
+ let graceTimer;
10069
+ const detail = await Promise.race([
10070
+ detailRead,
10071
+ new Promise((resolve8) => {
10072
+ graceTimer = setTimeout(() => resolve8(null), WATCH_READ_GRACE_MS);
10073
+ })
10074
+ ]);
10075
+ clearTimeout(graceTimer);
10076
+ if (watchesTerminal(detail)) {
10077
+ watching = true;
10078
+ const members = await request(config, "GET", "/organization/members").catch(() => []);
10079
+ process.stdout.write(
10080
+ `\r\x1B[2K${dim6(watchNotice(ownerNameOf(detail?.session.ownerUserId, members)))}\r
10081
+ ${dim6(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}\r
10082
+ \r
10083
+ `
10084
+ );
10085
+ }
8295
10086
  const connected = await connect2();
8296
10087
  stopConnecting();
8297
10088
  if (connected === "unavailable") return "unavailable";
@@ -8305,10 +10096,12 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
8305
10096
  cancelHint: detachKeyEnabled ? "Ctrl+] detaches" : ""
8306
10097
  });
8307
10098
  starting = gate;
8308
- void request(config, "GET", `/sessions/${sessionId}`).then(
8309
- (detail) => gate.update(agentStartingFacts(detail.currentAgent, detail.processes, Date.now())),
8310
- () => void 0
8311
- );
10099
+ void detailRead.then((read) => {
10100
+ if (read !== null) {
10101
+ gate.update(agentStartingFacts(read.currentAgent, read.processes, Date.now()));
10102
+ }
10103
+ return null;
10104
+ });
8312
10105
  }
8313
10106
  stopHerdrHint = hintHerdrAttachment(harness);
8314
10107
  const onClose = () => finishAttachment?.();
@@ -8477,7 +10270,7 @@ var stopCommand = async (config, args) => {
8477
10270
  const all = args.includes("--all");
8478
10271
  const projectFlag = args.indexOf("--project");
8479
10272
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
8480
- const prefix = args.find((arg, index) => !arg.startsWith("--") && index !== projectFlag + 1);
10273
+ const prefix = firstPositional(args, ["--project"]);
8481
10274
  if (!all && prefix === void 0 && projectName === null) {
8482
10275
  const picked = await resolveLiveSession(config, void 0, "stop");
8483
10276
  await stopSessions(config, [picked], false);
@@ -8581,7 +10374,7 @@ var resolveLiveSession = async (config, prefix, command) => {
8581
10374
  const candidates = project2 === void 0 ? sessions : sessions.filter((s) => s.projectId === project2.id);
8582
10375
  const only = candidates[0];
8583
10376
  if (only === void 0) {
8584
- return fail6("no live or retained session \u2014 start one with mend codex|claude|opencode");
10377
+ return fail6("no live or retained session \u2014 start one with mend codex|claude|opencode|pi");
8585
10378
  }
8586
10379
  if (candidates.length === 1) return only;
8587
10380
  if (process.stdin.isTTY !== true) {
@@ -8854,9 +10647,7 @@ var serviceRun = async (config, args) => {
8854
10647
  var serviceLogs = async (config, args) => {
8855
10648
  const fromFlag = args.indexOf("--from");
8856
10649
  const from = fromFlag === -1 ? "0" : args[fromFlag + 1];
8857
- const needle = args.find(
8858
- (argument, index) => !argument.startsWith("--") && index !== fromFlag + 1
8859
- );
10650
+ const needle = firstPositional(args, ["--from"]);
8860
10651
  if (needle === void 0 || from === void 0 || !/^(0|[1-9]\d*)$/.test(from)) {
8861
10652
  return fail6(usageOf("service logs"));
8862
10653
  }
@@ -8903,7 +10694,7 @@ var serviceLogs = async (config, args) => {
8903
10694
  cursor = page.nextFrom;
8904
10695
  const ended = page.status === "exited" || page.status === "failed";
8905
10696
  if (ended && !advanced) break;
8906
- if (!advanced) await new Promise((resolve7) => setTimeout(resolve7, 250));
10697
+ if (!advanced) await new Promise((resolve8) => setTimeout(resolve8, 250));
8907
10698
  }
8908
10699
  say6("");
8909
10700
  say6(dim6(`stream ended \xB7 next sequence ${cursor}`));
@@ -8921,13 +10712,13 @@ var serviceRestart = async (config, args) => {
8921
10712
  };
8922
10713
  var serviceInit = async (args) => {
8923
10714
  const root = gitTopLevel(process.cwd()) ?? process.cwd();
8924
- const target = path18.join(root, "mend.toml");
8925
- if (fs17.existsSync(target)) {
10715
+ const target = path21.join(root, "mend.toml");
10716
+ if (fs20.existsSync(target)) {
8926
10717
  return fail6(`${target} already exists \u2014 edit it directly (init never merges)`);
8927
10718
  }
8928
- const rootFiles = fs17.readdirSync(root);
10719
+ const rootFiles = fs20.readdirSync(root);
8929
10720
  const proposals = [];
8930
- const readRoot = (name) => fs17.readFileSync(path18.join(root, name), "utf8");
10721
+ const readRoot = (name) => fs20.readFileSync(path21.join(root, name), "utf8");
8931
10722
  if (rootFiles.includes("package.json")) {
8932
10723
  proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
8933
10724
  }
@@ -8937,13 +10728,13 @@ var serviceInit = async (args) => {
8937
10728
  );
8938
10729
  for (const glob of globs) {
8939
10730
  const parent = glob.replace(/\/?\*+$/, "");
8940
- const dirs = glob.endsWith("*") ? fs17.existsSync(path18.join(root, parent)) ? fs17.readdirSync(path18.join(root, parent)).map((dir) => path18.join(parent, dir)) : [] : [glob];
10731
+ const dirs = glob.endsWith("*") ? fs20.existsSync(path21.join(root, parent)) ? fs20.readdirSync(path21.join(root, parent)).map((dir) => path21.join(parent, dir)) : [] : [glob];
8941
10732
  for (const dir of dirs) {
8942
- const manifest = path18.join(root, dir, "package.json");
8943
- if (!fs17.existsSync(manifest)) continue;
10733
+ const manifest = path21.join(root, dir, "package.json");
10734
+ if (!fs20.existsSync(manifest)) continue;
8944
10735
  for (const proposal of proposeFromWorkspacePackage(
8945
- path18.basename(dir),
8946
- fs17.readFileSync(manifest, "utf8"),
10736
+ path21.basename(dir),
10737
+ fs20.readFileSync(manifest, "utf8"),
8947
10738
  rootFiles
8948
10739
  )) {
8949
10740
  if (!proposals.some((existing) => existing.name === proposal.name)) {
@@ -8987,13 +10778,13 @@ var serviceInit = async (args) => {
8987
10778
  return;
8988
10779
  }
8989
10780
  }
8990
- fs17.writeFileSync(target, toml);
10781
+ fs20.writeFileSync(target, toml);
8991
10782
  say6(`${green6("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
8992
10783
  };
8993
10784
  var tunnelServices = async (config, services, portOverride) => {
8994
10785
  for (const service of services) {
8995
10786
  const port = portOverride ?? service.hostPort ?? service.workspacePort;
8996
- const server = net4.createServer(
10787
+ const server = net5.createServer(
8997
10788
  (socket) => pumpConnection(socket, tunnelUrlFor(config, service.id))
8998
10789
  );
8999
10790
  await listenLocal(server, port, false).catch(
@@ -9035,7 +10826,7 @@ var attachTunnels = (config, optOut) => {
9035
10826
  start: async (sessionId) => {
9036
10827
  await Promise.race([
9037
10828
  tunnels.focus(sessionId),
9038
- new Promise((resolve7) => setTimeout(resolve7, 3e3))
10829
+ new Promise((resolve8) => setTimeout(resolve8, 3e3))
9039
10830
  ]);
9040
10831
  },
9041
10832
  attached: (work) => {
@@ -9090,19 +10881,173 @@ var serviceCommand = async (config, args) => {
9090
10881
  return serviceRun(config, [verb, ...rest]);
9091
10882
  }
9092
10883
  };
10884
+ var kilobytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
10885
+ var memoryPathOf = (name) => {
10886
+ if (name.startsWith(".")) return name;
10887
+ const single = AGENT_MEMORY_FILES.find((file) => file.path.endsWith(`/${name}`));
10888
+ if (single !== void 0) return single.path;
10889
+ const qualified = /^([a-z]+):(.+)$/.exec(name);
10890
+ const harness = qualified?.[1] ?? "claude";
10891
+ const rest = qualified?.[2] ?? name;
10892
+ const root = AGENT_MEMORY_ROOTS.find((candidate) => candidate.harness === harness)?.root;
10893
+ return root === void 0 ? name : `${root}/${rest}`;
10894
+ };
10895
+ var memoryImport = async (config, args) => {
10896
+ const repoRoot = cwdFacts(process.cwd()).repoRoot;
10897
+ if (repoRoot === null) return fail6("run mend memory import inside the repository's checkout");
10898
+ const claudeDir = claudeMemoryDirFor(repoRoot);
10899
+ const claude = claudeDir === null ? null : scanClaudeMemory(claudeDir);
10900
+ const codex = await scanCodexMemory(repoRoot);
10901
+ for (const note of [...claude?.notes ?? [], ...codex.notes]) say6(dim6(` ${note}`));
10902
+ const files = [...claude?.files ?? [], ...codex.files];
10903
+ if (files.length === 0) {
10904
+ return fail6(`neither claude nor codex keeps memory for ${repoRoot} on this machine`);
10905
+ }
10906
+ const project2 = await findProject(config, takeFlagValue4(args, "--project"));
10907
+ if (claude !== null && claude.files.length > 0) {
10908
+ say6(
10909
+ `claude memory \xB7 ${claude.dir} \xB7 ${claude.files.length} file${claude.files.length === 1 ? "" : "s"}`
10910
+ );
10911
+ }
10912
+ if (codex.files.length > 0) {
10913
+ say6(
10914
+ `codex memory \xB7 ${codex.home} \xB7 ${codex.summaries} conversation summar${codex.summaries === 1 ? "y" : "ies"} of this repository`
10915
+ );
10916
+ }
10917
+ const dryRun = args.includes("--dry-run");
10918
+ const source = importSourceFor(mendCliHome(), repoRoot, { create: !dryRun });
10919
+ const report = await withSpinner(
10920
+ dryRun ? "planning" : "importing",
10921
+ api(
10922
+ config,
10923
+ "POST",
10924
+ `/projects/${project2.id}/memory/import${dryRun ? "/plan" : ""}`,
10925
+ source === null ? { files } : { files, source }
10926
+ )
10927
+ );
10928
+ say6(
10929
+ `${dryRun ? "would import" : green6("imported")} into ${project2.name} \xB7 ${importReportCounts(report)}`
10930
+ );
10931
+ for (const line of importReportLines(report)) say6(dim6(line));
10932
+ say6(
10933
+ dim6(
10934
+ dryRun ? "--dry-run: nothing written" : "every version replaced is kept \xB7 sessions in the project receive it from the next launch"
10935
+ )
10936
+ );
10937
+ };
10938
+ var memoryCommand = async (config, args) => {
10939
+ const [verb, ...rest] = args;
10940
+ if (verb === "import") return memoryImport(config, rest);
10941
+ const flags = verb === "show" || verb === "rm" ? rest : args;
10942
+ const project2 = await findProject(config, takeFlagValue4(flags, "--project"));
10943
+ const base = `/projects/${project2.id}/memory`;
10944
+ if (verb === "show" || verb === "rm") {
10945
+ const name = rest.find(
10946
+ (arg, index) => !arg.startsWith("--") && rest[index - 1] !== "--project"
10947
+ );
10948
+ if (name === void 0) return fail6(usageOf(`memory ${verb}`));
10949
+ const query = `?path=${encodeURIComponent(memoryPathOf(name))}`;
10950
+ if (verb === "rm") {
10951
+ const removed = await api(
10952
+ config,
10953
+ "DELETE",
10954
+ `${base}/file${query}`
10955
+ );
10956
+ return say6(
10957
+ removed.removed ? `removed ${name} \xB7 kept as a version` : `${name}: not in memory`
10958
+ );
10959
+ }
10960
+ const file = await api(
10961
+ config,
10962
+ "GET",
10963
+ `${base}/file${query}`
10964
+ );
10965
+ process.stdout.write(
10966
+ file.encoding === "utf8" ? file.contents : Buffer.from(file.contents, "base64")
10967
+ );
10968
+ return;
10969
+ }
10970
+ const view = await api(
10971
+ config,
10972
+ "GET",
10973
+ base
10974
+ );
10975
+ if (view.files.length === 0) {
10976
+ say6(
10977
+ `${project2.name}: no agent memory yet ${dim6("\xB7 mend memory import brings this machine's")}`
10978
+ );
10979
+ return;
10980
+ }
10981
+ const width = Math.max(...view.files.map((file) => file.name.length));
10982
+ for (const file of view.files) {
10983
+ say6(
10984
+ `${file.harness.padEnd(7)} ${file.name.padEnd(width)} ${kilobytes(file.bytes).padStart(8)} ${dim6(
10985
+ `${file.updatedAt.slice(0, 16).replace("T", " ")} \xB7 ${file.updatedBySession === null ? "imported" : `session ${file.updatedBySession.slice(0, 8)}`}`
10986
+ )}`
10987
+ );
10988
+ }
10989
+ };
10990
+ var secretsCommand = async (config, args) => {
10991
+ const [verb, ...rest] = args;
10992
+ if (verb === "add" || verb === "rm") {
10993
+ const given = rest.find((arg, index) => !arg.startsWith("--") && rest[index - 1] !== "--from");
10994
+ if (given === void 0) return fail6(usageOf(`secrets ${verb}`));
10995
+ const resolved = secretFilePathOf(given, os13.homedir());
10996
+ if (resolved.issue !== null) return fail6(resolved.issue);
10997
+ if (verb === "rm") {
10998
+ const removed = await api(
10999
+ config,
11000
+ "DELETE",
11001
+ `/me/secret-files?path=${encodeURIComponent(resolved.path)}`
11002
+ );
11003
+ return say6(
11004
+ removed.removed ? `removed ~/${resolved.path} ${dim6("\xB7 sessions launched from now on do not receive it")}` : `~/${resolved.path}: not kept`
11005
+ );
11006
+ }
11007
+ const from = takeFlagValue4(rest, "--from");
11008
+ let bytes;
11009
+ try {
11010
+ bytes = fs20.readFileSync(from ?? 0);
11011
+ } catch (error) {
11012
+ return fail6(
11013
+ from === null ? `nothing on stdin: pass --from <file> or pipe the content in` : `${from}: ${error instanceof Error ? error.message : String(error)}`
11014
+ );
11015
+ }
11016
+ if (bytes.byteLength === 0)
11017
+ return fail6(from === null ? "nothing on stdin" : `${from} is empty`);
11018
+ const saved = await api(config, "PUT", "/me/secret-files", secretFileUploadOf(resolved.path, bytes));
11019
+ say6(
11020
+ `${green6(saved.action)} ~/${saved.file.path} \xB7 ${saved.file.bytes} B ${dim6(
11021
+ "\xB7 sessions launched from now on receive it"
11022
+ )}`
11023
+ );
11024
+ return;
11025
+ }
11026
+ if (verb !== void 0 && verb !== "list") return fail6(usageOf("secrets"));
11027
+ const view = await api(
11028
+ config,
11029
+ "GET",
11030
+ "/me/secret-files"
11031
+ );
11032
+ if (view.files.length === 0) {
11033
+ say6(`no secret files ${dim6("\xB7 mend secrets add <path> --from <file> keeps one")}`);
11034
+ return;
11035
+ }
11036
+ for (const line of secretFileLines(view.files)) say6(line);
11037
+ };
9093
11038
  var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
9094
- var readIfExists = (file) => fs17.existsSync(file) ? fs17.readFileSync(file, "utf8") : null;
11039
+ var readIfExists = (file) => fs20.existsSync(file) ? fs20.readFileSync(file, "utf8") : null;
9095
11040
  var localCredential = (provider) => {
9096
- const home = os11.homedir();
11041
+ const home = os13.homedir();
9097
11042
  switch (provider) {
9098
11043
  case "codex":
9099
11044
  return readIfExists(
9100
- path18.join(process.env["CODEX_HOME"] ?? path18.join(home, ".codex"), "auth.json")
11045
+ path21.join(process.env["CODEX_HOME"] ?? path21.join(home, ".codex"), "auth.json")
9101
11046
  );
9102
11047
  case "claude":
9103
11048
  return readIfExists(
9104
- path18.join(
9105
- process.env["CLAUDE_CONFIG_DIR"] ?? path18.join(home, ".claude"),
11049
+ path21.join(
11050
+ process.env["CLAUDE_CONFIG_DIR"] ?? path21.join(home, ".claude"),
9106
11051
  ".credentials.json"
9107
11052
  )
9108
11053
  );
@@ -9152,7 +11097,7 @@ var claudeGrantSecret = () => {
9152
11097
  return read.kind === "missing" ? null : read.secret;
9153
11098
  };
9154
11099
  var doctorBundle = (config, args) => doctorBundleCommand(args, {
9155
- defaultDir: path18.join(mendCliHome(), "bundles"),
11100
+ defaultDir: path21.join(mendCliHome(), "bundles"),
9156
11101
  now: () => /* @__PURE__ */ new Date(),
9157
11102
  say: say6,
9158
11103
  warn: (line) => process.stderr.write(`${line}
@@ -9215,7 +11160,7 @@ var claudeGrant = async () => {
9215
11160
  fail6("claude: the login completed but Claude still reports no grant in Mend's directory");
9216
11161
  return null;
9217
11162
  }
9218
- if (status.configDirectory !== null && path18.resolve(status.configDirectory) !== path18.resolve(dir)) {
11163
+ if (status.configDirectory !== null && path21.resolve(status.configDirectory) !== path21.resolve(dir)) {
9219
11164
  fail6(
9220
11165
  `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`
9221
11166
  );
@@ -9248,7 +11193,7 @@ var claudeGrant = async () => {
9248
11193
  say6(dim6(" your own Claude login still works \xB7 verified"));
9249
11194
  }
9250
11195
  const kept = claudeGrantDir(home);
9251
- if (fs17.existsSync(kept)) {
11196
+ if (fs20.existsSync(kept)) {
9252
11197
  forgetGrant(kept);
9253
11198
  say6(dim6(` removed the copy an older mend kept in ${kept} \xB7 the server refreshes the login`));
9254
11199
  }
@@ -9257,8 +11202,38 @@ var claudeGrant = async () => {
9257
11202
  forgetGrant(dir);
9258
11203
  }
9259
11204
  };
11205
+ var connectPi = async (config, flags) => {
11206
+ if (flags.includes("--remove")) {
11207
+ const removed = await api(config, "DELETE", "/me/pi-profile");
11208
+ process.stdout.write(removed.removed ? "pi: profile removed\n" : "pi: no profile saved\n");
11209
+ return;
11210
+ }
11211
+ const scan = scanPiProfile(takeFlagValue4(flags, "--dir") ?? piAgentDir());
11212
+ if ("error" in scan) return fail6(scan.error);
11213
+ for (const line of piProfileLines(scan)) say6(line);
11214
+ const issue = validatePiProfile(scan.files);
11215
+ if (issue !== null) return fail6(`pi: ${issue}`);
11216
+ if (flags.includes("--dry-run")) {
11217
+ say6(dim6(" --dry-run: nothing sent"));
11218
+ return;
11219
+ }
11220
+ const saved = await withSpinner(
11221
+ "saving the pi profile",
11222
+ api(
11223
+ config,
11224
+ "PUT",
11225
+ "/me/pi-profile",
11226
+ { files: scan.files }
11227
+ )
11228
+ );
11229
+ process.stdout.write(
11230
+ `pi profile ${saved.changed ? "saved" : "unchanged"} \xB7 revision ${saved.profile.revision} \xB7 new pi sessions receive it
11231
+ `
11232
+ );
11233
+ };
9260
11234
  var connectCommand = async (config, args) => {
9261
11235
  const [providerArg, ...flags] = args;
11236
+ if (providerArg === "pi") return connectPi(config, flags);
9262
11237
  if (!isProvider(providerArg)) {
9263
11238
  return fail6(usageOf("connect"));
9264
11239
  }
@@ -9274,12 +11249,12 @@ var connectCommand = async (config, args) => {
9274
11249
  }
9275
11250
  let secret;
9276
11251
  if (flags.includes("--from-stdin")) {
9277
- secret = fs17.readFileSync(0, "utf8").trim();
11252
+ secret = fs20.readFileSync(0, "utf8").trim();
9278
11253
  if (secret === "") return fail6("nothing on stdin");
9279
11254
  } else if (provider === "codex" && !flags.includes("--use-my-login")) {
9280
11255
  const grant = codexGrant({
9281
11256
  cli: codexCli(),
9282
- personalAuthJson: readIfExists(path18.join(personalCodexHome(), "auth.json")),
11257
+ personalAuthJson: readIfExists(path21.join(personalCodexHome(), "auth.json")),
9283
11258
  say: say6,
9284
11259
  parent: mendCliHome()
9285
11260
  });
@@ -9329,14 +11304,14 @@ var takeFlagValue4 = (args, flag) => {
9329
11304
  return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
9330
11305
  };
9331
11306
  var saveCliConfig = (next) => {
9332
- fs17.mkdirSync(path18.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
9333
- fs17.writeFileSync(
11307
+ fs20.mkdirSync(path21.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
11308
+ fs20.writeFileSync(
9334
11309
  CONFIG_PATH,
9335
11310
  `${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
9336
11311
  `,
9337
11312
  { mode: 384 }
9338
11313
  );
9339
- fs17.chmodSync(CONFIG_PATH, 384);
11314
+ fs20.chmodSync(CONFIG_PATH, 384);
9340
11315
  };
9341
11316
  var login = async (config, args) => {
9342
11317
  await loginCommand(args, {
@@ -9349,7 +11324,7 @@ var login = async (config, args) => {
9349
11324
  });
9350
11325
  };
9351
11326
  var logout = async (config) => {
9352
- if (!fs17.existsSync(CONFIG_PATH) && config.token === null) {
11327
+ if (!fs20.existsSync(CONFIG_PATH) && config.token === null) {
9353
11328
  say6(dim6("nothing saved \u2014 already signed out"));
9354
11329
  return;
9355
11330
  }
@@ -9402,8 +11377,8 @@ var uninstallCommand = async (config, args) => {
9402
11377
  const runtime = {
9403
11378
  server,
9404
11379
  cliHome: mendCliHome(),
9405
- sshConfigFile: path18.join(os11.homedir(), ".ssh", "config"),
9406
- signedIn: config.token === null || !fs17.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
11380
+ sshConfigFile: path21.join(os13.homedir(), ".ssh", "config"),
11381
+ signedIn: config.token === null || !fs20.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
9407
11382
  revokeDevice: async () => {
9408
11383
  if (config.deviceId === null) return "no device id saved";
9409
11384
  try {
@@ -9467,7 +11442,7 @@ var keysShare = async (config) => {
9467
11442
  return fail6("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
9468
11443
  }
9469
11444
  await shareAgent({
9470
- url: () => socketUrl(config, "keys-bridge", { host: os11.hostname() }),
11445
+ url: () => socketUrl(config, "keys-bridge", { host: os13.hostname() }),
9471
11446
  agentSock,
9472
11447
  signal: new AbortController().signal,
9473
11448
  onEvent: (event) => {
@@ -9597,7 +11572,7 @@ ${usageOf("dotfiles repo")}`);
9597
11572
  var dotfilesSync = async (config, args) => {
9598
11573
  const all = args.includes("--all");
9599
11574
  const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
9600
- const home = os11.homedir();
11575
+ const home = os13.homedir();
9601
11576
  if (!all && requestedPaths.length === 0) {
9602
11577
  const found = scanDotfileCandidates(home);
9603
11578
  if (found.length === 0) {
@@ -9624,13 +11599,13 @@ var dotfilesSync = async (config, args) => {
9624
11599
  if ("error" in read) return fail6(read.error);
9625
11600
  const result = await api(config, "POST", "/dotfiles/snapshot", {
9626
11601
  files: read.files,
9627
- source: os11.hostname(),
11602
+ source: os13.hostname(),
9628
11603
  merge: false
9629
11604
  });
9630
11605
  const snapshot = result.snapshot;
9631
11606
  if (snapshot === null) return fail6("the server accepted the sync but reports no snapshot");
9632
11607
  say6(
9633
- `${green6("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os11.hostname()} \xB7 ${dim6(snapshot.sha.slice(0, 7))} ${dim6("\u2014 applies from the next session launch")}`
11608
+ `${green6("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os13.hostname()} \xB7 ${dim6(snapshot.sha.slice(0, 7))} ${dim6("\u2014 applies from the next session launch")}`
9634
11609
  );
9635
11610
  };
9636
11611
  var envLoad = async (config, args) => {
@@ -9644,10 +11619,10 @@ var envLoad = async (config, args) => {
9644
11619
  const positional = args.filter(
9645
11620
  (arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
9646
11621
  );
9647
- const file = path18.resolve(positional[0] ?? ".env");
11622
+ const file = path21.resolve(positional[0] ?? ".env");
9648
11623
  let contents;
9649
11624
  try {
9650
- contents = fs17.readFileSync(file, "utf8");
11625
+ contents = fs20.readFileSync(file, "utf8");
9651
11626
  } catch {
9652
11627
  return fail6(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
9653
11628
  }
@@ -9663,10 +11638,10 @@ var envLoad = async (config, args) => {
9663
11638
  { contents, allSecret, secretNames }
9664
11639
  );
9665
11640
  if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
9666
- say6(dim6(`${path18.basename(file)} has no variables`));
11641
+ say6(dim6(`${path21.basename(file)} has no variables`));
9667
11642
  return;
9668
11643
  }
9669
- say6(`${green6("\u2713")} loaded ${path18.basename(file)} into ${project2.name}`);
11644
+ say6(`${green6("\u2713")} loaded ${path21.basename(file)} into ${project2.name}`);
9670
11645
  for (const line of formatLoadReport(report, { dim: dim6, warn: amber2 })) say6(line);
9671
11646
  const plaintextUrls = report.loaded.filter(
9672
11647
  (entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
@@ -9857,7 +11832,7 @@ var skillsList = async (config, args) => {
9857
11832
  };
9858
11833
  var skillsPush = async (config, args) => {
9859
11834
  const dirFlag = takeFlagValue4(args, "--dir");
9860
- const root = dirFlag === null ? path18.join(os11.homedir(), DEFAULT_SKILLS_DIR) : path18.resolve(dirFlag);
11835
+ const root = dirFlag === null ? path21.join(os13.homedir(), DEFAULT_SKILLS_DIR) : path21.resolve(dirFlag);
9861
11836
  const scanned = scanSkillLibrary(root);
9862
11837
  if ("error" in scanned) return fail6(scanned.error);
9863
11838
  for (const note of scanned.notes) say6(dim6(` ${note.skill}: ${note.message}`));
@@ -9921,7 +11896,7 @@ _mend() {
9921
11896
  local -a commands
9922
11897
  commands=(
9923
11898
  'adopt:adopt a repository into the store'
9924
- 'codex:new session + codex' 'claude:new session + claude' 'opencode:new session + opencode'
11899
+ 'codex:new session + codex' 'claude:new session + claude' 'opencode:new session + opencode' 'pi:new session + pi'
9925
11900
  'run:new session + arbitrary command'
9926
11901
  'attach:reattach to a running session' 'stop:stop the agent \u2014 record and review remain'
9927
11902
  'shell:open a shell in a live session workspace'
@@ -9931,9 +11906,10 @@ _mend() {
9931
11906
  'keys:the machine Mend deploy key \u2014 init, show, share'
9932
11907
  'git-author:the name and email your workspaces commit as'
9933
11908
  'skills:skill libraries \u2014 list, push'
11909
+ 'memory:what the agents remember about a project \u2014 list, show, rm, import'
9934
11910
  'accounts:your connected accounts on the platform'
9935
11911
  'pair:pair a phone or a second machine' 'doctor:read-only checklist of this setup'
9936
- 'connect:send this machine'"'"'s claude/codex/github credential to the platform'
11912
+ 'connect:send this machine'"'"'s claude/codex/github credential, or your pi setup'
9937
11913
  'continue:resume with the pending follow-up' 'resume:rejoin a settled session'
9938
11914
  'rejoin:attach if live, otherwise resume'
9939
11915
  'land:push a session change to origin and open its pull request'
@@ -9952,7 +11928,7 @@ _mend() {
9952
11928
  else
9953
11929
  case $words[3] in
9954
11930
  setup) compadd -- --version --assets-dir --offline --context --port --ssh-port --bind --url --origin --docker-socket ;;
9955
- upgrade) compadd -- --version --assets-dir --offline ;;
11931
+ upgrade) compadd -- --version --assets-dir --offline --from-preview ;;
9956
11932
  start|restart) compadd -- --offline ;;
9957
11933
  logs) compadd -- --tail ;;
9958
11934
  esac
@@ -9970,7 +11946,7 @@ _mend "$@"
9970
11946
  var BASH_COMPLETIONS = `_mend() {
9971
11947
  local cur=\${COMP_WORDS[COMP_CWORD]}
9972
11948
  if [ "$COMP_CWORD" -eq 1 ]; then
9973
- COMPREPLY=( $(compgen -W "adopt codex claude opencode run attach stop shell service server uninstall keys git-author skills pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
11949
+ COMPREPLY=( $(compgen -W "adopt codex claude opencode pi run attach stop shell service server uninstall keys git-author skills memory connect pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
9974
11950
  return
9975
11951
  fi
9976
11952
  case \${COMP_WORDS[1]} in
@@ -9981,7 +11957,7 @@ var BASH_COMPLETIONS = `_mend() {
9981
11957
  else
9982
11958
  case \${COMP_WORDS[2]} in
9983
11959
  setup) options="--version --assets-dir --offline --context --port --ssh-port --bind --url --origin --docker-socket" ;;
9984
- upgrade) options="--version --assets-dir --offline" ;;
11960
+ upgrade) options="--version --assets-dir --offline --from-preview" ;;
9985
11961
  start|restart) options="--offline" ;;
9986
11962
  logs) options="--tail" ;;
9987
11963
  esac
@@ -10128,7 +12104,7 @@ var agentLiveIn = (detail, session) => agentIsLive(
10128
12104
  var resumeCommand = async (config, args) => {
10129
12105
  const withFlag = args.indexOf("--with");
10130
12106
  const withHarness = withFlag !== -1 && args[withFlag + 1] !== void 0 ? String(args[withFlag + 1]) : null;
10131
- const prefix = args.find((a, i) => !a.startsWith("--") && i !== withFlag + 1);
12107
+ const prefix = firstPositional(args, ["--with"]);
10132
12108
  const project2 = await findProject(config, null);
10133
12109
  const detail = await api(config, "GET", `/projects/${project2.id}`);
10134
12110
  const match = prefix === void 0 ? detail.sessions.find((s) => !agentLiveIn(detail, s)) : detail.sessions.find((s) => s.id.startsWith(prefix));
@@ -10178,7 +12154,7 @@ var resumeForRejoin = async (config, sessionId, label) => {
10178
12154
  var rejoinCommand = async (config, args) => {
10179
12155
  const harnessFlag = args.indexOf("--harness");
10180
12156
  const harness = harnessFlag !== -1 && args[harnessFlag + 1] !== void 0 ? String(args[harnessFlag + 1]) : null;
10181
- const prefix = args.find((arg, index) => !arg.startsWith("--") && index !== harnessFlag + 1);
12157
+ const prefix = firstPositional(args, ["--harness"]);
10182
12158
  const tunnels = attachTunnels(config, args.includes("--no-tunnel"));
10183
12159
  const project2 = await findProject(config, null);
10184
12160
  const detail = await api(config, "GET", `/projects/${project2.id}`);
@@ -10309,10 +12285,20 @@ var printSessionRow = (row) => {
10309
12285
  if (annotation !== void 0 && annotation.pendingFollowUp)
10310
12286
  facts.push(amber2("follow-up pending"));
10311
12287
  const base = session.baseRef === null ? session.baseSha.slice(0, 12) : session.baseRef;
12288
+ const model = session.model === null || session.model === void 0 ? "" : ` \xB7 ${session.model}${session.effort === null || session.effort === void 0 ? "" : ` \xB7 ${session.effort}`}`;
10312
12289
  say6(
10313
- `${session.harness.padEnd(8)} ${dim6(session.id.slice(0, 8))} ${live ? green6(status) : dim6(status)} ${row.projectName} ${dim6(`${session.branch} \xB7 base ${base}`)}${facts.length > 0 ? ` ${facts.join(dim6(" \xB7 "))}` : ""}`
12290
+ `${session.harness.padEnd(8)} ${dim6(session.id.slice(0, 8))} ${live ? green6(status) : dim6(status)} ${row.projectName} ${dim6(`${session.branch} \xB7 base ${base}${model}`)}${facts.length > 0 ? ` ${facts.join(dim6(" \xB7 "))}` : ""}`
10314
12291
  );
10315
12292
  };
12293
+ var modelsCommand = async (config, args) => {
12294
+ const catalogs = await api(
12295
+ config,
12296
+ "GET",
12297
+ "/harnesses/models"
12298
+ );
12299
+ if (args.includes("--json")) return say6(modelCatalogJson(catalogs));
12300
+ for (const line of modelCatalogLines(catalogs, dim6)) say6(line);
12301
+ };
10316
12302
  var sessionsCommand = async (config, args) => {
10317
12303
  const all = args.includes("--all");
10318
12304
  if (args.includes("--json=v2")) {
@@ -10478,6 +12464,18 @@ var buildWorktreesJson = async (config, projectName) => {
10478
12464
  return { version: 2, worktrees };
10479
12465
  };
10480
12466
  var worktreesCommand = async (config, args) => {
12467
+ if (args[0] === "rm") {
12468
+ return worktreesRmCommand(
12469
+ {
12470
+ request: (method, route, body) => request(config, method, route, body),
12471
+ listWorktrees: async (project2) => (await buildWorktreesJson(config, project2)).worktrees,
12472
+ liveStatuses: ACTIVE_STATUSES,
12473
+ say: say6,
12474
+ fail: fail6
12475
+ },
12476
+ args.slice(1)
12477
+ );
12478
+ }
10481
12479
  const json2 = args.includes("--json");
10482
12480
  const projectFlag = args.indexOf("--project");
10483
12481
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
@@ -10539,7 +12537,7 @@ var dashboard = async (config, options = {}) => {
10539
12537
  );
10540
12538
  process.exit(rerun.status ?? 0);
10541
12539
  }
10542
- const { runDashboard } = await import("./dashboard-MGR4YUFD.js");
12540
+ const { runDashboard } = await import("./dashboard-AVABQC2T.js");
10543
12541
  const agentShare = await startShareIfBridge(config);
10544
12542
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
10545
12543
  listServices: () => fetchServices(config),
@@ -10573,7 +12571,7 @@ var startShareIfBridge = async (config) => {
10573
12571
  }
10574
12572
  if (access.mode !== "bridge") return null;
10575
12573
  return startAgentShare({
10576
- url: () => socketUrl(config, "keys-bridge", { host: os11.hostname() }),
12574
+ url: () => socketUrl(config, "keys-bridge", { host: os13.hostname() }),
10577
12575
  agentSock
10578
12576
  });
10579
12577
  };
@@ -10613,10 +12611,10 @@ var manCommand = (words) => {
10613
12611
  }
10614
12612
  const version = cliVersion();
10615
12613
  const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
10616
- const file = path18.join(os11.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
10617
- fs17.writeFileSync(file, page);
12614
+ const file = path21.join(os13.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
12615
+ fs20.writeFileSync(file, page);
10618
12616
  const result = spawnSync6("man", ["-l", file], { stdio: "inherit" });
10619
- fs17.rmSync(file, { force: true });
12617
+ fs20.rmSync(file, { force: true });
10620
12618
  if (result.error !== void 0 || result.status !== 0) {
10621
12619
  say6(doc === null ? renderIndex() : renderCommand(doc));
10622
12620
  }
@@ -10640,6 +12638,7 @@ var main = async () => {
10640
12638
  case "codex":
10641
12639
  case "claude":
10642
12640
  case "opencode":
12641
+ case "pi":
10643
12642
  case "run":
10644
12643
  return withAgentShare(config, () => launch(config, command, rest));
10645
12644
  case "attach":
@@ -10664,6 +12663,10 @@ var main = async () => {
10664
12663
  return dotfilesCommand(config, rest);
10665
12664
  case "skills":
10666
12665
  return skillsCommand(config, rest);
12666
+ case "memory":
12667
+ return memoryCommand(config, rest);
12668
+ case "secrets":
12669
+ return secretsCommand(config, rest);
10667
12670
  case "accounts":
10668
12671
  return accountsCommand(config);
10669
12672
  case "connect":
@@ -10717,6 +12720,8 @@ var main = async () => {
10717
12720
  case "sessions":
10718
12721
  case "status":
10719
12722
  return sessionsCommand(config, rest);
12723
+ case "models":
12724
+ return modelsCommand(config, rest);
10720
12725
  case "worktrees":
10721
12726
  return worktreesCommand(config, rest);
10722
12727
  case void 0: