@sealant/mend 0.35.1 → 0.36.0-next.628
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.
- package/dist/{chunk-SMH65F6Z.js → chunk-OTYH35OK.js} +581 -4
- package/dist/{dashboard-MGR4YUFD.js → dashboard-P73HKQOT.js} +225 -98
- package/dist/main.js +2495 -490
- package/man/mend-accounts.1 +1 -1
- package/man/mend-adopt.1 +1 -1
- package/man/mend-attach.1 +1 -1
- package/man/mend-codex.1 +7 -7
- package/man/mend-completions.1 +1 -1
- package/man/mend-connect-pi.1 +28 -0
- package/man/mend-connect.1 +4 -2
- package/man/mend-continue.1 +1 -1
- package/man/mend-doctor.1 +1 -1
- package/man/mend-dotfiles-repo.1 +1 -1
- package/man/mend-dotfiles-sync.1 +1 -1
- package/man/mend-dotfiles.1 +1 -1
- package/man/mend-env-cluster.1 +1 -1
- package/man/mend-env-load.1 +1 -1
- package/man/mend-env-show.1 +1 -1
- package/man/mend-folder-create.1 +1 -1
- package/man/mend-folder-list.1 +1 -1
- package/man/mend-folder-push.1 +1 -1
- package/man/mend-folder-rm.1 +1 -1
- package/man/mend-git-author.1 +1 -1
- package/man/mend-help.1 +1 -1
- package/man/mend-invite.1 +1 -1
- package/man/mend-keys-init.1 +1 -1
- package/man/mend-keys-mode.1 +1 -1
- package/man/mend-keys-share.1 +1 -1
- package/man/mend-keys-show.1 +1 -1
- package/man/mend-land.1 +1 -1
- package/man/mend-login.1 +1 -1
- package/man/mend-logout.1 +1 -1
- package/man/mend-man.1 +1 -1
- package/man/mend-members.1 +1 -1
- package/man/mend-memory-import.1 +23 -0
- package/man/mend-memory-rm.1 +15 -0
- package/man/mend-memory-show.1 +15 -0
- package/man/mend-memory.1 +32 -0
- package/man/mend-models.1 +22 -0
- package/man/mend-operator-exposure.1 +1 -1
- package/man/mend-operator-gate.1 +1 -1
- package/man/mend-operator-grant-owner.1 +1 -1
- package/man/mend-operator-org-create.1 +1 -1
- package/man/mend-operator-org-invite-owner.1 +1 -1
- package/man/mend-operator-org-list.1 +1 -1
- package/man/mend-operator-org-rename.1 +1 -1
- package/man/mend-operator-reset-link.1 +1 -1
- package/man/mend-pair.1 +1 -1
- package/man/mend-projects.1 +1 -1
- package/man/mend-pull.1 +1 -1
- package/man/mend-refresh.1 +1 -1
- package/man/mend-rejoin.1 +1 -1
- package/man/mend-resume.1 +1 -1
- package/man/mend-run.1 +1 -1
- package/man/mend-secrets-add.1 +24 -0
- package/man/mend-secrets-rm.1 +14 -0
- package/man/mend-secrets.1 +18 -0
- package/man/mend-server-logs.1 +1 -1
- package/man/mend-server-restart.1 +1 -1
- package/man/mend-server-setup.1 +27 -3
- package/man/mend-server-start.1 +1 -1
- package/man/mend-server-status.1 +5 -3
- package/man/mend-server-stop.1 +3 -3
- package/man/mend-server-upgrade.1 +7 -2
- package/man/mend-server.1 +1 -1
- package/man/mend-service-add.1 +1 -1
- package/man/mend-service-connect.1 +1 -1
- package/man/mend-service-init.1 +1 -1
- package/man/mend-service-list.1 +1 -1
- package/man/mend-service-logs.1 +1 -1
- package/man/mend-service-restart.1 +1 -1
- package/man/mend-service-run.1 +1 -1
- package/man/mend-service-stop.1 +1 -1
- package/man/mend-session-share.1 +2 -2
- package/man/mend-sessions.1 +1 -1
- package/man/mend-shell.1 +1 -1
- package/man/mend-skills-push.1 +1 -1
- package/man/mend-skills.1 +1 -1
- package/man/mend-snake.1 +1 -1
- package/man/mend-ssh-setup.1 +1 -1
- package/man/mend-ssh.1 +1 -1
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +1 -1
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-worktrees-rm.1 +29 -0
- package/man/mend-worktrees.1 +2 -2
- package/man/mend.1 +35 -5
- 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
|
-
|
|
37
|
-
|
|
51
|
+
terminalReadOnlyLine,
|
|
52
|
+
trackBracketedPaste,
|
|
53
|
+
validatePiProfile,
|
|
54
|
+
validateSecretFilePath
|
|
55
|
+
} from "./chunk-OTYH35OK.js";
|
|
38
56
|
|
|
39
57
|
// src/main.ts
|
|
40
58
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
41
|
-
import * as
|
|
59
|
+
import * as fs20 from "node:fs";
|
|
42
60
|
import { createRequire } from "node:module";
|
|
43
|
-
import * as
|
|
44
|
-
import * as
|
|
45
|
-
import * as
|
|
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((
|
|
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
|
-
|
|
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((
|
|
98
|
-
ws.addEventListener("close", () =>
|
|
371
|
+
const closed = new Promise((resolve8) => {
|
|
372
|
+
ws.addEventListener("close", () => resolve8(), { once: true });
|
|
99
373
|
});
|
|
100
|
-
const opened = await new Promise((
|
|
101
|
-
ws.addEventListener("open", () =>
|
|
102
|
-
ws.addEventListener("error", () =>
|
|
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
|
|
223
|
-
import * as
|
|
224
|
-
import * as
|
|
225
|
-
var claudeGrantDir = (home) =>
|
|
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
|
-
|
|
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",
|
|
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 === "" ?
|
|
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
|
|
260
|
-
const text = (key) => typeof
|
|
542
|
+
const record2 = { ...parsed };
|
|
543
|
+
const text = (key) => typeof record2[key] === "string" ? record2[key] : null;
|
|
261
544
|
return {
|
|
262
|
-
loggedIn:
|
|
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
|
-
|
|
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 =
|
|
282
|
-
if (
|
|
283
|
-
const secret2 =
|
|
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",
|
|
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((
|
|
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
|
-
|
|
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
|
|
357
|
-
import * as
|
|
358
|
-
import * as
|
|
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
|
|
362
|
-
import * as
|
|
644
|
+
import * as fs3 from "node:fs";
|
|
645
|
+
import * as path3 from "node:path";
|
|
363
646
|
var throwawayLoginDir = (parent, prefix) => {
|
|
364
|
-
|
|
365
|
-
const dir =
|
|
366
|
-
|
|
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 === "" ?
|
|
660
|
+
return configured === void 0 || configured === "" ? path4.join(os3.homedir(), ".codex") : configured;
|
|
378
661
|
};
|
|
379
|
-
var runCodexDeviceLogin = (cli,
|
|
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:
|
|
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 =
|
|
411
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
712
|
+
fs4.rmSync(home, { recursive: true, force: true });
|
|
430
713
|
}
|
|
431
714
|
};
|
|
432
715
|
|
|
433
716
|
// src/doctor-bundle-collectors.ts
|
|
434
|
-
import * as
|
|
435
|
-
import * as
|
|
436
|
-
import * as
|
|
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
|
|
441
|
-
import * as
|
|
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((
|
|
1007
|
+
return new Promise((resolve8) => {
|
|
485
1008
|
let fd;
|
|
486
1009
|
try {
|
|
487
|
-
if (options.stdoutFile !== void 0) fd =
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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)
|
|
1080
|
+
if (fd !== void 0) fs5.closeSync(fd);
|
|
558
1081
|
} catch {
|
|
559
1082
|
}
|
|
560
|
-
|
|
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
|
-
|
|
1101
|
+
path5.join(installation.directory, "server.env"),
|
|
579
1102
|
"-f",
|
|
580
|
-
|
|
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(
|
|
1153
|
+
for (const directory of (env["PATH"] ?? "").split(path6.delimiter)) {
|
|
627
1154
|
if (directory === "") continue;
|
|
628
|
-
const candidate =
|
|
1155
|
+
const candidate = path6.join(directory, command);
|
|
629
1156
|
try {
|
|
630
|
-
|
|
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:
|
|
649
|
-
osRelease:
|
|
650
|
-
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
|
|
1027
|
-
if (
|
|
1028
|
-
files2.push({ path: `sessions/${session.id}-record.txt`, content:
|
|
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:
|
|
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
|
|
1121
|
-
import * as
|
|
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: [
|
|
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
|
|
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
|
|
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
|
-
{
|
|
1232
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2563
|
-
|
|
2564
|
-
const root =
|
|
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
|
-
|
|
2574
|
-
|
|
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 =
|
|
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
|
|
2644
|
-
import * as
|
|
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
|
|
2649
|
-
import * as
|
|
2650
|
-
import * as
|
|
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
|
|
3399
|
+
return path8.join(facts.home, ".docker", "daemon.json");
|
|
2676
3400
|
case "orbstack":
|
|
2677
|
-
return
|
|
3401
|
+
return path8.join(facts.home, ".orbstack", "config", "docker.json");
|
|
2678
3402
|
case "rootless":
|
|
2679
|
-
return
|
|
2680
|
-
facts.xdgConfigHome ??
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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:
|
|
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(
|
|
3928
|
+
for (const directory of (process.env["PATH"] ?? "").split(path9.delimiter)) {
|
|
3192
3929
|
if (directory === "") continue;
|
|
3193
3930
|
try {
|
|
3194
|
-
|
|
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
|
|
3220
|
-
import * as
|
|
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,
|
|
3998
|
+
var statFile = (home, relative5) => {
|
|
3262
3999
|
try {
|
|
3263
|
-
const stat =
|
|
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((
|
|
3271
|
-
const stat = statFile(home,
|
|
3272
|
-
return stat === null ? [] : [{ path:
|
|
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
|
|
3277
|
-
const outside =
|
|
3278
|
-
return outside ? null :
|
|
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
|
|
3284
|
-
if (
|
|
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,
|
|
4026
|
+
const stat = statFile(home, relative5);
|
|
3290
4027
|
if (stat === null) {
|
|
3291
|
-
return { error: `${
|
|
4028
|
+
return { error: `${relative5} is not a file under ${home}` };
|
|
3292
4029
|
}
|
|
3293
4030
|
if (stat.size > MAX_FILE_BYTES) {
|
|
3294
|
-
return { error: `${
|
|
4031
|
+
return { error: `${relative5} is over 1MB \u2014 dotfiles are text; trim the selection` };
|
|
3295
4032
|
}
|
|
3296
4033
|
files.push({
|
|
3297
|
-
path:
|
|
3298
|
-
contentsBase64:
|
|
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
|
|
3406
|
-
import * as
|
|
3407
|
-
import * as
|
|
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 ?
|
|
4382
|
+
const originPath = origin === null ? path11.resolve(originUrl.replace(/^file:\/\//, "")) : null;
|
|
3646
4383
|
return remotes.find(
|
|
3647
|
-
(remote) => origin === null ?
|
|
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 =
|
|
4407
|
+
const dir = fs11.mkdtempSync(path11.join(os6.tmpdir(), "mend-pull-"));
|
|
3671
4408
|
try {
|
|
3672
|
-
const file =
|
|
3673
|
-
|
|
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
|
-
|
|
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 (/^
|
|
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
|
|
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((
|
|
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:
|
|
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
|
|
4118
|
-
import * as
|
|
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
|
|
4182
|
-
const entries =
|
|
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 =
|
|
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
|
|
4961
|
+
else walk2(child);
|
|
4190
4962
|
} else if (entry.isFile()) {
|
|
4191
|
-
const 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
|
-
|
|
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 (!
|
|
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:
|
|
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
|
|
4443
|
-
import * as
|
|
4444
|
-
import * as
|
|
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
|
|
4663
|
-
import * as
|
|
4664
|
-
import * as
|
|
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
|
|
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 =
|
|
5786
|
+
const fd = fs14.openSync(directory, "r");
|
|
4693
5787
|
try {
|
|
4694
|
-
|
|
5788
|
+
fs14.fsyncSync(fd);
|
|
4695
5789
|
} finally {
|
|
4696
|
-
|
|
5790
|
+
fs14.closeSync(fd);
|
|
4697
5791
|
}
|
|
4698
5792
|
};
|
|
4699
5793
|
var writeDurable = (file, content, mode = 384) => {
|
|
4700
|
-
const fd =
|
|
5794
|
+
const fd = fs14.openSync(file, "wx", mode);
|
|
4701
5795
|
try {
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
5796
|
+
fs14.writeFileSync(fd, content, "utf8");
|
|
5797
|
+
fs14.fchmodSync(fd, mode);
|
|
5798
|
+
fs14.fsyncSync(fd);
|
|
4705
5799
|
} finally {
|
|
4706
|
-
|
|
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(
|
|
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 !==
|
|
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
|
-
|
|
4747
|
-
|
|
4748
|
-
} else if (!
|
|
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 =
|
|
5862
|
+
const lockDir = path15.join(configDir, "server.lock");
|
|
4754
5863
|
try {
|
|
4755
|
-
|
|
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 =
|
|
4761
|
-
const ownerFile =
|
|
4762
|
-
const owner = `${JSON.stringify({ pid: process.pid, hostname:
|
|
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 =
|
|
4767
|
-
if (!open || current.dev !== owned.dev || current.ino !== owned.ino ||
|
|
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
|
-
|
|
4784
|
-
|
|
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 && (
|
|
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 =
|
|
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 =
|
|
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:
|
|
4816
|
-
config:
|
|
4817
|
-
env:
|
|
4818
|
-
compose:
|
|
4819
|
-
postgresInit:
|
|
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 =
|
|
5945
|
+
const temporary = path15.join(paths.configDir, `.identity-${randomUUID2()}`);
|
|
4830
5946
|
writeDurable(temporary, identity);
|
|
4831
|
-
|
|
5947
|
+
fs14.linkSync(temporary, paths.identity);
|
|
4832
5948
|
syncDirectory(paths.configDir);
|
|
4833
|
-
|
|
5949
|
+
fs14.unlinkSync(temporary);
|
|
4834
5950
|
};
|
|
4835
5951
|
var prepareGeneration = (paths, files) => {
|
|
4836
|
-
|
|
4837
|
-
const generationName = `gen-${
|
|
4838
|
-
const directory =
|
|
4839
|
-
|
|
4840
|
-
for (const key of
|
|
4841
|
-
|
|
4842
|
-
|
|
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
|
|
4854
|
-
if (!/^generations\/gen-[0-9a-f-]{36}$/.test(
|
|
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
|
|
4858
|
-
if (
|
|
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 =
|
|
4863
|
-
|
|
4864
|
-
|
|
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 &&
|
|
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:
|
|
4887
|
-
generations:
|
|
4888
|
-
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 =
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
const directory =
|
|
4905
|
-
|
|
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
|
-
|
|
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 =
|
|
6036
|
+
const partialFile = path15.join(directory, "database.sql.partial");
|
|
4924
6037
|
return {
|
|
4925
6038
|
directory,
|
|
4926
6039
|
partialFile,
|
|
4927
6040
|
complete: () => whileOwned(() => {
|
|
4928
|
-
if (
|
|
6041
|
+
if (fs14.statSync(partialFile).size === 0)
|
|
4929
6042
|
throw new ServerStoreError("Database backup is empty; refusing target startup.");
|
|
4930
|
-
const fd =
|
|
6043
|
+
const fd = fs14.openSync(partialFile, "r");
|
|
4931
6044
|
try {
|
|
4932
|
-
|
|
4933
|
-
|
|
6045
|
+
fs14.fchmodSync(fd, 384);
|
|
6046
|
+
fs14.fsyncSync(fd);
|
|
4934
6047
|
} finally {
|
|
4935
|
-
|
|
6048
|
+
fs14.closeSync(fd);
|
|
4936
6049
|
}
|
|
4937
|
-
|
|
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) =>
|
|
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 =
|
|
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
|
|
6084
|
+
import * as fs15 from "node:fs";
|
|
4972
6085
|
var cliVersion = () => {
|
|
4973
|
-
const raw =
|
|
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(
|
|
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 = (
|
|
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 (
|
|
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
|
|
5175
|
-
|
|
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 {
|
|
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
|
|
5338
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
5630
|
-
|
|
5631
|
-
|
|
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 (!
|
|
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(
|
|
5703
|
-
readFile(
|
|
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,
|
|
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
|
-
|
|
5807
|
-
|
|
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
|
|
5836
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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,
|
|
5924
|
-
await initGarage(runtime,
|
|
5925
|
-
await probeHealth(runtime, config
|
|
5926
|
-
runtime.writeLine(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6007
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
6141
|
-
runtime.
|
|
6142
|
-
|
|
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
|
-
|
|
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(`${
|
|
7801
|
+
const response = await runtime.fetchText(`${healthOrigin(config)}/api/health`, 2e3);
|
|
6158
7802
|
let fields = null;
|
|
7803
|
+
let body = null;
|
|
6159
7804
|
try {
|
|
6160
|
-
|
|
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") !==
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
6230
|
-
const configHome = environment["XDG_CONFIG_HOME"] ??
|
|
7881
|
+
const home = os10.homedir();
|
|
7882
|
+
const configHome = environment["XDG_CONFIG_HOME"] ?? path16.join(home, ".config");
|
|
6231
7883
|
return {
|
|
6232
|
-
configDir:
|
|
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, {
|
|
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((
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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 =
|
|
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
|
|
6686
|
-
import * as
|
|
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
|
|
8378
|
+
for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
6710
8379
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
6711
|
-
const absolute =
|
|
8380
|
+
const absolute = path17.join(dir, entry.name);
|
|
6712
8381
|
let stat;
|
|
6713
8382
|
try {
|
|
6714
|
-
stat =
|
|
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 =
|
|
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 =
|
|
8405
|
+
const skillDir = path17.join(root, entry.name);
|
|
6737
8406
|
let stat;
|
|
6738
8407
|
try {
|
|
6739
|
-
stat =
|
|
8408
|
+
stat = fs17.statSync(skillDir);
|
|
6740
8409
|
} catch {
|
|
6741
8410
|
continue;
|
|
6742
8411
|
}
|
|
6743
8412
|
if (!stat.isDirectory()) continue;
|
|
6744
|
-
if (!
|
|
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
|
|
6752
|
-
const buffer =
|
|
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: `${
|
|
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: `${
|
|
8427
|
+
notes.push({ skill: entry.name, message: `${relative5} is binary \u2014 skipped` });
|
|
6759
8428
|
continue;
|
|
6760
8429
|
}
|
|
6761
|
-
files.push({ path:
|
|
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
|
|
6788
|
-
import * as
|
|
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
|
|
6794
|
-
import { isIP as
|
|
6795
|
-
import * as
|
|
6796
|
-
import * as
|
|
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 = (
|
|
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 = (
|
|
6816
|
-
const label =
|
|
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
|
|
6821
|
-
if (
|
|
6822
|
-
if (
|
|
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(
|
|
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 ((
|
|
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(
|
|
6842
|
-
if ((
|
|
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 =
|
|
6884
|
-
value.startsWith("~/") ?
|
|
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(
|
|
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 (!
|
|
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 =
|
|
8772
|
+
const identityFile = path18.resolve(
|
|
7104
8773
|
configHome,
|
|
7105
8774
|
"ssh",
|
|
7106
8775
|
`agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
|
|
7107
8776
|
);
|
|
7108
8777
|
try {
|
|
7109
|
-
|
|
7110
|
-
|
|
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
|
-
|
|
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 ${
|
|
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-${
|
|
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 =
|
|
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
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
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 = () =>
|
|
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:
|
|
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
|
|
7330
|
-
import * as
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
9061
|
+
server = exists(path20.join(configDir, "identity.env")) ? {
|
|
7393
9062
|
version: "(no active generation)",
|
|
7394
9063
|
appUrl: "",
|
|
7395
9064
|
dockerContext: "",
|
|
7396
|
-
|
|
7397
|
-
|
|
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
|
-
|
|
7405
|
-
|
|
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 =
|
|
7412
|
-
const sshDirectory =
|
|
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(
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9229
|
+
fs19.rmSync(plan.cliConfig, { force: true });
|
|
7556
9230
|
server.writeLine(`removed ${plan.cliConfig}`);
|
|
7557
9231
|
}
|
|
7558
9232
|
if (plan.sshDirectory !== null) {
|
|
7559
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
9255
|
+
remaining = fs19.readdirSync(runtime.cliHome);
|
|
7582
9256
|
} catch {
|
|
7583
9257
|
return;
|
|
7584
9258
|
}
|
|
7585
9259
|
if (remaining.length === 0) {
|
|
7586
|
-
|
|
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 =
|
|
7651
|
-
xdg === void 0 || 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 =
|
|
7655
|
-
return !
|
|
9409
|
+
const legacy = path21.join(os13.homedir(), ".mend");
|
|
9410
|
+
return !fs20.existsSync(preferred) && fs20.existsSync(legacy) ? legacy : preferred;
|
|
7656
9411
|
};
|
|
7657
|
-
var CONFIG_PATH =
|
|
9412
|
+
var CONFIG_PATH = path21.join(mendCliHome(), "cli.json");
|
|
7658
9413
|
var loadConfig = () => {
|
|
7659
9414
|
let fileConfig = {};
|
|
7660
|
-
if (
|
|
9415
|
+
if (fs20.existsSync(CONFIG_PATH)) {
|
|
7661
9416
|
try {
|
|
7662
|
-
fileConfig = JSON.parse(
|
|
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((
|
|
9607
|
+
const sleep3 = (ms) => new Promise((resolve8) => {
|
|
7853
9608
|
const timer = setTimeout(() => {
|
|
7854
9609
|
timers.delete(timer);
|
|
7855
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
}
|
|
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((
|
|
8169
|
-
finishAttachment =
|
|
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((
|
|
8262
|
-
const onAbort = () =>
|
|
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
|
-
|
|
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
|
|
8309
|
-
|
|
8310
|
-
|
|
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
|
|
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
|
|
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((
|
|
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 =
|
|
8925
|
-
if (
|
|
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 =
|
|
10719
|
+
const rootFiles = fs20.readdirSync(root);
|
|
8929
10720
|
const proposals = [];
|
|
8930
|
-
const readRoot = (name) =>
|
|
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("*") ?
|
|
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 =
|
|
8943
|
-
if (!
|
|
10733
|
+
const manifest = path21.join(root, dir, "package.json");
|
|
10734
|
+
if (!fs20.existsSync(manifest)) continue;
|
|
8944
10735
|
for (const proposal of proposeFromWorkspacePackage(
|
|
8945
|
-
|
|
8946
|
-
|
|
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
|
-
|
|
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 =
|
|
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((
|
|
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) =>
|
|
11039
|
+
var readIfExists = (file) => fs20.existsSync(file) ? fs20.readFileSync(file, "utf8") : null;
|
|
9095
11040
|
var localCredential = (provider) => {
|
|
9096
|
-
const home =
|
|
11041
|
+
const home = os13.homedir();
|
|
9097
11042
|
switch (provider) {
|
|
9098
11043
|
case "codex":
|
|
9099
11044
|
return readIfExists(
|
|
9100
|
-
|
|
11045
|
+
path21.join(process.env["CODEX_HOME"] ?? path21.join(home, ".codex"), "auth.json")
|
|
9101
11046
|
);
|
|
9102
11047
|
case "claude":
|
|
9103
11048
|
return readIfExists(
|
|
9104
|
-
|
|
9105
|
-
process.env["CLAUDE_CONFIG_DIR"] ??
|
|
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:
|
|
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 &&
|
|
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 (
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
9333
|
-
|
|
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
|
-
|
|
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 (!
|
|
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:
|
|
9406
|
-
signedIn: config.token === null || !
|
|
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:
|
|
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 =
|
|
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:
|
|
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 ${
|
|
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 =
|
|
11622
|
+
const file = path21.resolve(positional[0] ?? ".env");
|
|
9648
11623
|
let contents;
|
|
9649
11624
|
try {
|
|
9650
|
-
contents =
|
|
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(`${
|
|
11641
|
+
say6(dim6(`${path21.basename(file)} has no variables`));
|
|
9667
11642
|
return;
|
|
9668
11643
|
}
|
|
9669
|
-
say6(`${green6("\u2713")} loaded ${
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
12540
|
+
const { runDashboard } = await import("./dashboard-P73HKQOT.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:
|
|
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 =
|
|
10617
|
-
|
|
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
|
-
|
|
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:
|