@sealant/mend 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-BMFB2UN7.js → chunk-J3TR4UAG.js} +81 -7
- package/dist/{dashboard-OF5UXII2.js → dashboard-3HNFSQZD.js} +60 -7
- package/dist/main.js +1821 -871
- 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 +1 -1
- package/man/mend-completions.1 +1 -1
- package/man/mend-connect.1 +1 -1
- package/man/mend-continue.1 +1 -1
- package/man/mend-doctor.1 +28 -3
- 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 +24 -0
- 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 +9 -2
- 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-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-server-logs.1 +1 -1
- package/man/mend-server-restart.1 +1 -1
- package/man/mend-server-setup.1 +1 -1
- package/man/mend-server-start.1 +1 -1
- package/man/mend-server-status.1 +1 -1
- package/man/mend-server-stop.1 +1 -1
- package/man/mend-server-upgrade.1 +1 -1
- 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 +1 -1
- 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 +8 -1
- package/man/mend-ui.1 +2 -2
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +6 -3
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
claudeGrantFacts,
|
|
8
8
|
createSseParser,
|
|
9
9
|
cwdFacts,
|
|
10
|
+
gitAuthorIssue,
|
|
10
11
|
gitCurrentBranch,
|
|
11
12
|
gitOriginUrl,
|
|
12
13
|
gitTopLevel,
|
|
@@ -16,23 +17,25 @@ import {
|
|
|
16
17
|
landingFactsFromWire,
|
|
17
18
|
matchProjectByCwd,
|
|
18
19
|
narrowCredential,
|
|
20
|
+
normalizeGitAuthor,
|
|
19
21
|
normalizeProjectName,
|
|
20
22
|
normalizeRemoteUrl,
|
|
21
23
|
parseLaunchArgs,
|
|
22
24
|
pasteBytes,
|
|
23
25
|
repositoryCloneUrlIssue,
|
|
24
26
|
sameGrant,
|
|
27
|
+
servicesHoldOf,
|
|
25
28
|
sessionDisplayName,
|
|
26
29
|
trackBracketedPaste
|
|
27
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-J3TR4UAG.js";
|
|
28
31
|
|
|
29
32
|
// src/main.ts
|
|
30
33
|
import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
|
|
31
|
-
import * as
|
|
34
|
+
import * as fs14 from "node:fs";
|
|
32
35
|
import { createRequire } from "node:module";
|
|
33
36
|
import * as net4 from "node:net";
|
|
34
|
-
import * as
|
|
35
|
-
import * as
|
|
37
|
+
import * as os9 from "node:os";
|
|
38
|
+
import * as path15 from "node:path";
|
|
36
39
|
|
|
37
40
|
// src/agent-share.ts
|
|
38
41
|
import * as net from "node:net";
|
|
@@ -40,7 +43,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
40
43
|
var retryDelayMs = (attempt2) => Math.min(3e4, 1e3 * 2 ** Math.min(Math.max(attempt2 - 1, 0), 5));
|
|
41
44
|
var SSH_AGENTC_REQUEST_IDENTITIES = 11;
|
|
42
45
|
var SSH_AGENTC_SIGN_REQUEST = 13;
|
|
43
|
-
var askLocalAgent = (agentSock, payload) => new Promise((
|
|
46
|
+
var askLocalAgent = (agentSock, payload) => new Promise((resolve7, reject) => {
|
|
44
47
|
const connection = net.connect(agentSock, () => {
|
|
45
48
|
connection.write(payload);
|
|
46
49
|
});
|
|
@@ -54,7 +57,7 @@ var askLocalAgent = (agentSock, payload) => new Promise((resolve6, reject) => {
|
|
|
54
57
|
if (pending.length >= 4 && pending.length >= 4 + pending.readUInt32BE(0)) {
|
|
55
58
|
clearTimeout(timer);
|
|
56
59
|
connection.end();
|
|
57
|
-
|
|
60
|
+
resolve7(pending.subarray(0, 4 + pending.readUInt32BE(0)));
|
|
58
61
|
}
|
|
59
62
|
});
|
|
60
63
|
connection.on("error", (error) => {
|
|
@@ -84,12 +87,12 @@ var shareAgent = async (options) => {
|
|
|
84
87
|
const ws = new WebSocket(target);
|
|
85
88
|
const onAbort = () => ws.close();
|
|
86
89
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
87
|
-
const closed = new Promise((
|
|
88
|
-
ws.addEventListener("close", () =>
|
|
90
|
+
const closed = new Promise((resolve7) => {
|
|
91
|
+
ws.addEventListener("close", () => resolve7(), { once: true });
|
|
89
92
|
});
|
|
90
|
-
const opened = await new Promise((
|
|
91
|
-
ws.addEventListener("open", () =>
|
|
92
|
-
ws.addEventListener("error", () =>
|
|
93
|
+
const opened = await new Promise((resolve7) => {
|
|
94
|
+
ws.addEventListener("open", () => resolve7(true), { once: true });
|
|
95
|
+
ws.addEventListener("error", () => resolve7(false), { once: true });
|
|
93
96
|
});
|
|
94
97
|
if (opened) {
|
|
95
98
|
attempt2 = 0;
|
|
@@ -107,19 +110,19 @@ var shareAgent = async (options) => {
|
|
|
107
110
|
}
|
|
108
111
|
const id = frame.id;
|
|
109
112
|
const payload = Buffer.from(frame.payload, "base64");
|
|
110
|
-
const
|
|
113
|
+
const type2 = payload[4];
|
|
111
114
|
const context = typeof frame.context === "string" ? frame.context : "mend";
|
|
112
115
|
chain = chain.then(async () => {
|
|
113
116
|
const started = Date.now();
|
|
114
|
-
if (
|
|
115
|
-
else if (
|
|
117
|
+
if (type2 === SSH_AGENTC_SIGN_REQUEST) onEvent({ kind: "sign-requested", context });
|
|
118
|
+
else if (type2 === SSH_AGENTC_REQUEST_IDENTITIES) {
|
|
116
119
|
onEvent({ kind: "identities-requested", context });
|
|
117
120
|
}
|
|
118
121
|
try {
|
|
119
122
|
const response = await askLocalAgent(agentSock, payload);
|
|
120
123
|
if (ws.readyState !== WebSocket.OPEN) return null;
|
|
121
124
|
ws.send(JSON.stringify({ t: "res", id, payload: response.toString("base64") }));
|
|
122
|
-
if (
|
|
125
|
+
if (type2 === SSH_AGENTC_SIGN_REQUEST) {
|
|
123
126
|
onEvent({ kind: "signed", context, seconds: (Date.now() - started) / 1e3 });
|
|
124
127
|
}
|
|
125
128
|
} catch (error) {
|
|
@@ -127,7 +130,7 @@ var shareAgent = async (options) => {
|
|
|
127
130
|
if (ws.readyState === WebSocket.OPEN) {
|
|
128
131
|
ws.send(JSON.stringify({ t: "err", id, message }));
|
|
129
132
|
}
|
|
130
|
-
if (
|
|
133
|
+
if (type2 === SSH_AGENTC_SIGN_REQUEST) onEvent({ kind: "not-signed", context, message });
|
|
131
134
|
}
|
|
132
135
|
return null;
|
|
133
136
|
});
|
|
@@ -240,8 +243,8 @@ var readGrant = (configDir, platform = process.platform) => {
|
|
|
240
243
|
import { execFile } from "node:child_process";
|
|
241
244
|
var IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
242
245
|
var pickImageType = (offered) => {
|
|
243
|
-
const types = offered.split(/\s+/).map((
|
|
244
|
-
for (const
|
|
246
|
+
const types = offered.split(/\s+/).map((type2) => type2.trim().toLowerCase());
|
|
247
|
+
for (const type2 of IMAGE_TYPES) if (types.includes(type2)) return type2;
|
|
245
248
|
return null;
|
|
246
249
|
};
|
|
247
250
|
var parsePngfHex = (output) => {
|
|
@@ -250,13 +253,13 @@ var parsePngfHex = (output) => {
|
|
|
250
253
|
return Buffer.from(match[1], "hex");
|
|
251
254
|
};
|
|
252
255
|
var MAX_BYTES = 32 * 1024 * 1024;
|
|
253
|
-
var run = (command, args) => new Promise((
|
|
256
|
+
var run = (command, args) => new Promise((resolve7) => {
|
|
254
257
|
execFile(
|
|
255
258
|
command,
|
|
256
259
|
[...args],
|
|
257
260
|
{ encoding: "buffer", maxBuffer: MAX_BYTES, timeout: 3e3 },
|
|
258
261
|
(error, stdout) => {
|
|
259
|
-
|
|
262
|
+
resolve7(error === null ? stdout : null);
|
|
260
263
|
}
|
|
261
264
|
);
|
|
262
265
|
});
|
|
@@ -293,450 +296,736 @@ var readClipboardImage = async (platform = process.platform, env = process.env)
|
|
|
293
296
|
return null;
|
|
294
297
|
};
|
|
295
298
|
|
|
296
|
-
// src/doctor.ts
|
|
299
|
+
// src/doctor-bundle-collectors.ts
|
|
300
|
+
import * as fs3 from "node:fs";
|
|
301
|
+
import * as os2 from "node:os";
|
|
302
|
+
import * as path3 from "node:path";
|
|
303
|
+
|
|
304
|
+
// src/server-runtime.ts
|
|
305
|
+
import { spawn } from "node:child_process";
|
|
297
306
|
import * as fs2 from "node:fs";
|
|
298
307
|
import * as path2 from "node:path";
|
|
299
|
-
var
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
308
|
+
var serverProcessDeadlines = {
|
|
309
|
+
ordinary: 3e4,
|
|
310
|
+
pull: 10 * 6e4,
|
|
311
|
+
startup: 3 * 6e4,
|
|
312
|
+
stop: 9e4,
|
|
313
|
+
dump: 15 * 6e4,
|
|
314
|
+
composeWaitSeconds: 120
|
|
315
|
+
};
|
|
316
|
+
var runServerProcess = (command, args, parentEnvironment, options = {}) => {
|
|
317
|
+
const timeoutMs = options?.timeoutMs ?? serverProcessDeadlines.ordinary;
|
|
318
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
|
|
319
|
+
return Promise.resolve({
|
|
320
|
+
status: null,
|
|
321
|
+
stdout: "",
|
|
322
|
+
stderr: "",
|
|
323
|
+
error: "Invalid process timeout"
|
|
309
324
|
});
|
|
310
|
-
if (!response.ok) return { value: null, status: response.status };
|
|
311
|
-
return { value: await response.json(), status: response.status };
|
|
312
|
-
} catch {
|
|
313
|
-
return { value: null, status: null };
|
|
314
325
|
}
|
|
326
|
+
if (options.stdoutFile !== void 0 && options.stdout === "inherit") {
|
|
327
|
+
return Promise.resolve({
|
|
328
|
+
status: null,
|
|
329
|
+
stdout: "",
|
|
330
|
+
stderr: "",
|
|
331
|
+
error: "Invalid process output options"
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const env = {};
|
|
335
|
+
for (const key of [
|
|
336
|
+
"PATH",
|
|
337
|
+
"HOME",
|
|
338
|
+
"USER",
|
|
339
|
+
"LOGNAME",
|
|
340
|
+
"XDG_CONFIG_HOME",
|
|
341
|
+
"XDG_RUNTIME_DIR",
|
|
342
|
+
"DOCKER_CONFIG",
|
|
343
|
+
"SSH_AUTH_SOCK",
|
|
344
|
+
"TMPDIR",
|
|
345
|
+
"TMP",
|
|
346
|
+
"TEMP"
|
|
347
|
+
]) {
|
|
348
|
+
if (parentEnvironment[key] !== void 0) env[key] = parentEnvironment[key];
|
|
349
|
+
}
|
|
350
|
+
return new Promise((resolve7) => {
|
|
351
|
+
let fd;
|
|
352
|
+
try {
|
|
353
|
+
if (options.stdoutFile !== void 0) fd = fs2.openSync(options.stdoutFile, "wx", 384);
|
|
354
|
+
const child = spawn(command, [...args], {
|
|
355
|
+
env,
|
|
356
|
+
detached: process.platform !== "win32",
|
|
357
|
+
stdio: ["ignore", fd ?? (options.stdout === "inherit" ? "inherit" : "pipe"), "pipe"]
|
|
358
|
+
});
|
|
359
|
+
let stdout = "";
|
|
360
|
+
let stderr = "";
|
|
361
|
+
let stdoutBytes = 0;
|
|
362
|
+
let stderrBytes = 0;
|
|
363
|
+
let failure2;
|
|
364
|
+
const terminate = () => {
|
|
365
|
+
if (process.platform === "win32" || child.pid === void 0) {
|
|
366
|
+
child.kill("SIGKILL");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
process.kill(-child.pid, "SIGKILL");
|
|
371
|
+
} catch (cause) {
|
|
372
|
+
if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) {
|
|
373
|
+
failure2 = "Could not terminate the process group; retain the installation lock until its processes stop.";
|
|
374
|
+
child.kill("SIGKILL");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
child.stdout?.on("data", (chunk) => {
|
|
379
|
+
stdoutBytes += chunk.length;
|
|
380
|
+
if (stdoutBytes <= 4 * 1024 * 1024) stdout += chunk.toString();
|
|
381
|
+
else {
|
|
382
|
+
failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
|
|
383
|
+
terminate();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
child.stderr?.on("data", (chunk) => {
|
|
387
|
+
stderrBytes += chunk.length;
|
|
388
|
+
if (stderrBytes <= 64 * 1024) stderr += chunk.toString();
|
|
389
|
+
else {
|
|
390
|
+
failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
|
|
391
|
+
terminate();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
const timer = setTimeout(() => {
|
|
395
|
+
failure2 ??= `Process timed out after ${timeoutMs}ms`;
|
|
396
|
+
terminate();
|
|
397
|
+
}, timeoutMs);
|
|
398
|
+
child.once("error", (error) => {
|
|
399
|
+
failure2 ??= error.message;
|
|
400
|
+
});
|
|
401
|
+
child.once("close", (status) => {
|
|
402
|
+
globalThis.clearTimeout(timer);
|
|
403
|
+
try {
|
|
404
|
+
if (fd !== void 0) fs2.fsyncSync(fd);
|
|
405
|
+
} catch {
|
|
406
|
+
failure2 ??= "Could not durably write process output.";
|
|
407
|
+
} finally {
|
|
408
|
+
try {
|
|
409
|
+
if (fd !== void 0) fs2.closeSync(fd);
|
|
410
|
+
} catch {
|
|
411
|
+
failure2 ??= "Could not close process output file.";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
resolve7({
|
|
415
|
+
status: failure2 === void 0 ? status : null,
|
|
416
|
+
stdout,
|
|
417
|
+
stderr,
|
|
418
|
+
...failure2 === void 0 ? {} : { error: failure2 }
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
} catch (cause) {
|
|
422
|
+
try {
|
|
423
|
+
if (fd !== void 0) fs2.closeSync(fd);
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
resolve7({
|
|
427
|
+
status: null,
|
|
428
|
+
stdout: "",
|
|
429
|
+
stderr: "",
|
|
430
|
+
error: cause instanceof Error ? cause.message : "Could not start process."
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
});
|
|
315
434
|
};
|
|
316
|
-
var
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
435
|
+
var serverComposeArgs = (installation, command) => [
|
|
436
|
+
"--context",
|
|
437
|
+
installation.dockerContext,
|
|
438
|
+
"compose",
|
|
439
|
+
"--project-name",
|
|
440
|
+
"mend",
|
|
441
|
+
"--project-directory",
|
|
442
|
+
installation.directory,
|
|
443
|
+
"--env-file",
|
|
444
|
+
path2.join(installation.directory, "server.env"),
|
|
445
|
+
"-f",
|
|
446
|
+
path2.join(installation.directory, "compose.yaml"),
|
|
447
|
+
...command
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
// src/doctor-bundle-collectors.ts
|
|
451
|
+
var json = (value) => `${JSON.stringify(value, null, 2)}
|
|
452
|
+
`;
|
|
453
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
454
|
+
var field = (value, key) => isRecord(value) ? value[key] : void 0;
|
|
455
|
+
var stringField = (value, key) => {
|
|
456
|
+
const found = field(value, key);
|
|
457
|
+
return typeof found === "string" ? found : null;
|
|
458
|
+
};
|
|
459
|
+
var stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
460
|
+
var outputOrFailure = (output) => {
|
|
461
|
+
if (output.error !== void 0) return `failed: ${output.error}`;
|
|
462
|
+
if (output.status !== 0)
|
|
463
|
+
return `exit ${output.status ?? "unknown"}
|
|
464
|
+
${output.stderr.trim() || output.stdout.trim()}`;
|
|
465
|
+
return output.stdout.trimEnd();
|
|
466
|
+
};
|
|
467
|
+
var failedOutput = (output, what) => new Error(
|
|
468
|
+
`${what}: ${output.error ?? `exit ${output.status ?? "unknown"}`}${output.stderr.trim() === "" ? "" : `
|
|
469
|
+
${output.stderr.trim()}`}`
|
|
470
|
+
);
|
|
471
|
+
var lastLines = (text, count) => {
|
|
472
|
+
const lines = text.split("\n");
|
|
473
|
+
return lines.length <= count ? text : lines.slice(lines.length - count).join("\n");
|
|
474
|
+
};
|
|
475
|
+
var mapLimit = async (items, limit, work) => {
|
|
476
|
+
const results = [];
|
|
477
|
+
let next = 0;
|
|
478
|
+
const worker = async () => {
|
|
479
|
+
for (; ; ) {
|
|
480
|
+
const index = next;
|
|
481
|
+
next += 1;
|
|
482
|
+
if (index >= items.length) return;
|
|
483
|
+
const item = items[index];
|
|
484
|
+
if (item === void 0) return;
|
|
485
|
+
results[index] = await work(item);
|
|
486
|
+
}
|
|
331
487
|
};
|
|
488
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
489
|
+
return results;
|
|
332
490
|
};
|
|
333
|
-
var
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
];
|
|
343
|
-
var notChecked = (label) => ({
|
|
344
|
-
label,
|
|
345
|
-
state: "todo",
|
|
346
|
-
detail: "not checked",
|
|
347
|
-
fix: null
|
|
348
|
-
});
|
|
349
|
-
var identityOf = (account) => {
|
|
350
|
-
const meta = account.metadata;
|
|
351
|
-
for (const key of ["login", "email", "accountEmail", "accountId"]) {
|
|
352
|
-
const value = meta[key];
|
|
353
|
-
if (typeof value === "string" && value !== "") return value;
|
|
491
|
+
var pathOf = (command, env = process.env) => {
|
|
492
|
+
for (const directory of (env["PATH"] ?? "").split(path3.delimiter)) {
|
|
493
|
+
if (directory === "") continue;
|
|
494
|
+
const candidate = path3.join(directory, command);
|
|
495
|
+
try {
|
|
496
|
+
fs3.accessSync(candidate, fs3.constants.X_OK);
|
|
497
|
+
return candidate;
|
|
498
|
+
} catch {
|
|
499
|
+
}
|
|
354
500
|
}
|
|
355
501
|
return null;
|
|
356
502
|
};
|
|
357
|
-
var
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
503
|
+
var cliCollector = (deps) => ({
|
|
504
|
+
name: "cli",
|
|
505
|
+
collect: async () => [
|
|
506
|
+
{
|
|
507
|
+
path: "cli.json",
|
|
508
|
+
content: json({
|
|
509
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
510
|
+
cliVersion: deps.cliVersion,
|
|
511
|
+
node: process.versions.node,
|
|
512
|
+
platform: process.platform,
|
|
513
|
+
arch: process.arch,
|
|
514
|
+
osType: os2.type(),
|
|
515
|
+
osRelease: os2.release(),
|
|
516
|
+
hostname: os2.hostname(),
|
|
517
|
+
term: deps.env["TERM"] ?? null,
|
|
518
|
+
shell: deps.env["SHELL"] ?? null,
|
|
519
|
+
lang: deps.env["LANG"] ?? null,
|
|
520
|
+
stdinTty: deps.stdinTty,
|
|
521
|
+
stdoutTty: deps.stdoutTty,
|
|
522
|
+
serverUrl: deps.serverUrl,
|
|
523
|
+
configuredUrl: deps.configuredUrl,
|
|
524
|
+
deviceId: deps.deviceId,
|
|
525
|
+
tokenSaved: deps.tokenSaved,
|
|
526
|
+
mendEnvNames: Object.keys(deps.env).filter((name) => name.startsWith("MEND_")).toSorted()
|
|
527
|
+
})
|
|
371
528
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
529
|
+
]
|
|
530
|
+
});
|
|
531
|
+
var doctorCollector = (deps) => ({
|
|
532
|
+
name: "doctor",
|
|
533
|
+
collect: async () => [{ path: "doctor.txt", content: await deps.doctor() }]
|
|
534
|
+
});
|
|
535
|
+
var serverHealthCollector = (deps) => ({
|
|
536
|
+
name: "server-health",
|
|
537
|
+
collect: async () => [
|
|
538
|
+
{ path: "server-health.json", content: json(await deps.get("/health")) }
|
|
539
|
+
]
|
|
540
|
+
});
|
|
541
|
+
var NO_SERVER = "no Mend server is installed on this machine (mend server setup installs one)";
|
|
542
|
+
var serverFactsOnce = (deps) => {
|
|
543
|
+
let pending = null;
|
|
544
|
+
return () => {
|
|
545
|
+
pending ??= deps.readServer();
|
|
546
|
+
return pending;
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
var serverConfigCollector = (facts) => ({
|
|
550
|
+
name: "server-config",
|
|
551
|
+
collect: async () => {
|
|
552
|
+
const server = await facts();
|
|
553
|
+
if (server === null) throw new Error(NO_SERVER);
|
|
554
|
+
return [
|
|
555
|
+
{
|
|
556
|
+
path: "server-config.json",
|
|
557
|
+
content: json({
|
|
558
|
+
directory: server.directory,
|
|
559
|
+
config: server.config,
|
|
560
|
+
envKeys: server.envKeys
|
|
561
|
+
})
|
|
562
|
+
},
|
|
563
|
+
{ path: "server-compose.yaml", content: server.compose }
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
var COMPOSE_TIMEOUT_MS = 3e4;
|
|
568
|
+
var serverLogsCollector = (deps, facts) => ({
|
|
569
|
+
name: "server-logs",
|
|
570
|
+
collect: async () => {
|
|
571
|
+
const server = await facts();
|
|
572
|
+
if (server === null) throw new Error(NO_SERVER);
|
|
573
|
+
const installation = {
|
|
574
|
+
directory: server.directory,
|
|
575
|
+
dockerContext: server.config.dockerContext
|
|
576
|
+
};
|
|
577
|
+
const listed = await deps.run(
|
|
578
|
+
"docker",
|
|
579
|
+
serverComposeArgs(installation, ["ps", "--all", "--services"]),
|
|
580
|
+
COMPOSE_TIMEOUT_MS
|
|
393
581
|
);
|
|
582
|
+
if (listed.status !== 0 || listed.error !== void 0)
|
|
583
|
+
throw failedOutput(listed, "docker compose ps failed");
|
|
584
|
+
const services = listed.stdout.split(/\s+/).filter((name) => name !== "");
|
|
585
|
+
if (services.length === 0) throw new Error("no Compose containers found (mend server start)");
|
|
586
|
+
const files = [];
|
|
587
|
+
for (const service of services) {
|
|
588
|
+
const logs = await deps.run(
|
|
589
|
+
"docker",
|
|
590
|
+
serverComposeArgs(installation, [
|
|
591
|
+
"logs",
|
|
592
|
+
"--no-color",
|
|
593
|
+
"--tail",
|
|
594
|
+
String(deps.tail),
|
|
595
|
+
service
|
|
596
|
+
]),
|
|
597
|
+
COMPOSE_TIMEOUT_MS
|
|
598
|
+
);
|
|
599
|
+
const text = logs.error !== void 0 || logs.status !== 0 ? outputOrFailure(logs) : `${logs.stdout.trimEnd()}${logs.stderr.trim() === "" ? "" : `
|
|
600
|
+
--- stderr ---
|
|
601
|
+
${logs.stderr.trimEnd()}`}`;
|
|
602
|
+
files.push({ path: `server-logs/${service}.log`, content: `${text}
|
|
603
|
+
` });
|
|
604
|
+
}
|
|
605
|
+
return files;
|
|
394
606
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
checks.push({
|
|
409
|
-
label: "sealant",
|
|
410
|
-
state: "ok",
|
|
411
|
-
detail: `connected \xB7 ${connection.value.baseUrl}`,
|
|
412
|
-
fix: null
|
|
413
|
-
});
|
|
414
|
-
} else {
|
|
415
|
-
checks.push({
|
|
416
|
-
label: "sealant",
|
|
417
|
-
state: "failed",
|
|
418
|
-
detail: `${connection.value.status} \xB7 ${connection.value.detail ?? connection.value.baseUrl}`,
|
|
419
|
-
fix: null
|
|
420
|
-
});
|
|
607
|
+
});
|
|
608
|
+
var DOCKER_TIMEOUT_MS = 2e4;
|
|
609
|
+
var CONTAINER_FILTERS = [
|
|
610
|
+
["--filter", "label=com.docker.compose.project=mend"],
|
|
611
|
+
["--filter", "name=sealant-"],
|
|
612
|
+
["--filter", "label=sealant.workspace"]
|
|
613
|
+
];
|
|
614
|
+
var parseContainerRows = (stdout) => stdout.split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
615
|
+
let parsed;
|
|
616
|
+
try {
|
|
617
|
+
parsed = JSON.parse(line);
|
|
618
|
+
} catch {
|
|
619
|
+
return [];
|
|
421
620
|
}
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
621
|
+
const id = stringField(parsed, "ID");
|
|
622
|
+
if (id === null) return [];
|
|
623
|
+
return [
|
|
624
|
+
{
|
|
625
|
+
id,
|
|
626
|
+
name: stringField(parsed, "Names") ?? id,
|
|
627
|
+
image: stringField(parsed, "Image") ?? "",
|
|
628
|
+
state: stringField(parsed, "State") ?? "",
|
|
629
|
+
status: stringField(parsed, "Status") ?? "",
|
|
630
|
+
labels: stringField(parsed, "Labels") ?? ""
|
|
428
631
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
632
|
+
];
|
|
633
|
+
});
|
|
634
|
+
var dockerFactsOnce = (deps, facts) => {
|
|
635
|
+
let pending = null;
|
|
636
|
+
const read = async () => {
|
|
637
|
+
let context = [];
|
|
638
|
+
const notes = [];
|
|
639
|
+
try {
|
|
640
|
+
const server = await facts();
|
|
641
|
+
if (server !== null) context = ["--context", server.config.dockerContext];
|
|
642
|
+
} catch (error) {
|
|
643
|
+
notes.push(`server config: ${error instanceof Error ? error.message : String(error)}`);
|
|
644
|
+
}
|
|
645
|
+
const version = await deps.run("docker", [...context, "version"], DOCKER_TIMEOUT_MS);
|
|
646
|
+
if (version.error !== void 0 || version.status !== 0)
|
|
647
|
+
throw failedOutput(version, "docker version failed");
|
|
648
|
+
const info = await deps.run("docker", [...context, "info"], DOCKER_TIMEOUT_MS);
|
|
649
|
+
const contexts = await deps.run("docker", ["context", "ls"], DOCKER_TIMEOUT_MS);
|
|
650
|
+
const byId = /* @__PURE__ */ new Map();
|
|
651
|
+
for (const filter of CONTAINER_FILTERS) {
|
|
652
|
+
const listed = await deps.run(
|
|
653
|
+
"docker",
|
|
654
|
+
[...context, "ps", "--all", "--no-trunc", "--format", "{{json .}}", ...filter],
|
|
655
|
+
DOCKER_TIMEOUT_MS
|
|
656
|
+
);
|
|
657
|
+
if (listed.error !== void 0 || listed.status !== 0) {
|
|
658
|
+
notes.push(`docker ps ${filter.join(" ")}: ${outputOrFailure(listed)}`);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
for (const row of parseContainerRows(listed.stdout)) byId.set(row.id, row);
|
|
438
662
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
663
|
+
return {
|
|
664
|
+
context,
|
|
665
|
+
version: outputOrFailure(version),
|
|
666
|
+
info: outputOrFailure(info),
|
|
667
|
+
contexts: outputOrFailure(contexts),
|
|
668
|
+
containers: [...byId.values()].toSorted((a, b) => a.name.localeCompare(b.name)),
|
|
669
|
+
notes
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
return () => {
|
|
673
|
+
pending ??= read();
|
|
674
|
+
return pending;
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
var summarizeInspect = (container) => {
|
|
678
|
+
const config = field(container, "Config");
|
|
679
|
+
const hostConfig = field(container, "HostConfig");
|
|
680
|
+
const state = field(container, "State");
|
|
681
|
+
const mounts = field(container, "Mounts");
|
|
682
|
+
return {
|
|
683
|
+
id: stringField(container, "Id"),
|
|
684
|
+
name: stringField(container, "Name"),
|
|
685
|
+
created: stringField(container, "Created"),
|
|
686
|
+
image: stringField(config, "Image"),
|
|
687
|
+
imageId: stringField(container, "Image"),
|
|
688
|
+
state: isRecord(state) ? {
|
|
689
|
+
status: state["Status"],
|
|
690
|
+
exitCode: state["ExitCode"],
|
|
691
|
+
oomKilled: state["OOMKilled"],
|
|
692
|
+
startedAt: state["StartedAt"],
|
|
693
|
+
finishedAt: state["FinishedAt"],
|
|
694
|
+
error: state["Error"]
|
|
695
|
+
} : null,
|
|
696
|
+
privileged: field(hostConfig, "Privileged"),
|
|
697
|
+
runtime: field(hostConfig, "Runtime"),
|
|
698
|
+
networkMode: field(hostConfig, "NetworkMode"),
|
|
699
|
+
restartPolicy: field(hostConfig, "RestartPolicy"),
|
|
700
|
+
portBindings: field(hostConfig, "PortBindings"),
|
|
701
|
+
mounts: Array.isArray(mounts) ? mounts.map((mount) => ({
|
|
702
|
+
type: field(mount, "Type"),
|
|
703
|
+
source: field(mount, "Source"),
|
|
704
|
+
destination: field(mount, "Destination"),
|
|
705
|
+
mode: field(mount, "Mode"),
|
|
706
|
+
rw: field(mount, "RW")
|
|
707
|
+
})) : null,
|
|
708
|
+
envNames: stringList(field(config, "Env")).map((entry) => entry.split("=")[0] ?? entry),
|
|
709
|
+
labels: field(config, "Labels")
|
|
710
|
+
};
|
|
711
|
+
};
|
|
712
|
+
var dockerCollector = (deps, docker2) => ({
|
|
713
|
+
name: "docker",
|
|
714
|
+
collect: async () => {
|
|
715
|
+
const found = await docker2();
|
|
716
|
+
const sections = [
|
|
717
|
+
`# docker version${found.context.length === 0 ? "" : ` (${found.context.join(" ")})`}`,
|
|
718
|
+
found.version,
|
|
719
|
+
"",
|
|
720
|
+
"# docker info",
|
|
721
|
+
found.info,
|
|
722
|
+
"",
|
|
723
|
+
"# docker context ls",
|
|
724
|
+
found.contexts,
|
|
725
|
+
"",
|
|
726
|
+
"# docker ps -a (Compose project mend \xB7 names sealant-* \xB7 label sealant.workspace)",
|
|
727
|
+
found.containers.length === 0 ? "none" : found.containers.map((row) => `${row.id.slice(0, 12)} ${row.name} ${row.image} ${row.status}`).join("\n")
|
|
728
|
+
];
|
|
729
|
+
if (found.notes.length > 0) sections.push("", "# notes", ...found.notes);
|
|
730
|
+
if (found.containers.length > 0) {
|
|
731
|
+
const inspected = await deps.run(
|
|
732
|
+
"docker",
|
|
733
|
+
[...found.context, "inspect", ...found.containers.map((row) => row.id)],
|
|
734
|
+
DOCKER_TIMEOUT_MS
|
|
735
|
+
);
|
|
736
|
+
sections.push("", "# docker inspect (Env as names only)");
|
|
737
|
+
if (inspected.error !== void 0 || inspected.status !== 0) {
|
|
738
|
+
sections.push(outputOrFailure(inspected));
|
|
739
|
+
} else {
|
|
740
|
+
let parsed = null;
|
|
741
|
+
try {
|
|
742
|
+
parsed = JSON.parse(inspected.stdout);
|
|
743
|
+
} catch {
|
|
744
|
+
sections.push("docker inspect returned invalid JSON");
|
|
745
|
+
}
|
|
746
|
+
if (Array.isArray(parsed)) sections.push(json(parsed.map(summarizeInspect)).trimEnd());
|
|
451
747
|
}
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
const grant = probes.claudeGrant();
|
|
455
|
-
if (grant !== null) {
|
|
456
|
-
const facts = claudeGrantFacts(grant);
|
|
457
|
-
if (facts === null) {
|
|
458
|
-
checks.push({
|
|
459
|
-
label: "grant",
|
|
460
|
-
state: "failed",
|
|
461
|
-
detail: "unreadable",
|
|
462
|
-
fix: "mend connect claude"
|
|
463
|
-
});
|
|
464
|
-
} else if (!facts.hasRefreshToken) {
|
|
465
|
-
checks.push({
|
|
466
|
-
label: "grant",
|
|
467
|
-
state: "failed",
|
|
468
|
-
detail: "signed out",
|
|
469
|
-
fix: "mend connect claude"
|
|
470
|
-
});
|
|
471
|
-
} else if (facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now()) {
|
|
472
|
-
checks.push({
|
|
473
|
-
label: "grant",
|
|
474
|
-
state: "todo",
|
|
475
|
-
detail: `expired ${day(facts.refreshExpiresAt)}`,
|
|
476
|
-
fix: "mend connect claude"
|
|
477
|
-
});
|
|
478
|
-
} else {
|
|
479
|
-
checks.push({
|
|
480
|
-
label: "grant",
|
|
481
|
-
state: "ok",
|
|
482
|
-
detail: facts.refreshExpiresAt === null ? "Mend's own" : `Mend's own \xB7 expires ${day(facts.refreshExpiresAt)}`,
|
|
483
|
-
fix: null
|
|
484
|
-
});
|
|
485
748
|
}
|
|
749
|
+
return [{ path: "docker.txt", content: `${sections.join("\n")}
|
|
750
|
+
` }];
|
|
486
751
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const label = `${command} cli`;
|
|
496
|
-
if (!probes.onPath(command)) {
|
|
497
|
-
checks.push({ label, state: "todo", detail: "not on PATH", fix: null });
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
const credential = probes.localCredential(provider);
|
|
501
|
-
checks.push(
|
|
502
|
-
credential === null ? {
|
|
503
|
-
label,
|
|
504
|
-
state: "todo",
|
|
505
|
-
detail: "on PATH \xB7 no credential here",
|
|
506
|
-
fix: LOGIN_COMMANDS[provider]
|
|
507
|
-
} : { label, state: "ok", detail: "on PATH \xB7 credential present", fix: null }
|
|
752
|
+
});
|
|
753
|
+
var isWorkspaceContainer = (row) => row.name.startsWith("sealant-") || row.labels.includes("sealant.workspace=");
|
|
754
|
+
var workspaceLogsCollector = (deps, docker2) => ({
|
|
755
|
+
name: "workspace-logs",
|
|
756
|
+
collect: async () => {
|
|
757
|
+
const found = await docker2();
|
|
758
|
+
const running = found.containers.filter(
|
|
759
|
+
(row) => isWorkspaceContainer(row) && row.state === "running"
|
|
508
760
|
);
|
|
761
|
+
if (running.length === 0) {
|
|
762
|
+
return [
|
|
763
|
+
{
|
|
764
|
+
path: "workspace-logs.txt",
|
|
765
|
+
content: "no running workspace container observed (names sealant-* or label sealant.workspace)\n"
|
|
766
|
+
}
|
|
767
|
+
];
|
|
768
|
+
}
|
|
769
|
+
const files = [];
|
|
770
|
+
for (const row of running) {
|
|
771
|
+
const logs = await deps.run(
|
|
772
|
+
"docker",
|
|
773
|
+
[...found.context, "logs", "--tail", String(deps.tail), row.id],
|
|
774
|
+
DOCKER_TIMEOUT_MS
|
|
775
|
+
);
|
|
776
|
+
const text = logs.error !== void 0 || logs.status !== 0 ? outputOrFailure(logs) : `${logs.stdout.trimEnd()}${logs.stderr.trim() === "" ? "" : `
|
|
777
|
+
--- stderr ---
|
|
778
|
+
${logs.stderr.trimEnd()}`}`;
|
|
779
|
+
files.push({ path: `workspace-logs/${row.name}.log`, content: `${text}
|
|
780
|
+
` });
|
|
781
|
+
}
|
|
782
|
+
return files;
|
|
509
783
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
var
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
784
|
+
});
|
|
785
|
+
var RECORD_PAGE_LIMIT = 1e3;
|
|
786
|
+
var RECORD_MAX_PAGES = 200;
|
|
787
|
+
var RECORD_TIME_BUDGET_MS = 3e4;
|
|
788
|
+
var SESSION_CONCURRENCY = 4;
|
|
789
|
+
var readRecordTail = async (deps, processId) => {
|
|
790
|
+
let cursor = "0";
|
|
791
|
+
let text = "";
|
|
792
|
+
let chunks = 0;
|
|
793
|
+
let pages = 0;
|
|
794
|
+
let status = null;
|
|
795
|
+
const deadline = Date.now() + RECORD_TIME_BUDGET_MS;
|
|
796
|
+
try {
|
|
797
|
+
for (; ; ) {
|
|
798
|
+
const page = await deps.get(
|
|
799
|
+
`/processes/${processId}/logs?from=${encodeURIComponent(cursor)}&limit=${RECORD_PAGE_LIMIT}`
|
|
800
|
+
);
|
|
801
|
+
pages += 1;
|
|
802
|
+
status = page.status;
|
|
803
|
+
for (const chunk of page.chunks)
|
|
804
|
+
text += Buffer.from(chunk.dataBase64, "base64").toString("utf8");
|
|
805
|
+
chunks += page.chunks.length;
|
|
806
|
+
text = lastLines(text, deps.tail + 1);
|
|
807
|
+
const advanced = page.nextFrom !== cursor;
|
|
808
|
+
cursor = page.nextFrom;
|
|
809
|
+
if (!advanced) return { text, chunks, pages, complete: true, status, failure: null };
|
|
810
|
+
if (pages >= RECORD_MAX_PAGES || Date.now() > deadline)
|
|
811
|
+
return { text, chunks, pages, complete: false, status, failure: null };
|
|
522
812
|
}
|
|
813
|
+
} catch (error) {
|
|
814
|
+
return {
|
|
815
|
+
text,
|
|
816
|
+
chunks,
|
|
817
|
+
pages,
|
|
818
|
+
complete: false,
|
|
819
|
+
status,
|
|
820
|
+
failure: error instanceof Error ? error.message : String(error)
|
|
821
|
+
};
|
|
523
822
|
}
|
|
524
|
-
return false;
|
|
525
823
|
};
|
|
526
|
-
var
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
824
|
+
var processLine = (row) => [
|
|
825
|
+
row.id,
|
|
826
|
+
row.kind,
|
|
827
|
+
row.label ?? row.harness ?? "",
|
|
828
|
+
row.status,
|
|
829
|
+
row.exitCode === null ? "" : `exit ${row.exitCode}`,
|
|
830
|
+
row.argv.length === 0 ? "" : `argv: ${row.argv.join(" ")}`
|
|
831
|
+
].filter((part) => part !== "").join(" \xB7 ");
|
|
832
|
+
var recordFile = async (deps, detail) => {
|
|
833
|
+
const recorded = detail.processes.filter((row) => row.sealantSessionId !== null);
|
|
834
|
+
if (recorded.length === 0) return null;
|
|
835
|
+
const sections = [
|
|
836
|
+
`# session ${detail.session.id} \xB7 ${recorded.length} recorded process(es) \xB7 last ${deps.tail} lines of each, raw PTY output as UTF-8`
|
|
837
|
+
];
|
|
838
|
+
for (const row of recorded) {
|
|
839
|
+
const tail = await readRecordTail(deps, row.id);
|
|
840
|
+
const facts = [
|
|
841
|
+
`${tail.chunks} chunks in ${tail.pages} page(s)`,
|
|
842
|
+
tail.status === null ? null : `record ${tail.status}`,
|
|
843
|
+
tail.complete ? null : `the read stopped at the page budget (${RECORD_MAX_PAGES} pages / ${RECORD_TIME_BUDGET_MS / 1e3} s) before the record ended, so these are the last lines READ, not the last lines recorded`,
|
|
844
|
+
tail.failure === null ? null : `read failed: ${tail.failure}`
|
|
845
|
+
].filter((fact) => fact !== null);
|
|
846
|
+
sections.push("", `== ${processLine(row)}`, `(${facts.join(" \xB7 ")})`, tail.text.trimEnd());
|
|
847
|
+
}
|
|
848
|
+
return `${sections.join("\n")}
|
|
849
|
+
`;
|
|
530
850
|
};
|
|
531
|
-
var
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
851
|
+
var sessionsCollector = (deps) => ({
|
|
852
|
+
name: "sessions",
|
|
853
|
+
collect: async () => {
|
|
854
|
+
const projects = await deps.get("/projects");
|
|
855
|
+
const details = await mapLimit(
|
|
856
|
+
projects,
|
|
857
|
+
SESSION_CONCURRENCY,
|
|
858
|
+
(project2) => deps.get(`/projects/${project2.id}?deadEnds=include`)
|
|
859
|
+
);
|
|
860
|
+
const rows = details.flatMap(
|
|
861
|
+
(detail) => detail.sessions.map((session) => ({ session, projectName: detail.project.name }))
|
|
862
|
+
).toSorted((a, b) => b.session.createdAt.localeCompare(a.session.createdAt));
|
|
863
|
+
const collected = await mapLimit(
|
|
864
|
+
rows,
|
|
865
|
+
SESSION_CONCURRENCY,
|
|
866
|
+
async ({ session, projectName }) => {
|
|
867
|
+
const files2 = [];
|
|
868
|
+
let detail;
|
|
869
|
+
try {
|
|
870
|
+
detail = await deps.get(`/sessions/${session.id}`);
|
|
871
|
+
} catch (error) {
|
|
872
|
+
files2.push({
|
|
873
|
+
path: `sessions/${session.id}.error.txt`,
|
|
874
|
+
content: `${error instanceof Error ? error.message : String(error)}
|
|
875
|
+
`
|
|
876
|
+
});
|
|
877
|
+
return {
|
|
878
|
+
files: files2,
|
|
879
|
+
summary: {
|
|
880
|
+
id: session.id,
|
|
881
|
+
project: projectName,
|
|
882
|
+
harness: session.harness,
|
|
883
|
+
label: session.label,
|
|
884
|
+
status: session.status,
|
|
885
|
+
createdAt: session.createdAt,
|
|
886
|
+
updatedAt: session.updatedAt ?? null,
|
|
887
|
+
detail: `sessions/${session.id}.error.txt`
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
files2.push({ path: `sessions/${session.id}.json`, content: json(detail) });
|
|
892
|
+
const record = await recordFile(deps, detail);
|
|
893
|
+
if (record !== null)
|
|
894
|
+
files2.push({ path: `sessions/${session.id}-record.txt`, content: record });
|
|
895
|
+
const services = new Set(
|
|
896
|
+
detail.processes.flatMap((row) => row.serviceId === null ? [] : [row.serviceId])
|
|
897
|
+
);
|
|
898
|
+
return {
|
|
899
|
+
files: files2,
|
|
900
|
+
summary: {
|
|
901
|
+
id: session.id,
|
|
902
|
+
project: projectName,
|
|
903
|
+
harness: session.harness,
|
|
904
|
+
label: session.label,
|
|
905
|
+
status: session.status,
|
|
906
|
+
worktree: session.worktree,
|
|
907
|
+
branch: session.branch,
|
|
908
|
+
createdAt: session.createdAt,
|
|
909
|
+
updatedAt: session.updatedAt ?? null,
|
|
910
|
+
processes: detail.processes.map((row) => ({
|
|
911
|
+
id: row.id,
|
|
912
|
+
kind: row.kind,
|
|
913
|
+
label: row.label,
|
|
914
|
+
harness: row.harness,
|
|
915
|
+
status: row.status,
|
|
916
|
+
exitCode: row.exitCode,
|
|
917
|
+
argv: row.argv,
|
|
918
|
+
createdAt: row.createdAt,
|
|
919
|
+
exitedAt: row.exitedAt,
|
|
920
|
+
recorded: row.sealantSessionId !== null
|
|
921
|
+
})),
|
|
922
|
+
services: services.size,
|
|
923
|
+
liveServices: detail.liveServices ?? null,
|
|
924
|
+
detail: `sessions/${session.id}.json`,
|
|
925
|
+
record: record === null ? null : `sessions/${session.id}-record.txt`
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
);
|
|
930
|
+
const summaries = collected.map((item) => item.summary);
|
|
931
|
+
const files = collected.flatMap((item) => item.files);
|
|
932
|
+
files.unshift({
|
|
933
|
+
path: "sessions.json",
|
|
934
|
+
content: json({
|
|
935
|
+
projects: projects.length,
|
|
936
|
+
sessions: rows.length,
|
|
937
|
+
recordTailLines: deps.tail,
|
|
938
|
+
rows: summaries
|
|
939
|
+
})
|
|
940
|
+
});
|
|
941
|
+
return files;
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
var accountsCollector = (deps) => ({
|
|
945
|
+
name: "accounts",
|
|
946
|
+
collect: async () => [{ path: "accounts.txt", content: await deps.accounts() }]
|
|
947
|
+
});
|
|
948
|
+
var TOOLS = ["claude", "codex", "gh", "git", "docker"];
|
|
949
|
+
var TOOL_TIMEOUT_MS = 15e3;
|
|
950
|
+
var toolsCollector = (deps) => ({
|
|
951
|
+
name: "tools",
|
|
952
|
+
collect: async () => {
|
|
953
|
+
const lines = [];
|
|
954
|
+
for (const tool of TOOLS) {
|
|
955
|
+
const found = deps.pathOf(tool);
|
|
956
|
+
if (found === null) {
|
|
957
|
+
lines.push(`${tool.padEnd(8)} not on PATH`);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const output = await deps.run(tool, ["--version"], TOOL_TIMEOUT_MS);
|
|
961
|
+
const version = output.error !== void 0 || output.status !== 0 ? `--version ${outputOrFailure(output).split("\n")[0] ?? ""}` : output.stdout.trim().split("\n")[0] ?? "";
|
|
962
|
+
lines.push(`${tool.padEnd(8)} ${found} \xB7 ${version}`);
|
|
963
|
+
}
|
|
964
|
+
return [{ path: "tools.txt", content: `${lines.join("\n")}
|
|
965
|
+
` }];
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
var bundleCollectors = (deps) => {
|
|
969
|
+
const server = serverFactsOnce(deps);
|
|
970
|
+
const docker2 = dockerFactsOnce(deps, server);
|
|
971
|
+
return [
|
|
972
|
+
cliCollector(deps),
|
|
973
|
+
doctorCollector(deps),
|
|
974
|
+
serverHealthCollector(deps),
|
|
975
|
+
serverConfigCollector(server),
|
|
976
|
+
dockerCollector(deps, docker2),
|
|
977
|
+
serverLogsCollector(deps, server),
|
|
978
|
+
workspaceLogsCollector(deps, docker2),
|
|
979
|
+
sessionsCollector(deps),
|
|
980
|
+
accountsCollector(deps),
|
|
981
|
+
toolsCollector(deps)
|
|
982
|
+
];
|
|
536
983
|
};
|
|
537
984
|
|
|
538
|
-
// src/
|
|
539
|
-
import * as
|
|
540
|
-
import * as
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
985
|
+
// src/doctor-bundle.ts
|
|
986
|
+
import * as fs4 from "node:fs";
|
|
987
|
+
import * as path4 from "node:path";
|
|
988
|
+
import { gzipSync } from "node:zlib";
|
|
989
|
+
|
|
990
|
+
// src/help.ts
|
|
991
|
+
var SECTIONS = [
|
|
992
|
+
"start",
|
|
993
|
+
"sessions",
|
|
994
|
+
"services",
|
|
995
|
+
"project setup",
|
|
996
|
+
"organization",
|
|
997
|
+
"this machine"
|
|
998
|
+
];
|
|
999
|
+
var project = (what = "the project") => ({
|
|
1000
|
+
flag: "--project <p>",
|
|
1001
|
+
text: `${what} by name, when the current directory is not inside it`
|
|
1002
|
+
});
|
|
1003
|
+
var sessionArg = "With no id, the one live session is taken; with several, a picker opens. A prefix of the id is enough.";
|
|
1004
|
+
var tunnelText = "On a server that is not this machine, the session's live Services declared --http or --https are tunneled to this machine's loopback while this terminal is attached, on the Service's own port when it is free and on a free one when it is not. One line per tunnel says where it opens, for example web \u2192 http://localhost:5173. A Service that stops closes its tunnel, and detaching closes them all. The Services keep running.";
|
|
1005
|
+
var noTunnel = {
|
|
1006
|
+
flag: "--no-tunnel",
|
|
1007
|
+
text: "do not tunnel the session's Services to this machine"
|
|
1008
|
+
};
|
|
1009
|
+
var COMMANDS = [
|
|
1010
|
+
// ── start ──────────────────────────────────────────────────────────────
|
|
556
1011
|
{
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
".
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
".config/mise/config.toml",
|
|
575
|
-
".config/atuin/config.toml",
|
|
576
|
-
".config/zellij/config.kdl"
|
|
577
|
-
]
|
|
578
|
-
}
|
|
579
|
-
];
|
|
580
|
-
var MAX_FILE_BYTES = 1024 * 1024;
|
|
581
|
-
var statFile = (home, relative4) => {
|
|
582
|
-
try {
|
|
583
|
-
const stat = fs3.statSync(path3.join(home, relative4));
|
|
584
|
-
return stat.isFile() ? stat : null;
|
|
585
|
-
} catch {
|
|
586
|
-
return null;
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
var scanDotfileCandidates = (home) => DOTFILE_CANDIDATES.flatMap(
|
|
590
|
-
({ group, paths }) => paths.flatMap((relative4) => {
|
|
591
|
-
const stat = statFile(home, relative4);
|
|
592
|
-
return stat === null ? [] : [{ path: relative4, group, bytes: stat.size }];
|
|
593
|
-
})
|
|
594
|
-
);
|
|
595
|
-
var homeRelativePath = (home, requested) => {
|
|
596
|
-
const relative4 = path3.relative(home, path3.resolve(home, requested));
|
|
597
|
-
const outside = relative4 === "" || relative4 === ".." || relative4.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative4);
|
|
598
|
-
return outside ? null : relative4.split(path3.sep).join("/");
|
|
599
|
-
};
|
|
600
|
-
var readSyncFiles = (home, paths_) => {
|
|
601
|
-
const files = [];
|
|
602
|
-
for (const requested of paths_) {
|
|
603
|
-
const relative4 = homeRelativePath(home, requested);
|
|
604
|
-
if (relative4 === null) {
|
|
605
|
-
return {
|
|
606
|
-
error: `${requested} is not under ${home} \u2014 only files in your home directory sync`
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
const stat = statFile(home, relative4);
|
|
610
|
-
if (stat === null) {
|
|
611
|
-
return { error: `${relative4} is not a file under ${home}` };
|
|
612
|
-
}
|
|
613
|
-
if (stat.size > MAX_FILE_BYTES) {
|
|
614
|
-
return { error: `${relative4} is over 1MB \u2014 dotfiles are text; trim the selection` };
|
|
615
|
-
}
|
|
616
|
-
files.push({
|
|
617
|
-
path: relative4,
|
|
618
|
-
contentsBase64: fs3.readFileSync(path3.join(home, relative4)).toString("base64"),
|
|
619
|
-
// eslint-disable-next-line no-bitwise -- permission bits are the point
|
|
620
|
-
mode: (stat.mode & 511).toString(8)
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
return { files };
|
|
624
|
-
};
|
|
625
|
-
var DOTFILES_MANAGERS = ["auto", "copy", "stow", "chezmoi"];
|
|
626
|
-
var VALUE_FLAGS = ["--ref", "--subdirectory", "--manager"];
|
|
627
|
-
var parseDotfilesRepoArgs = (args) => {
|
|
628
|
-
if (args.includes("--clear")) {
|
|
629
|
-
return args.length === 1 ? { kind: "clear" } : { kind: "error", error: "--clear takes no URL or other options" };
|
|
630
|
-
}
|
|
631
|
-
let url = null;
|
|
632
|
-
const values = /* @__PURE__ */ new Map();
|
|
633
|
-
let bootstrap = true;
|
|
634
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
635
|
-
const arg = args[index] ?? "";
|
|
636
|
-
if (arg === "--no-bootstrap") {
|
|
637
|
-
bootstrap = false;
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
const flag = VALUE_FLAGS.find((candidate) => candidate === arg);
|
|
641
|
-
if (flag !== void 0) {
|
|
642
|
-
const value = args[index + 1];
|
|
643
|
-
if (value === void 0 || value.trim() === "" || value.startsWith("--")) {
|
|
644
|
-
return { kind: "error", error: `${flag} needs a value` };
|
|
645
|
-
}
|
|
646
|
-
values.set(flag, value.trim());
|
|
647
|
-
index += 1;
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
if (arg.startsWith("-")) return { kind: "error", error: `unknown flag ${arg}` };
|
|
651
|
-
if (url !== null) return { kind: "error", error: "one repository URL only" };
|
|
652
|
-
url = arg.trim();
|
|
653
|
-
}
|
|
654
|
-
if (url === null || url === "") {
|
|
655
|
-
return { kind: "error", error: "name the repository URL, or --clear to remove it" };
|
|
656
|
-
}
|
|
657
|
-
const requested = values.get("--manager") ?? "auto";
|
|
658
|
-
const manager = DOTFILES_MANAGERS.find((candidate) => candidate === requested);
|
|
659
|
-
if (manager === void 0) {
|
|
660
|
-
return { kind: "error", error: `--manager must be one of ${DOTFILES_MANAGERS.join(", ")}` };
|
|
661
|
-
}
|
|
662
|
-
return {
|
|
663
|
-
kind: "set",
|
|
664
|
-
repository: {
|
|
665
|
-
url,
|
|
666
|
-
ref: values.get("--ref") ?? null,
|
|
667
|
-
subdirectory: values.get("--subdirectory") ?? null,
|
|
668
|
-
manager,
|
|
669
|
-
bootstrap
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
};
|
|
673
|
-
var dotfilesRepositoryFacts = (repository) => [
|
|
674
|
-
repository.ref ?? "default branch",
|
|
675
|
-
...repository.subdirectory === null ? [] : [`${repository.subdirectory}/`],
|
|
676
|
-
`manager ${repository.manager}`,
|
|
677
|
-
repository.bootstrap ? "install.sh on" : "install.sh off"
|
|
678
|
-
].join(" \xB7 ");
|
|
679
|
-
|
|
680
|
-
// src/env.ts
|
|
681
|
-
var formatLoadReport = (report, paint6) => {
|
|
682
|
-
const width = Math.max(
|
|
683
|
-
0,
|
|
684
|
-
...report.loaded.map((entry) => entry.name.length),
|
|
685
|
-
...report.rejected.map((entry) => entry.name.length)
|
|
686
|
-
);
|
|
687
|
-
const lines = [];
|
|
688
|
-
for (const entry of report.loaded) {
|
|
689
|
-
lines.push(
|
|
690
|
-
` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint6.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
|
|
691
|
-
);
|
|
692
|
-
}
|
|
693
|
-
for (const entry of report.rejected) {
|
|
694
|
-
lines.push(
|
|
695
|
-
` ${entry.name.padEnd(width)} ${paint6.warn("rejected")} ${paint6.dim(`\xB7 ${entry.reason}`)}`
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
return lines;
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
// src/help.ts
|
|
702
|
-
var SECTIONS = [
|
|
703
|
-
"start",
|
|
704
|
-
"sessions",
|
|
705
|
-
"services",
|
|
706
|
-
"project setup",
|
|
707
|
-
"organization",
|
|
708
|
-
"this machine"
|
|
709
|
-
];
|
|
710
|
-
var project = (what = "the project") => ({
|
|
711
|
-
flag: "--project <p>",
|
|
712
|
-
text: `${what} by name, when the current directory is not inside it`
|
|
713
|
-
});
|
|
714
|
-
var sessionArg = "With no id, the one live session is taken; with several, a picker opens. A prefix of the id is enough.";
|
|
715
|
-
var tunnelText = "On a server that is not this machine, the session's live Services declared --http or --https are tunneled to this machine's loopback while this terminal is attached, on the Service's own port when it is free and on a free one when it is not. One line per tunnel says where it opens, for example web \u2192 http://localhost:5173. A Service that stops closes its tunnel, and detaching closes them all. The Services keep running.";
|
|
716
|
-
var noTunnel = {
|
|
717
|
-
flag: "--no-tunnel",
|
|
718
|
-
text: "do not tunnel the session's Services to this machine"
|
|
719
|
-
};
|
|
720
|
-
var COMMANDS = [
|
|
721
|
-
// ── start ──────────────────────────────────────────────────────────────
|
|
722
|
-
{
|
|
723
|
-
name: "login",
|
|
724
|
-
section: "start",
|
|
725
|
-
summary: "sign this terminal in through the browser",
|
|
726
|
-
synopsis: ["[--url <server>]"],
|
|
727
|
-
description: [
|
|
728
|
-
"Opens <server>/authorize in your browser. You press Authorize there. The CLI saves a device token to ~/.config/mend/cli.json with mode 0600. No password is typed in the terminal, and the token can be revoked from the server at any time."
|
|
729
|
-
],
|
|
730
|
-
options: [
|
|
731
|
-
{
|
|
732
|
-
flag: "--url <server>",
|
|
733
|
-
text: "the Mend server. Default: MEND_URL, then http://localhost:3105"
|
|
734
|
-
}
|
|
735
|
-
],
|
|
736
|
-
examples: [
|
|
737
|
-
{ command: "mend login --url http://10.0.0.216:3105", text: "sign in to a LAN server" }
|
|
738
|
-
],
|
|
739
|
-
see: ["logout", "pair", "doctor"]
|
|
1012
|
+
name: "login",
|
|
1013
|
+
section: "start",
|
|
1014
|
+
summary: "sign this terminal in through the browser",
|
|
1015
|
+
synopsis: ["[--url <server>]"],
|
|
1016
|
+
description: [
|
|
1017
|
+
"Opens <server>/authorize in your browser. You press Authorize there. The CLI saves a device token to ~/.config/mend/cli.json with mode 0600. No password is typed in the terminal, and the token can be revoked from the server at any time."
|
|
1018
|
+
],
|
|
1019
|
+
options: [
|
|
1020
|
+
{
|
|
1021
|
+
flag: "--url <server>",
|
|
1022
|
+
text: "the Mend server. Default: MEND_URL, then http://localhost:3105"
|
|
1023
|
+
}
|
|
1024
|
+
],
|
|
1025
|
+
examples: [
|
|
1026
|
+
{ command: "mend login --url http://10.0.0.216:3105", text: "sign in to a LAN server" }
|
|
1027
|
+
],
|
|
1028
|
+
see: ["logout", "pair", "doctor"]
|
|
740
1029
|
},
|
|
741
1030
|
{
|
|
742
1031
|
name: "connect",
|
|
@@ -847,12 +1136,29 @@ var COMMANDS = [
|
|
|
847
1136
|
{
|
|
848
1137
|
name: "doctor",
|
|
849
1138
|
section: "start",
|
|
850
|
-
summary: "check this machine's setup",
|
|
851
|
-
synopsis: [],
|
|
1139
|
+
summary: "check this machine's setup, or bundle it for a bug report",
|
|
1140
|
+
synopsis: ["", "--bundle [--out <path>] [--tail <n>]"],
|
|
852
1141
|
description: [
|
|
853
|
-
"Read-only. One line per fact: the server, the sign-in, the platform connection, the provider accounts, the git key. Each unfinished line ends with the command that fixes it."
|
|
1142
|
+
"Read-only. One line per fact: the server, the sign-in, the platform connection, the provider accounts, the git key. Each unfinished line ends with the command that fixes it.",
|
|
1143
|
+
"--bundle collects what a maintainer asks for one output at a time into one tar.gz: this CLI and its environment, the doctor lines, the server's health, the local server's configuration (its compose file and the names of its .env keys, never their values), docker version, info, contexts, the Mend and workspace containers with their inspect facts, the local server's container logs, the running workspace containers' logs, every session with its processes, exit codes and argv, the recorded terminal output of each, the connected accounts, and the versions and paths of claude, codex, gh, git and docker on this machine.",
|
|
1144
|
+
"Each part is collected on its own: one that fails leaves a <name>.error.txt in the bundle instead of stopping it. One redactor runs over every file before it is written: bearer and header values, Slack, OpenAI, GitHub and AWS token shapes, JWTs, passwords in URLs, and any password, secret or token value. The archive is written with mode 0600. It still contains logs and configuration; read it before you share it."
|
|
1145
|
+
],
|
|
1146
|
+
options: [
|
|
1147
|
+
{ flag: "--bundle", text: "write the diagnostic archive instead of printing the checklist" },
|
|
1148
|
+
{
|
|
1149
|
+
flag: "--out <path>",
|
|
1150
|
+
text: "where the archive goes. Default: ~/.config/mend/bundles/mend-bundle-<time>.tgz"
|
|
1151
|
+
},
|
|
1152
|
+
{
|
|
1153
|
+
flag: "--tail <n>",
|
|
1154
|
+
text: "lines per container log and per recorded process, 1..2000. Default: 500"
|
|
1155
|
+
}
|
|
1156
|
+
],
|
|
1157
|
+
examples: [
|
|
1158
|
+
{ command: "mend doctor", text: "the checklist" },
|
|
1159
|
+
{ command: "mend doctor --bundle", text: "one archive to attach to a bug report" }
|
|
854
1160
|
],
|
|
855
|
-
see: ["login", "connect", "keys init"]
|
|
1161
|
+
see: ["login", "connect", "keys init", "server logs"]
|
|
856
1162
|
},
|
|
857
1163
|
// ── sessions ───────────────────────────────────────────────────────────
|
|
858
1164
|
{
|
|
@@ -862,7 +1168,7 @@ var COMMANDS = [
|
|
|
862
1168
|
synopsis: ["[--no-tunnel]"],
|
|
863
1169
|
description: [
|
|
864
1170
|
"A full-screen view of every project and session, updating live. The session pane takes three quarters of the screen: a read-only detail for the selected session with the conversation record it has written so far. The remaining quarter is a sidebar of three stacked sections, projects then worktrees then sessions, where the section you are in stands open and the other two fold to the line that says what is selected. Bare mend with no command opens the same thing.",
|
|
865
|
-
"Moving the selection only ever navigates: arrows or j/k move inside the open section, enter and the arrows move between the sidebar and the session pane, and nothing takes this terminal until you ask. Reading the record leaves the sidebar as it was. The verbs are a to attach a live session, r to resume a settled one, n for another session in the selected worktree, w for a new worktree, e to rename, v to review the change, o to open it in the browser, Shift+K to stop, Shift+D to remove, and Shift+R to refresh.",
|
|
1171
|
+
"Moving the selection only ever navigates: arrows or j/k move inside the open section, enter and the arrows move between the sidebar and the session pane, and nothing takes this terminal until you ask. Reading the record leaves the sidebar as it was. The verbs are a to attach a live session, r to resume a settled one, n for another session in the selected worktree, w for a new worktree, e to rename, v to review the change, o to open it in the browser, Shift+K to stop (on a row whose agent has stopped while its Services keep the workspace up, Shift+K stops those Services), Shift+D to remove, and Shift+R to refresh.",
|
|
866
1172
|
"Nothing is ever squeezed. A terminal too narrow for both gives the whole width to the side you are on, a terminal too short for three drawn panes shows the open section alone, and a one-line breadcrumb states whatever did not fit.",
|
|
867
1173
|
"On a server that is not this machine, the selected session's live Services declared --http or --https are tunneled to this machine's loopback while it stays selected, and the session pane shows where each one opens, for example web \u2192 http://localhost:5173.",
|
|
868
1174
|
"The dashboard needs Node 26 or newer for its terminal. Every other command works on Node 22."
|
|
@@ -899,12 +1205,17 @@ var COMMANDS = [
|
|
|
899
1205
|
name: "stop",
|
|
900
1206
|
section: "sessions",
|
|
901
1207
|
summary: "stop the agent; the record and the review remain",
|
|
902
|
-
synopsis: ["[session-id-prefix]", "--all [--project <p>]"],
|
|
1208
|
+
synopsis: ["[session-id-prefix]", "--all [--project <p>]", "--services [session-id-prefix]"],
|
|
903
1209
|
description: [
|
|
904
1210
|
"Ends the agent process. The workspace harvests the harness state and closes. The worktree, the record, and the review stay, and mend resume brings the conversation back.",
|
|
1211
|
+
"Services keep running after a stop, and a running Service keeps the workspace up. The stop then prints, for example, agent stopped \xB7 3 services keep the workspace up. --services stops all of them. Once nothing is live, the workspace closes.",
|
|
905
1212
|
sessionArg
|
|
906
1213
|
],
|
|
907
|
-
options: [
|
|
1214
|
+
options: [
|
|
1215
|
+
{ flag: "--all", text: "every live session" },
|
|
1216
|
+
project("limit --all to one project"),
|
|
1217
|
+
{ flag: "--services", text: "stop the session's Services instead of its agent" }
|
|
1218
|
+
],
|
|
908
1219
|
see: ["resume", "attach"]
|
|
909
1220
|
},
|
|
910
1221
|
{
|
|
@@ -1017,18 +1328,26 @@ var COMMANDS = [
|
|
|
1017
1328
|
name: "land",
|
|
1018
1329
|
section: "sessions",
|
|
1019
1330
|
summary: "push a session's change to origin and open its pull request",
|
|
1020
|
-
synopsis: [
|
|
1331
|
+
synopsis: [
|
|
1332
|
+
"<session> [--branch <name>] [--no-pr] [--title <text>] [--project <p>]",
|
|
1333
|
+
"<session> --check [--project <p>]"
|
|
1334
|
+
],
|
|
1021
1335
|
description: [
|
|
1022
1336
|
"Only the change's owner lands it: the owner of the worktree's first session, not someone who started a session in it later. Mend takes a checkpoint, commits what the agent left uncommitted on top of the agent's own commits and the last landing, and pushes that to origin with the project's git access. The agent's commits are pushed as they are. The session's branch, and the worktree's files, index and HEAD, are not touched.",
|
|
1023
1337
|
"The push only fast-forwards. When origin's branch has commits Mend has not seen, or origin refuses the push, nothing is pushed and the command prints the remote's own words. Mend never force-pushes.",
|
|
1024
1338
|
"When origin is on GitHub, Mend then opens a pull request into the session's base branch, or updates the one it opened before. That call runs in a workspace with your GitHub account (mend connect github). The description holds Mend's review summary when there is one, the changed files and links back to the session, and an update replaces only Mend's section of it. For any other origin the push happens and the pull request is reported as unavailable, with the reason.",
|
|
1025
1339
|
"Landing again after more work adds commits to the same branch and updates the same pull request. When nothing is new since the last landing, nothing is pushed and the command says so. The branch on origin is never the project's default branch or the pull request's base. The command then prints what Mend observed: the push, the pull request and its state, what changed since, and pushes the agent made itself. It exits 1 when the push was refused or a step failed.",
|
|
1340
|
+
"A pull request someone opened outside Mend, the agent through gh included, is found and recorded as the change's own: Mend looks after the agent pushes a branch and when its turn ends, and --check looks now. It asks GitHub, as you, about the worktree's branch, every branch the agent pushed, and the agent's head commit. A landing then pushes that pull request's branch and updates it. A pull request from a fork is shown and never updated: Mend pushes to origin only, and opens no second pull request beside it.",
|
|
1026
1341
|
"<session> is a prefix of the session id or the worktree's name. Settled sessions count."
|
|
1027
1342
|
],
|
|
1028
1343
|
options: [
|
|
1029
1344
|
{
|
|
1030
1345
|
flag: "--branch <name>",
|
|
1031
|
-
text: "the branch on origin. Default: where the change last pushed to, else mend/<worktree>"
|
|
1346
|
+
text: "the branch on origin. Default: where the change last pushed to, the branch the agent pushed, an adopted pull request's branch, else mend/<worktree>"
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
flag: "--check",
|
|
1350
|
+
text: "only look on GitHub for a pull request opened outside Mend, and record it; push nothing"
|
|
1032
1351
|
},
|
|
1033
1352
|
{ flag: "--no-pr", text: "push only; open or update no pull request" },
|
|
1034
1353
|
{
|
|
@@ -1293,6 +1612,22 @@ var COMMANDS = [
|
|
|
1293
1612
|
options: [{ flag: "--all", text: "every known config file" }],
|
|
1294
1613
|
see: ["dotfiles", "dotfiles repo"]
|
|
1295
1614
|
},
|
|
1615
|
+
{
|
|
1616
|
+
name: "git-author",
|
|
1617
|
+
section: "project setup",
|
|
1618
|
+
summary: "the name and email your workspaces commit as",
|
|
1619
|
+
synopsis: ["[<name> <email>]", "--clear"],
|
|
1620
|
+
description: [
|
|
1621
|
+
"Commits the agent makes in your workspaces name this author. Until you set one, it is the name and email you registered with. Without arguments, prints it and where it comes from.",
|
|
1622
|
+
"Each workspace receives it as system git config before the agent starts, so a user section in your dotfiles' .gitconfig or in a repository's own config still decides. Sessions launched after a change commit as the new author."
|
|
1623
|
+
],
|
|
1624
|
+
options: [{ flag: "--clear", text: "go back to your account's name and email" }],
|
|
1625
|
+
examples: [
|
|
1626
|
+
{ command: 'mend git-author "Anna Example" anna@example.com', text: "" },
|
|
1627
|
+
{ command: "mend git-author --clear", text: "" }
|
|
1628
|
+
],
|
|
1629
|
+
see: ["dotfiles", "keys mode"]
|
|
1630
|
+
},
|
|
1296
1631
|
{
|
|
1297
1632
|
name: "keys init",
|
|
1298
1633
|
section: "project setup",
|
|
@@ -1992,13 +2327,608 @@ var renderManIndex = (version) => {
|
|
|
1992
2327
|
return `${out.join("\n")}
|
|
1993
2328
|
`;
|
|
1994
2329
|
};
|
|
1995
|
-
var manFileName = (doc) => doc === null ? "mend.1" : `mend-${doc.name.replace(/ /g, "-")}.1`;
|
|
2330
|
+
var manFileName = (doc) => doc === null ? "mend.1" : `mend-${doc.name.replace(/ /g, "-")}.1`;
|
|
2331
|
+
|
|
2332
|
+
// src/doctor-bundle.ts
|
|
2333
|
+
var REDACTED = "[redacted]";
|
|
2334
|
+
var KEY_WORDS = "password|passwd|secret|token|api[_-]?key|private[_-]?key|credential";
|
|
2335
|
+
var blank = () => REDACTED;
|
|
2336
|
+
var RULES = [
|
|
2337
|
+
[
|
|
2338
|
+
/\b(authorization|proxy-authorization|x-api-key|cookie|set-cookie)(\s*[:=]\s*)[^\r\n]+/gi,
|
|
2339
|
+
(_match, header, separator) => `${header}${separator}${REDACTED}`
|
|
2340
|
+
],
|
|
2341
|
+
[/\bBearer\s+[A-Za-z0-9._~+/=-]+/g, () => `Bearer ${REDACTED}`],
|
|
2342
|
+
[/\b(?:xox[abops]|xapp)-[A-Za-z0-9-]+/g, blank],
|
|
2343
|
+
[/\bsk-[A-Za-z0-9_-]{8,}/g, blank],
|
|
2344
|
+
[/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{6,}/g, blank],
|
|
2345
|
+
[/\bgithub_pat_[A-Za-z0-9_]{6,}/g, blank],
|
|
2346
|
+
[/\beyJ[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]+)*/g, blank],
|
|
2347
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, blank],
|
|
2348
|
+
[
|
|
2349
|
+
/\b([a-z][a-z0-9+.-]*:\/\/[^:/\s@]+):[^@\s/]+@/gi,
|
|
2350
|
+
(_match, prefix) => `${prefix}:${REDACTED}@`
|
|
2351
|
+
],
|
|
2352
|
+
[
|
|
2353
|
+
new RegExp(
|
|
2354
|
+
`\\b([A-Za-z0-9_.-]*(?:${KEY_WORDS})[A-Za-z0-9_.-]*)("?\\s*[=:]\\s*"?)([^"\\s,;&]+)`,
|
|
2355
|
+
"gi"
|
|
2356
|
+
),
|
|
2357
|
+
(_match, key, separator, value) => /^(?:true|false|null)$/i.test(value) ? `${key}${separator}${value}` : `${key}${separator}${REDACTED}`
|
|
2358
|
+
],
|
|
2359
|
+
[/^(\s*(?:export\s+)?[A-Z][A-Z0-9_]*=)(.+)$/gm, (_match, key) => `${key}${REDACTED}`]
|
|
2360
|
+
];
|
|
2361
|
+
var redact = (text) => {
|
|
2362
|
+
let out = text;
|
|
2363
|
+
for (const [pattern, replacer] of RULES) out = out.replace(pattern, replacer);
|
|
2364
|
+
return out;
|
|
2365
|
+
};
|
|
2366
|
+
var octal = (value, width) => `${value.toString(8).padStart(width - 1, "0")}\0`;
|
|
2367
|
+
var splitName = (name) => {
|
|
2368
|
+
if (Buffer.byteLength(name) <= 100) return { name, prefix: "" };
|
|
2369
|
+
let cut = name.lastIndexOf("/");
|
|
2370
|
+
while (cut > 0 && Buffer.byteLength(name.slice(cut + 1)) > 100)
|
|
2371
|
+
cut = name.lastIndexOf("/", cut - 1);
|
|
2372
|
+
if (cut <= 0 || Buffer.byteLength(name.slice(0, cut)) > 155)
|
|
2373
|
+
throw new Error(`tar entry name too long: ${name}`);
|
|
2374
|
+
return { name: name.slice(cut + 1), prefix: name.slice(0, cut) };
|
|
2375
|
+
};
|
|
2376
|
+
var tarHeader = (entry, mtime) => {
|
|
2377
|
+
const header = Buffer.alloc(512, 0);
|
|
2378
|
+
const { name, prefix } = splitName(entry.path);
|
|
2379
|
+
header.write(name, 0, 100, "utf8");
|
|
2380
|
+
header.write(octal(420, 8), 100, 8, "ascii");
|
|
2381
|
+
header.write(octal(0, 8), 108, 8, "ascii");
|
|
2382
|
+
header.write(octal(0, 8), 116, 8, "ascii");
|
|
2383
|
+
header.write(octal(entry.content.length, 12), 124, 12, "ascii");
|
|
2384
|
+
header.write(octal(mtime, 12), 136, 12, "ascii");
|
|
2385
|
+
header.write(" ", 148, 8, "ascii");
|
|
2386
|
+
header.write("0", 156, 1, "ascii");
|
|
2387
|
+
header.write("ustar\0", 257, 6, "ascii");
|
|
2388
|
+
header.write("00", 263, 2, "ascii");
|
|
2389
|
+
header.write("mend", 265, 32, "ascii");
|
|
2390
|
+
header.write("mend", 297, 32, "ascii");
|
|
2391
|
+
header.write(octal(0, 8), 329, 8, "ascii");
|
|
2392
|
+
header.write(octal(0, 8), 337, 8, "ascii");
|
|
2393
|
+
header.write(prefix, 345, 155, "utf8");
|
|
2394
|
+
let sum = 0;
|
|
2395
|
+
for (const byte of header) sum += byte;
|
|
2396
|
+
header.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "ascii");
|
|
2397
|
+
return header;
|
|
2398
|
+
};
|
|
2399
|
+
var tarGz = (entries, now) => {
|
|
2400
|
+
const mtime = Math.floor(now.getTime() / 1e3);
|
|
2401
|
+
const blocks = [];
|
|
2402
|
+
for (const entry of entries) {
|
|
2403
|
+
blocks.push(tarHeader(entry, mtime), entry.content);
|
|
2404
|
+
const remainder = entry.content.length % 512;
|
|
2405
|
+
if (remainder !== 0) blocks.push(Buffer.alloc(512 - remainder, 0));
|
|
2406
|
+
}
|
|
2407
|
+
blocks.push(Buffer.alloc(1024, 0));
|
|
2408
|
+
return gzipSync(Buffer.concat(blocks));
|
|
2409
|
+
};
|
|
2410
|
+
var describeError = (error) => `${error instanceof Error ? error.message : String(error)}
|
|
2411
|
+
`;
|
|
2412
|
+
var collectBundle = async (collectors) => {
|
|
2413
|
+
const files = [];
|
|
2414
|
+
for (const collector of collectors) {
|
|
2415
|
+
try {
|
|
2416
|
+
for (const file of await collector.collect()) {
|
|
2417
|
+
files.push({ path: file.path, content: redact(file.content) });
|
|
2418
|
+
}
|
|
2419
|
+
} catch (error) {
|
|
2420
|
+
files.push({ path: `${collector.name}.error.txt`, content: redact(describeError(error)) });
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
return files;
|
|
2424
|
+
};
|
|
2425
|
+
var bundleFileName = (now) => `mend-bundle-${now.toISOString().replace(/\.\d{3}Z$/, "Z").replace(/:/g, "-")}.tgz`;
|
|
2426
|
+
var writeBundle = (files, outPath, now) => {
|
|
2427
|
+
const directory = path4.dirname(outPath);
|
|
2428
|
+
fs4.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
2429
|
+
const root = path4.basename(outPath).replace(/\.(?:tgz|tar\.gz)$/, "");
|
|
2430
|
+
const entries = files.map((file) => ({
|
|
2431
|
+
path: file.path,
|
|
2432
|
+
content: Buffer.from(file.content, "utf8")
|
|
2433
|
+
}));
|
|
2434
|
+
const bytes = tarGz(
|
|
2435
|
+
entries.map((entry) => ({ path: `${root}/${entry.path}`, content: entry.content })),
|
|
2436
|
+
now
|
|
2437
|
+
);
|
|
2438
|
+
fs4.writeFileSync(outPath, bytes, { mode: 384 });
|
|
2439
|
+
fs4.chmodSync(outPath, 384);
|
|
2440
|
+
return {
|
|
2441
|
+
path: outPath,
|
|
2442
|
+
bytes: bytes.length,
|
|
2443
|
+
entries: entries.map((entry) => ({ path: entry.path, bytes: entry.content.length }))
|
|
2444
|
+
};
|
|
2445
|
+
};
|
|
2446
|
+
var formatBytes = (bytes) => {
|
|
2447
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
2448
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
2449
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2450
|
+
};
|
|
2451
|
+
var NOTICE = "Contains logs and configuration; secrets are redacted, but read it before sharing.";
|
|
2452
|
+
var bundleReport = (written) => {
|
|
2453
|
+
const width = Math.max(...written.entries.map((entry) => entry.path.length), 0);
|
|
2454
|
+
return [
|
|
2455
|
+
`${written.path} \xB7 ${formatBytes(written.bytes)}`,
|
|
2456
|
+
...written.entries.map((entry) => ` ${entry.path.padEnd(width)} ${formatBytes(entry.bytes)}`),
|
|
2457
|
+
NOTICE
|
|
2458
|
+
];
|
|
2459
|
+
};
|
|
2460
|
+
var DEFAULT_TAIL = 500;
|
|
2461
|
+
var MAX_TAIL = 2e3;
|
|
2462
|
+
var usage = (message) => ({
|
|
2463
|
+
kind: "usage",
|
|
2464
|
+
message: `${message}
|
|
2465
|
+
${usageOf("doctor")}`
|
|
2466
|
+
});
|
|
2467
|
+
var parseBundleArgs = (args) => {
|
|
2468
|
+
let out = null;
|
|
2469
|
+
let tail = DEFAULT_TAIL;
|
|
2470
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2471
|
+
const arg = args[index];
|
|
2472
|
+
if (arg === "--bundle") continue;
|
|
2473
|
+
if (arg === "--out") {
|
|
2474
|
+
const value = args[index + 1];
|
|
2475
|
+
if (value === void 0 || value.startsWith("--")) return usage("--out needs a path");
|
|
2476
|
+
out = value;
|
|
2477
|
+
index += 1;
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
if (arg === "--tail") {
|
|
2481
|
+
const value = args[index + 1];
|
|
2482
|
+
if (value === void 0 || !/^\d+$/.test(value)) return usage("--tail needs a number");
|
|
2483
|
+
tail = Number(value);
|
|
2484
|
+
if (tail < 1 || tail > MAX_TAIL) return usage(`--tail is 1..${MAX_TAIL}`);
|
|
2485
|
+
index += 1;
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
return usage(`unknown option "${arg ?? ""}"`);
|
|
2489
|
+
}
|
|
2490
|
+
return { kind: "ok", args: { out, tail } };
|
|
2491
|
+
};
|
|
2492
|
+
var doctorBundleCommand = async (args, deps) => {
|
|
2493
|
+
const parsed = parseBundleArgs(args);
|
|
2494
|
+
if (parsed.kind === "usage") {
|
|
2495
|
+
deps.warn(`mend: ${parsed.message}`);
|
|
2496
|
+
process.exitCode = 1;
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
const now = deps.now();
|
|
2500
|
+
const outPath = path4.resolve(parsed.args.out ?? path4.join(deps.defaultDir, bundleFileName(now)));
|
|
2501
|
+
deps.warn("collecting \xB7 this can take a minute");
|
|
2502
|
+
const files = await collectBundle(deps.collectors(parsed.args.tail));
|
|
2503
|
+
const written = writeBundle(files, outPath, now);
|
|
2504
|
+
for (const line of bundleReport(written)) deps.say(line);
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2507
|
+
// src/doctor.ts
|
|
2508
|
+
import * as fs5 from "node:fs";
|
|
2509
|
+
import * as path5 from "node:path";
|
|
2510
|
+
var TIMEOUT_MS = 3e3;
|
|
2511
|
+
var MARKS = { ok: "\u2713", todo: "\u25CB", failed: "\u2717" };
|
|
2512
|
+
var LABEL_WIDTH = 11;
|
|
2513
|
+
var day = (at) => at.toISOString().slice(0, 10);
|
|
2514
|
+
var formatCheck = (check, paint6 = (_state, mark) => mark) => `${paint6(check.state, MARKS[check.state])} ${check.label.padEnd(LABEL_WIDTH)} ${check.detail}${check.fix === null ? "" : ` \u2192 ${check.fix}`}`;
|
|
2515
|
+
var getJson = async (config, route) => {
|
|
2516
|
+
try {
|
|
2517
|
+
const response = await fetch(`${config.url}/api${route}`, {
|
|
2518
|
+
headers: config.token === null ? {} : { authorization: `Bearer ${config.token}` },
|
|
2519
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
2520
|
+
});
|
|
2521
|
+
if (!response.ok) return { value: null, status: response.status };
|
|
2522
|
+
return { value: await response.json(), status: response.status };
|
|
2523
|
+
} catch {
|
|
2524
|
+
return { value: null, status: null };
|
|
2525
|
+
}
|
|
2526
|
+
};
|
|
2527
|
+
var exposureCheck = (exposure) => {
|
|
2528
|
+
const beyondMachine = exposure.declared !== "loopback" && exposure.originOnMachine !== true;
|
|
2529
|
+
const gateOpen = beyondMachine ? exposure.gateOpen : 0;
|
|
2530
|
+
const facts = [
|
|
2531
|
+
`declared ${exposure.declared}`,
|
|
2532
|
+
`${exposure.originScheme} origin`,
|
|
2533
|
+
...exposure.arrivedVia === "trusted-proxy" ? ["arrived via a trusted proxy"] : [],
|
|
2534
|
+
...gateOpen === 0 ? [] : [`${gateOpen} gate items open`]
|
|
2535
|
+
].join(" \xB7 ");
|
|
2536
|
+
const plainBeyondMachine = beyondMachine && exposure.originScheme === "http";
|
|
2537
|
+
return {
|
|
2538
|
+
label: "exposure",
|
|
2539
|
+
state: plainBeyondMachine ? "todo" : "ok",
|
|
2540
|
+
detail: facts,
|
|
2541
|
+
fix: plainBeyondMachine ? "serve it over https and set APP_URL to that origin" : gateOpen === 0 ? null : "mend operator exposure"
|
|
2542
|
+
};
|
|
2543
|
+
};
|
|
2544
|
+
var LOGIN_COMMANDS = {
|
|
2545
|
+
claude: "claude setup-token",
|
|
2546
|
+
codex: "codex login",
|
|
2547
|
+
github: "gh auth login"
|
|
2548
|
+
};
|
|
2549
|
+
var HARNESS_CLIS = [
|
|
2550
|
+
{ command: "claude", provider: "claude" },
|
|
2551
|
+
{ command: "codex", provider: "codex" },
|
|
2552
|
+
{ command: "gh", provider: "github" }
|
|
2553
|
+
];
|
|
2554
|
+
var notChecked = (label) => ({
|
|
2555
|
+
label,
|
|
2556
|
+
state: "todo",
|
|
2557
|
+
detail: "not checked",
|
|
2558
|
+
fix: null
|
|
2559
|
+
});
|
|
2560
|
+
var identityOf = (account) => {
|
|
2561
|
+
const meta = account.metadata;
|
|
2562
|
+
for (const key of ["login", "email", "accountEmail", "accountId"]) {
|
|
2563
|
+
const value = meta[key];
|
|
2564
|
+
if (typeof value === "string" && value !== "") return value;
|
|
2565
|
+
}
|
|
2566
|
+
return null;
|
|
2567
|
+
};
|
|
2568
|
+
var runChecks = async (config, probes) => {
|
|
2569
|
+
const checks = [];
|
|
2570
|
+
const health = await getJson(config, "/health");
|
|
2571
|
+
checks.push(
|
|
2572
|
+
health.value === null ? {
|
|
2573
|
+
label: "server",
|
|
2574
|
+
state: "failed",
|
|
2575
|
+
detail: `cannot reach ${config.url}`,
|
|
2576
|
+
fix: "start the Mend server"
|
|
2577
|
+
} : {
|
|
2578
|
+
label: "server",
|
|
2579
|
+
state: "ok",
|
|
2580
|
+
detail: `${config.url} \xB7 mend ${health.value.version}`,
|
|
2581
|
+
fix: null
|
|
2582
|
+
}
|
|
2583
|
+
);
|
|
2584
|
+
const projects = health.value !== null && config.token !== null ? await getJson(config, "/projects") : null;
|
|
2585
|
+
if (health.value === null) checks.push(notChecked("signed in"));
|
|
2586
|
+
else if (config.token === null) {
|
|
2587
|
+
checks.push({
|
|
2588
|
+
label: "signed in",
|
|
2589
|
+
state: "failed",
|
|
2590
|
+
detail: "no token saved",
|
|
2591
|
+
fix: "mend login"
|
|
2592
|
+
});
|
|
2593
|
+
} else if (projects !== null && projects.value !== null) {
|
|
2594
|
+
checks.push({ label: "signed in", state: "ok", detail: "token accepted", fix: null });
|
|
2595
|
+
} else {
|
|
2596
|
+
const status = projects === null ? null : projects.status;
|
|
2597
|
+
checks.push(
|
|
2598
|
+
status === 401 || status === 403 ? { label: "signed in", state: "failed", detail: "token rejected", fix: "mend login" } : {
|
|
2599
|
+
label: "signed in",
|
|
2600
|
+
state: "failed",
|
|
2601
|
+
detail: `GET /projects \u2192 ${status ?? "no answer"}`,
|
|
2602
|
+
fix: null
|
|
2603
|
+
}
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
const signedIn = projects !== null && projects.value !== null;
|
|
2607
|
+
const connection = signedIn ? await getJson(config, "/sealant/connection") : null;
|
|
2608
|
+
if (connection === null) checks.push(notChecked("sealant"));
|
|
2609
|
+
else if (connection.value === null) {
|
|
2610
|
+
checks.push(
|
|
2611
|
+
connection.status === 404 ? notChecked("sealant") : {
|
|
2612
|
+
label: "sealant",
|
|
2613
|
+
state: "failed",
|
|
2614
|
+
detail: `GET /sealant/connection \u2192 ${connection.status ?? "no answer"}`,
|
|
2615
|
+
fix: null
|
|
2616
|
+
}
|
|
2617
|
+
);
|
|
2618
|
+
} else if (connection.value.status === "connected") {
|
|
2619
|
+
checks.push({
|
|
2620
|
+
label: "sealant",
|
|
2621
|
+
state: "ok",
|
|
2622
|
+
detail: `connected \xB7 ${connection.value.baseUrl}`,
|
|
2623
|
+
fix: null
|
|
2624
|
+
});
|
|
2625
|
+
} else {
|
|
2626
|
+
checks.push({
|
|
2627
|
+
label: "sealant",
|
|
2628
|
+
state: "failed",
|
|
2629
|
+
detail: `${connection.value.status} \xB7 ${connection.value.detail ?? connection.value.baseUrl}`,
|
|
2630
|
+
fix: null
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
const identity = signedIn ? await getJson(config, "/me/sealant") : null;
|
|
2634
|
+
const platform = identity === null ? null : identity.value;
|
|
2635
|
+
for (const { provider } of HARNESS_CLIS) {
|
|
2636
|
+
if (platform === null) {
|
|
2637
|
+
checks.push(notChecked(provider));
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
const account = platform.accounts.find((row) => row.provider === provider && row.name === "default") ?? platform.accounts.find((row) => row.provider === provider);
|
|
2641
|
+
if (account === void 0) {
|
|
2642
|
+
checks.push({
|
|
2643
|
+
label: provider,
|
|
2644
|
+
state: "todo",
|
|
2645
|
+
detail: "not connected",
|
|
2646
|
+
fix: `mend connect ${provider}`
|
|
2647
|
+
});
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
const who = identityOf(account);
|
|
2651
|
+
checks.push(
|
|
2652
|
+
account.status === "active" ? {
|
|
2653
|
+
label: provider,
|
|
2654
|
+
state: "ok",
|
|
2655
|
+
detail: who === null ? "connected" : `connected \xB7 ${who}`,
|
|
2656
|
+
fix: null
|
|
2657
|
+
} : {
|
|
2658
|
+
label: provider,
|
|
2659
|
+
state: "todo",
|
|
2660
|
+
detail: account.status,
|
|
2661
|
+
fix: `mend connect ${provider}`
|
|
2662
|
+
}
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
const grant = probes.claudeGrant();
|
|
2666
|
+
if (grant !== null) {
|
|
2667
|
+
const facts = claudeGrantFacts(grant);
|
|
2668
|
+
if (facts === null) {
|
|
2669
|
+
checks.push({
|
|
2670
|
+
label: "grant",
|
|
2671
|
+
state: "failed",
|
|
2672
|
+
detail: "unreadable",
|
|
2673
|
+
fix: "mend connect claude"
|
|
2674
|
+
});
|
|
2675
|
+
} else if (!facts.hasRefreshToken) {
|
|
2676
|
+
checks.push({
|
|
2677
|
+
label: "grant",
|
|
2678
|
+
state: "failed",
|
|
2679
|
+
detail: "signed out",
|
|
2680
|
+
fix: "mend connect claude"
|
|
2681
|
+
});
|
|
2682
|
+
} else if (facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now()) {
|
|
2683
|
+
checks.push({
|
|
2684
|
+
label: "grant",
|
|
2685
|
+
state: "todo",
|
|
2686
|
+
detail: `expired ${day(facts.refreshExpiresAt)}`,
|
|
2687
|
+
fix: "mend connect claude"
|
|
2688
|
+
});
|
|
2689
|
+
} else {
|
|
2690
|
+
checks.push({
|
|
2691
|
+
label: "grant",
|
|
2692
|
+
state: "ok",
|
|
2693
|
+
detail: facts.refreshExpiresAt === null ? "Mend's own" : `Mend's own \xB7 expires ${day(facts.refreshExpiresAt)}`,
|
|
2694
|
+
fix: null
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
const adopted = projects === null ? null : projects.value;
|
|
2699
|
+
if (adopted === null) checks.push(notChecked("projects"));
|
|
2700
|
+
else if (adopted.length === 0) {
|
|
2701
|
+
checks.push({ label: "projects", state: "todo", detail: "none adopted", fix: "mend adopt" });
|
|
2702
|
+
} else {
|
|
2703
|
+
checks.push({ label: "projects", state: "ok", detail: `${adopted.length} adopted`, fix: null });
|
|
2704
|
+
}
|
|
2705
|
+
for (const { command, provider } of HARNESS_CLIS) {
|
|
2706
|
+
const label = `${command} cli`;
|
|
2707
|
+
if (!probes.onPath(command)) {
|
|
2708
|
+
checks.push({ label, state: "todo", detail: "not on PATH", fix: null });
|
|
2709
|
+
continue;
|
|
2710
|
+
}
|
|
2711
|
+
const credential = probes.localCredential(provider);
|
|
2712
|
+
checks.push(
|
|
2713
|
+
credential === null ? {
|
|
2714
|
+
label,
|
|
2715
|
+
state: "todo",
|
|
2716
|
+
detail: "on PATH \xB7 no credential here",
|
|
2717
|
+
fix: LOGIN_COMMANDS[provider]
|
|
2718
|
+
} : { label, state: "ok", detail: "on PATH \xB7 credential present", fix: null }
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
const machine = signedIn ? await getJson(config, "/machine") : null;
|
|
2722
|
+
const exposure = machine === null || machine.value === null ? void 0 : machine.value.exposure;
|
|
2723
|
+
checks.push(exposure === void 0 ? notChecked("exposure") : exposureCheck(exposure));
|
|
2724
|
+
return checks;
|
|
2725
|
+
};
|
|
2726
|
+
var onPath = (command) => {
|
|
2727
|
+
for (const directory of (process.env["PATH"] ?? "").split(path5.delimiter)) {
|
|
2728
|
+
if (directory === "") continue;
|
|
2729
|
+
try {
|
|
2730
|
+
fs5.accessSync(path5.join(directory, command), fs5.constants.X_OK);
|
|
2731
|
+
return true;
|
|
2732
|
+
} catch {
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
return false;
|
|
2736
|
+
};
|
|
2737
|
+
var paintMark = (state, mark) => {
|
|
2738
|
+
if (process.stdout.isTTY !== true) return mark;
|
|
2739
|
+
const code = state === "ok" ? "32" : state === "todo" ? "33" : "31";
|
|
2740
|
+
return `\x1B[${code}m${mark}\x1B[0m`;
|
|
2741
|
+
};
|
|
2742
|
+
var doctorCommand = async (config, localCredential2, claudeGrant2) => {
|
|
2743
|
+
const checks = await runChecks(config, { localCredential: localCredential2, claudeGrant: claudeGrant2, onPath });
|
|
2744
|
+
for (const check of checks) process.stdout.write(`${formatCheck(check, paintMark)}
|
|
2745
|
+
`);
|
|
2746
|
+
if (checks.some((check) => check.state === "failed")) process.exitCode = 1;
|
|
2747
|
+
};
|
|
2748
|
+
|
|
2749
|
+
// src/dotfiles.ts
|
|
2750
|
+
import * as fs6 from "node:fs";
|
|
2751
|
+
import * as path6 from "node:path";
|
|
2752
|
+
var DOTFILE_CANDIDATES = [
|
|
2753
|
+
{
|
|
2754
|
+
group: "shell",
|
|
2755
|
+
paths: [
|
|
2756
|
+
".zshrc",
|
|
2757
|
+
".zshenv",
|
|
2758
|
+
".zprofile",
|
|
2759
|
+
".bashrc",
|
|
2760
|
+
".bash_profile",
|
|
2761
|
+
".profile",
|
|
2762
|
+
".aliases",
|
|
2763
|
+
".config/fish/config.fish",
|
|
2764
|
+
".config/starship.toml"
|
|
2765
|
+
]
|
|
2766
|
+
},
|
|
2767
|
+
{
|
|
2768
|
+
group: "git",
|
|
2769
|
+
paths: [".gitconfig", ".gitignore_global", ".config/git/config", ".config/git/ignore"]
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
group: "editors",
|
|
2773
|
+
paths: [".vimrc", ".ideavimrc", ".editorconfig", ".config/helix/config.toml"]
|
|
2774
|
+
},
|
|
2775
|
+
{
|
|
2776
|
+
group: "terminal",
|
|
2777
|
+
paths: [".tmux.conf", ".config/tmux/tmux.conf", ".inputrc", ".dir_colors"]
|
|
2778
|
+
},
|
|
2779
|
+
{
|
|
2780
|
+
group: "tools",
|
|
2781
|
+
paths: [
|
|
2782
|
+
".ripgreprc",
|
|
2783
|
+
".config/bat/config",
|
|
2784
|
+
".config/lazygit/config.yml",
|
|
2785
|
+
".config/mise/config.toml",
|
|
2786
|
+
".config/atuin/config.toml",
|
|
2787
|
+
".config/zellij/config.kdl"
|
|
2788
|
+
]
|
|
2789
|
+
}
|
|
2790
|
+
];
|
|
2791
|
+
var MAX_FILE_BYTES = 1024 * 1024;
|
|
2792
|
+
var statFile = (home, relative4) => {
|
|
2793
|
+
try {
|
|
2794
|
+
const stat = fs6.statSync(path6.join(home, relative4));
|
|
2795
|
+
return stat.isFile() ? stat : null;
|
|
2796
|
+
} catch {
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
};
|
|
2800
|
+
var scanDotfileCandidates = (home) => DOTFILE_CANDIDATES.flatMap(
|
|
2801
|
+
({ group, paths }) => paths.flatMap((relative4) => {
|
|
2802
|
+
const stat = statFile(home, relative4);
|
|
2803
|
+
return stat === null ? [] : [{ path: relative4, group, bytes: stat.size }];
|
|
2804
|
+
})
|
|
2805
|
+
);
|
|
2806
|
+
var homeRelativePath = (home, requested) => {
|
|
2807
|
+
const relative4 = path6.relative(home, path6.resolve(home, requested));
|
|
2808
|
+
const outside = relative4 === "" || relative4 === ".." || relative4.startsWith(`..${path6.sep}`) || path6.isAbsolute(relative4);
|
|
2809
|
+
return outside ? null : relative4.split(path6.sep).join("/");
|
|
2810
|
+
};
|
|
2811
|
+
var readSyncFiles = (home, paths_) => {
|
|
2812
|
+
const files = [];
|
|
2813
|
+
for (const requested of paths_) {
|
|
2814
|
+
const relative4 = homeRelativePath(home, requested);
|
|
2815
|
+
if (relative4 === null) {
|
|
2816
|
+
return {
|
|
2817
|
+
error: `${requested} is not under ${home} \u2014 only files in your home directory sync`
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
const stat = statFile(home, relative4);
|
|
2821
|
+
if (stat === null) {
|
|
2822
|
+
return { error: `${relative4} is not a file under ${home}` };
|
|
2823
|
+
}
|
|
2824
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
2825
|
+
return { error: `${relative4} is over 1MB \u2014 dotfiles are text; trim the selection` };
|
|
2826
|
+
}
|
|
2827
|
+
files.push({
|
|
2828
|
+
path: relative4,
|
|
2829
|
+
contentsBase64: fs6.readFileSync(path6.join(home, relative4)).toString("base64"),
|
|
2830
|
+
// eslint-disable-next-line no-bitwise -- permission bits are the point
|
|
2831
|
+
mode: (stat.mode & 511).toString(8)
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
return { files };
|
|
2835
|
+
};
|
|
2836
|
+
var DOTFILES_MANAGERS = ["auto", "copy", "stow", "chezmoi"];
|
|
2837
|
+
var VALUE_FLAGS = ["--ref", "--subdirectory", "--manager"];
|
|
2838
|
+
var parseDotfilesRepoArgs = (args) => {
|
|
2839
|
+
if (args.includes("--clear")) {
|
|
2840
|
+
return args.length === 1 ? { kind: "clear" } : { kind: "error", error: "--clear takes no URL or other options" };
|
|
2841
|
+
}
|
|
2842
|
+
let url = null;
|
|
2843
|
+
const values = /* @__PURE__ */ new Map();
|
|
2844
|
+
let bootstrap = true;
|
|
2845
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2846
|
+
const arg = args[index] ?? "";
|
|
2847
|
+
if (arg === "--no-bootstrap") {
|
|
2848
|
+
bootstrap = false;
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
const flag = VALUE_FLAGS.find((candidate) => candidate === arg);
|
|
2852
|
+
if (flag !== void 0) {
|
|
2853
|
+
const value = args[index + 1];
|
|
2854
|
+
if (value === void 0 || value.trim() === "" || value.startsWith("--")) {
|
|
2855
|
+
return { kind: "error", error: `${flag} needs a value` };
|
|
2856
|
+
}
|
|
2857
|
+
values.set(flag, value.trim());
|
|
2858
|
+
index += 1;
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
if (arg.startsWith("-")) return { kind: "error", error: `unknown flag ${arg}` };
|
|
2862
|
+
if (url !== null) return { kind: "error", error: "one repository URL only" };
|
|
2863
|
+
url = arg.trim();
|
|
2864
|
+
}
|
|
2865
|
+
if (url === null || url === "") {
|
|
2866
|
+
return { kind: "error", error: "name the repository URL, or --clear to remove it" };
|
|
2867
|
+
}
|
|
2868
|
+
const requested = values.get("--manager") ?? "auto";
|
|
2869
|
+
const manager = DOTFILES_MANAGERS.find((candidate) => candidate === requested);
|
|
2870
|
+
if (manager === void 0) {
|
|
2871
|
+
return { kind: "error", error: `--manager must be one of ${DOTFILES_MANAGERS.join(", ")}` };
|
|
2872
|
+
}
|
|
2873
|
+
return {
|
|
2874
|
+
kind: "set",
|
|
2875
|
+
repository: {
|
|
2876
|
+
url,
|
|
2877
|
+
ref: values.get("--ref") ?? null,
|
|
2878
|
+
subdirectory: values.get("--subdirectory") ?? null,
|
|
2879
|
+
manager,
|
|
2880
|
+
bootstrap
|
|
2881
|
+
}
|
|
2882
|
+
};
|
|
2883
|
+
};
|
|
2884
|
+
var dotfilesRepositoryFacts = (repository) => [
|
|
2885
|
+
repository.ref ?? "default branch",
|
|
2886
|
+
...repository.subdirectory === null ? [] : [`${repository.subdirectory}/`],
|
|
2887
|
+
`manager ${repository.manager}`,
|
|
2888
|
+
repository.bootstrap ? "install.sh on" : "install.sh off"
|
|
2889
|
+
].join(" \xB7 ");
|
|
2890
|
+
|
|
2891
|
+
// src/env.ts
|
|
2892
|
+
var formatLoadReport = (report, paint6) => {
|
|
2893
|
+
const width = Math.max(
|
|
2894
|
+
0,
|
|
2895
|
+
...report.loaded.map((entry) => entry.name.length),
|
|
2896
|
+
...report.rejected.map((entry) => entry.name.length)
|
|
2897
|
+
);
|
|
2898
|
+
const lines = [];
|
|
2899
|
+
for (const entry of report.loaded) {
|
|
2900
|
+
lines.push(
|
|
2901
|
+
` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint6.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
for (const entry of report.rejected) {
|
|
2905
|
+
lines.push(
|
|
2906
|
+
` ${entry.name.padEnd(width)} ${paint6.warn("rejected")} ${paint6.dim(`\xB7 ${entry.reason}`)}`
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
2909
|
+
return lines;
|
|
2910
|
+
};
|
|
2911
|
+
|
|
2912
|
+
// src/git-author.ts
|
|
2913
|
+
var parseGitAuthorArgs = (args) => {
|
|
2914
|
+
const clear = args.includes("--clear");
|
|
2915
|
+
const words = args.filter((arg) => !arg.startsWith("--"));
|
|
2916
|
+
if (clear) return words.length === 0 ? { kind: "clear" } : { kind: "usage" };
|
|
2917
|
+
if (words.length === 0) return { kind: "show" };
|
|
2918
|
+
const [name, email] = words;
|
|
2919
|
+
if (words.length !== 2 || name === void 0 || email === void 0) return { kind: "usage" };
|
|
2920
|
+
const issue = gitAuthorIssue({ name, email });
|
|
2921
|
+
if (issue !== null) return { kind: "invalid", message: `git author not saved \xB7 ${issue}` };
|
|
2922
|
+
const author = normalizeGitAuthor({ name, email });
|
|
2923
|
+
return { kind: "set", name: author.name, email: author.email };
|
|
2924
|
+
};
|
|
2925
|
+
var gitAuthorLine = (author) => `${author.name} <${author.email}> \xB7 ${author.source === "setting" ? "your setting" : "your account's name and email"}`;
|
|
1996
2926
|
|
|
1997
2927
|
// src/landing.ts
|
|
1998
2928
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1999
|
-
import * as
|
|
2000
|
-
import * as
|
|
2001
|
-
import * as
|
|
2929
|
+
import * as fs7 from "node:fs";
|
|
2930
|
+
import * as os3 from "node:os";
|
|
2931
|
+
import * as path7 from "node:path";
|
|
2002
2932
|
var paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
|
|
2003
2933
|
var dim = paint("2");
|
|
2004
2934
|
var green = paint("32");
|
|
@@ -2038,17 +2968,22 @@ var parseFlags = (args, valued, switches) => {
|
|
|
2038
2968
|
return { positional, values, on };
|
|
2039
2969
|
};
|
|
2040
2970
|
var parseLandArgs = (args) => {
|
|
2041
|
-
const flags = parseFlags(args, ["--branch", "--title", "--project"], ["--no-pr"]);
|
|
2971
|
+
const flags = parseFlags(args, ["--branch", "--title", "--project"], ["--no-pr", "--check"]);
|
|
2042
2972
|
if ("error" in flags) return flags;
|
|
2043
2973
|
const [session, extra] = flags.positional;
|
|
2044
2974
|
if (session === void 0) return { error: "name the session to land" };
|
|
2045
2975
|
if (extra !== void 0) return { error: `one session only; "${extra}" is extra` };
|
|
2976
|
+
const check = flags.on.has("--check");
|
|
2977
|
+
if (check && (flags.values.has("--branch") || flags.values.has("--title") || flags.on.has("--no-pr"))) {
|
|
2978
|
+
return { error: "--check pushes nothing; it takes no --branch, --title or --no-pr" };
|
|
2979
|
+
}
|
|
2046
2980
|
const branch = flags.values.get("--branch")?.trim() ?? null;
|
|
2047
2981
|
if (branch === "") return { error: "--branch needs a name" };
|
|
2048
2982
|
const title = flags.values.get("--title")?.trim() ?? null;
|
|
2049
2983
|
return {
|
|
2050
2984
|
args: {
|
|
2051
2985
|
session,
|
|
2986
|
+
check,
|
|
2052
2987
|
branch,
|
|
2053
2988
|
pullRequest: !flags.on.has("--no-pr"),
|
|
2054
2989
|
title: title === "" ? null : title,
|
|
@@ -2157,10 +3092,40 @@ var readFacts = (wire) => {
|
|
|
2157
3092
|
return fail("the server's landing facts are in a shape this CLI cannot read \xB7 update mend");
|
|
2158
3093
|
}
|
|
2159
3094
|
};
|
|
3095
|
+
var checkLine = (check) => {
|
|
3096
|
+
const pullRequest = check.landing?.pullRequest ?? null;
|
|
3097
|
+
switch (check.outcome) {
|
|
3098
|
+
case "adopted":
|
|
3099
|
+
return pullRequest === null ? "pull request recorded \xB7 opened outside Mend" : `pull request #${pullRequest.number} recorded \xB7 opened outside Mend \xB7 ${pullRequest.url}`;
|
|
3100
|
+
case "observed":
|
|
3101
|
+
return pullRequest === null ? "pull request already recorded" : `pull request #${pullRequest.number} already recorded \xB7 ${pullRequest.state} \xB7 observed`;
|
|
3102
|
+
case "none":
|
|
3103
|
+
return "no pull request on GitHub for the change's branches or the agent's commit";
|
|
3104
|
+
case "skipped":
|
|
3105
|
+
return `GitHub not checked \xB7 ${check.reason ?? "no reason given"}`;
|
|
3106
|
+
}
|
|
3107
|
+
};
|
|
2160
3108
|
var landCommand = async (api2, args) => {
|
|
2161
3109
|
const parsed = parseLandArgs(args);
|
|
2162
3110
|
if ("error" in parsed) return fail(parsed.error);
|
|
2163
3111
|
const { session, project: project2 } = await findSession(api2, parsed.args.session, parsed.args.project);
|
|
3112
|
+
if (parsed.args.check) {
|
|
3113
|
+
const before = await api2("GET", `/sessions/${session.id}/landings`);
|
|
3114
|
+
if (before.changeId === null) return fail("the session's worktree holds no change yet");
|
|
3115
|
+
const check = await api2(
|
|
3116
|
+
"POST",
|
|
3117
|
+
`/changes/${before.changeId}/pull-request/check`
|
|
3118
|
+
);
|
|
3119
|
+
const mark = check.outcome === "adopted" || check.outcome === "observed" ? green("\u2713") : dim("\xB7");
|
|
3120
|
+
say(`${mark} ${checkLine(check)}`);
|
|
3121
|
+
const after = await api2("GET", `/sessions/${session.id}/landings`);
|
|
3122
|
+
const facts = readFacts(after.facts);
|
|
3123
|
+
if (facts.length > 0) {
|
|
3124
|
+
say(dim(" observed"));
|
|
3125
|
+
for (const fact of facts) say(` ${landingFactLine(fact, /* @__PURE__ */ new Date())}`);
|
|
3126
|
+
}
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
2164
3129
|
const steps = parsed.args.pullRequest ? "checkpoint \xB7 commit \xB7 push \xB7 pull request" : "checkpoint \xB7 commit \xB7 push";
|
|
2165
3130
|
say(
|
|
2166
3131
|
dim(
|
|
@@ -2201,9 +3166,9 @@ var gitRemotes = (cwd) => {
|
|
|
2201
3166
|
var remoteForOrigin = (remotes, originUrl, cwd) => {
|
|
2202
3167
|
if (originUrl === null) return null;
|
|
2203
3168
|
const origin = normalizeRemoteUrl(originUrl);
|
|
2204
|
-
const originPath = origin === null ?
|
|
3169
|
+
const originPath = origin === null ? path7.resolve(originUrl.replace(/^file:\/\//, "")) : null;
|
|
2205
3170
|
return remotes.find(
|
|
2206
|
-
(remote) => origin === null ?
|
|
3171
|
+
(remote) => origin === null ? path7.resolve(cwd, remote.url.replace(/^file:\/\//, "")) === originPath : normalizeRemoteUrl(remote.url) === origin
|
|
2207
3172
|
) ?? null;
|
|
2208
3173
|
};
|
|
2209
3174
|
var LOG_LINES = 10;
|
|
@@ -2226,10 +3191,10 @@ var fetchBundle = (cwd, bundle) => {
|
|
|
2226
3191
|
const before = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
2227
3192
|
const previous = before.status === 0 ? before.stdout.trim() : null;
|
|
2228
3193
|
if (previous !== bundle.tip) {
|
|
2229
|
-
const dir =
|
|
3194
|
+
const dir = fs7.mkdtempSync(path7.join(os3.tmpdir(), "mend-pull-"));
|
|
2230
3195
|
try {
|
|
2231
|
-
const file =
|
|
2232
|
-
|
|
3196
|
+
const file = path7.join(dir, "change.bundle");
|
|
3197
|
+
fs7.writeFileSync(file, bundle.bytes, { mode: 384 });
|
|
2233
3198
|
const fetched = git(cwd, [
|
|
2234
3199
|
"fetch",
|
|
2235
3200
|
"--no-tags",
|
|
@@ -2239,7 +3204,7 @@ var fetchBundle = (cwd, bundle) => {
|
|
|
2239
3204
|
]);
|
|
2240
3205
|
if (fetched.status !== 0) return refusedWith(gitWords(fetched));
|
|
2241
3206
|
} finally {
|
|
2242
|
-
|
|
3207
|
+
fs7.rmSync(dir, { recursive: true, force: true });
|
|
2243
3208
|
}
|
|
2244
3209
|
}
|
|
2245
3210
|
const after = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).stdout.trim();
|
|
@@ -2277,7 +3242,7 @@ var fetchedLines = (fetched) => {
|
|
|
2277
3242
|
lines.push(`${dim(" switch to it")} git switch ${fetched.branch}`);
|
|
2278
3243
|
return lines;
|
|
2279
3244
|
};
|
|
2280
|
-
var
|
|
3245
|
+
var formatBytes2 = (bytes) => {
|
|
2281
3246
|
if (bytes < 1024) return `${bytes} bytes`;
|
|
2282
3247
|
const units = ["KiB", "MiB", "GiB"];
|
|
2283
3248
|
let value = bytes / 1024;
|
|
@@ -2290,13 +3255,13 @@ var formatBytes = (bytes) => {
|
|
|
2290
3255
|
};
|
|
2291
3256
|
var numberField = (value, key) => {
|
|
2292
3257
|
if (typeof value !== "object" || value === null || !(key in value)) return null;
|
|
2293
|
-
const
|
|
2294
|
-
return typeof
|
|
3258
|
+
const field2 = Reflect.get(value, key);
|
|
3259
|
+
return typeof field2 === "number" ? field2 : null;
|
|
2295
3260
|
};
|
|
2296
|
-
var
|
|
3261
|
+
var stringField2 = (value, key) => {
|
|
2297
3262
|
if (typeof value !== "object" || value === null || !(key in value)) return null;
|
|
2298
|
-
const
|
|
2299
|
-
return typeof
|
|
3263
|
+
const field2 = Reflect.get(value, key);
|
|
3264
|
+
return typeof field2 === "string" ? field2 : null;
|
|
2300
3265
|
};
|
|
2301
3266
|
var parseJson = (bytes) => {
|
|
2302
3267
|
try {
|
|
@@ -2310,9 +3275,9 @@ var bundleRefusal = (downloaded) => {
|
|
|
2310
3275
|
const size = numberField(body, "size");
|
|
2311
3276
|
const limit = numberField(body, "limit");
|
|
2312
3277
|
if (downloaded.status === 413 && size !== null && limit !== null) {
|
|
2313
|
-
return `bundle not sent \xB7 ${
|
|
3278
|
+
return `bundle not sent \xB7 ${formatBytes2(size)} (${size} bytes) \xB7 the server's limit is ${formatBytes2(limit)} (MEND_BUDGET_BUNDLE_BYTES) \xB7 nothing was fetched`;
|
|
2314
3279
|
}
|
|
2315
|
-
return
|
|
3280
|
+
return stringField2(body, "message") ?? `the bundle request answered ${downloaded.status}`;
|
|
2316
3281
|
};
|
|
2317
3282
|
var bundleFactsOf = (downloaded) => {
|
|
2318
3283
|
const branch = downloaded.header(BUNDLE_HEADERS.branch);
|
|
@@ -2357,8 +3322,8 @@ var pullCommand = async (api2, download2, args, cwd = process.cwd()) => {
|
|
|
2357
3322
|
};
|
|
2358
3323
|
|
|
2359
3324
|
// src/login.ts
|
|
2360
|
-
import { spawn } from "node:child_process";
|
|
2361
|
-
import * as
|
|
3325
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3326
|
+
import * as os4 from "node:os";
|
|
2362
3327
|
|
|
2363
3328
|
// src/pair.ts
|
|
2364
3329
|
var paint2 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
@@ -2421,23 +3386,23 @@ var qrCommand = async (args) => {
|
|
|
2421
3386
|
};
|
|
2422
3387
|
|
|
2423
3388
|
// src/login.ts
|
|
2424
|
-
var parseCliAuthStart = (
|
|
2425
|
-
if (typeof
|
|
2426
|
-
if (!("deviceCode" in
|
|
3389
|
+
var parseCliAuthStart = (json2) => {
|
|
3390
|
+
if (typeof json2 !== "object" || json2 === null) return null;
|
|
3391
|
+
if (!("deviceCode" in json2 && "code" in json2 && "verifyPath" in json2 && "expiresAt" in json2 && "intervalSeconds" in json2)) {
|
|
2427
3392
|
return null;
|
|
2428
3393
|
}
|
|
2429
|
-
const { deviceCode, code, verifyPath, expiresAt, intervalSeconds } =
|
|
3394
|
+
const { deviceCode, code, verifyPath, expiresAt, intervalSeconds } = json2;
|
|
2430
3395
|
if (typeof deviceCode !== "string" || typeof code !== "string" || typeof verifyPath !== "string" || typeof expiresAt !== "string" || typeof intervalSeconds !== "number") {
|
|
2431
3396
|
return null;
|
|
2432
3397
|
}
|
|
2433
3398
|
return { deviceCode, code, verifyPath, expiresAt, intervalSeconds };
|
|
2434
3399
|
};
|
|
2435
|
-
var parseCliAuthPoll = (
|
|
2436
|
-
if (typeof
|
|
2437
|
-
if (
|
|
2438
|
-
if (
|
|
2439
|
-
if (!("token" in
|
|
2440
|
-
const { token, user, device } =
|
|
3400
|
+
var parseCliAuthPoll = (json2) => {
|
|
3401
|
+
if (typeof json2 !== "object" || json2 === null || !("status" in json2)) return null;
|
|
3402
|
+
if (json2.status === "pending") return { status: "pending" };
|
|
3403
|
+
if (json2.status !== "approved") return null;
|
|
3404
|
+
if (!("token" in json2 && "user" in json2 && "device" in json2)) return null;
|
|
3405
|
+
const { token, user, device } = json2;
|
|
2441
3406
|
if (typeof token !== "string") return null;
|
|
2442
3407
|
if (typeof user !== "object" || user === null) return null;
|
|
2443
3408
|
if (!("id" in user && "name" in user && "email" in user)) return null;
|
|
@@ -2497,7 +3462,7 @@ var openBrowser = (url) => {
|
|
|
2497
3462
|
const opener = browserCommand(process.platform);
|
|
2498
3463
|
if (opener === null || process.stdout.isTTY !== true) return;
|
|
2499
3464
|
try {
|
|
2500
|
-
const child =
|
|
3465
|
+
const child = spawn2(opener.command, [...opener.args, url], {
|
|
2501
3466
|
stdio: "ignore",
|
|
2502
3467
|
detached: true
|
|
2503
3468
|
});
|
|
@@ -2518,21 +3483,21 @@ var post = async (url, body) => {
|
|
|
2518
3483
|
return fail3(`cannot reach the Mend server at ${new URL(url).origin} \u2014 is it running?`);
|
|
2519
3484
|
}
|
|
2520
3485
|
const text = await response.text();
|
|
2521
|
-
let
|
|
3486
|
+
let json2 = null;
|
|
2522
3487
|
try {
|
|
2523
|
-
|
|
3488
|
+
json2 = JSON.parse(text);
|
|
2524
3489
|
} catch {
|
|
2525
3490
|
}
|
|
2526
|
-
return { status: response.status, json };
|
|
3491
|
+
return { status: response.status, json: json2 };
|
|
2527
3492
|
};
|
|
2528
|
-
var retryAfterSecondsOf = (
|
|
2529
|
-
if (typeof
|
|
2530
|
-
const seconds = Number(
|
|
3493
|
+
var retryAfterSecondsOf = (json2) => {
|
|
3494
|
+
if (typeof json2 === "object" && json2 !== null && "retryAfterSeconds" in json2) {
|
|
3495
|
+
const seconds = Number(json2.retryAfterSeconds);
|
|
2531
3496
|
if (Number.isFinite(seconds) && seconds > 0) return seconds;
|
|
2532
3497
|
}
|
|
2533
3498
|
return 5;
|
|
2534
3499
|
};
|
|
2535
|
-
var sleep2 = (ms) => new Promise((
|
|
3500
|
+
var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
2536
3501
|
var resolveServerUrl = async (args, deps) => {
|
|
2537
3502
|
const given = takeFlagValue2(args, "--url") ?? deps.configuredUrl;
|
|
2538
3503
|
if (given !== null) {
|
|
@@ -2550,7 +3515,7 @@ var resolveServerUrl = async (args, deps) => {
|
|
|
2550
3515
|
};
|
|
2551
3516
|
var loginCommand = async (args, deps) => {
|
|
2552
3517
|
const base = await resolveServerUrl(args, deps);
|
|
2553
|
-
const started = await post(`${base}/api/cli/auth`, { name:
|
|
3518
|
+
const started = await post(`${base}/api/cli/auth`, { name: os4.hostname() });
|
|
2554
3519
|
if (started.status === 429) {
|
|
2555
3520
|
return fail3(
|
|
2556
3521
|
`the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
|
|
@@ -2601,8 +3566,8 @@ var loginCommand = async (args, deps) => {
|
|
|
2601
3566
|
};
|
|
2602
3567
|
|
|
2603
3568
|
// src/organization.ts
|
|
2604
|
-
import * as
|
|
2605
|
-
import * as
|
|
3569
|
+
import * as fs8 from "node:fs";
|
|
3570
|
+
import * as path8 from "node:path";
|
|
2606
3571
|
var paint4 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
|
|
2607
3572
|
var dim4 = paint4("2");
|
|
2608
3573
|
var green4 = paint4("32");
|
|
@@ -2666,7 +3631,7 @@ var scanFolder = (dir) => {
|
|
|
2666
3631
|
const files = [];
|
|
2667
3632
|
const skipped = [];
|
|
2668
3633
|
const walk = (relative4) => {
|
|
2669
|
-
const entries =
|
|
3634
|
+
const entries = fs8.readdirSync(path8.join(dir, relative4), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
2670
3635
|
for (const entry of entries) {
|
|
2671
3636
|
const child = relative4 === "" ? entry.name : `${relative4}/${entry.name}`;
|
|
2672
3637
|
if (entry.isSymbolicLink()) {
|
|
@@ -2675,7 +3640,7 @@ var scanFolder = (dir) => {
|
|
|
2675
3640
|
if (SKIPPED_DIRECTORIES.has(entry.name)) skipped.push({ path: child, reason: "skipped" });
|
|
2676
3641
|
else walk(child);
|
|
2677
3642
|
} else if (entry.isFile()) {
|
|
2678
|
-
const size =
|
|
3643
|
+
const size = fs8.statSync(path8.join(dir, child)).size;
|
|
2679
3644
|
if (size > FOLDER_FILE_BYTES) skipped.push({ path: child, reason: "over 1 MiB" });
|
|
2680
3645
|
else files.push({ path: child, size });
|
|
2681
3646
|
}
|
|
@@ -2728,7 +3693,7 @@ var folderCommand = async (api2, args) => {
|
|
|
2728
3693
|
}
|
|
2729
3694
|
case "push": {
|
|
2730
3695
|
if (dir === void 0) return fail4("usage: mend folder push <name> <dir> [--replace]");
|
|
2731
|
-
if (!
|
|
3696
|
+
if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) {
|
|
2732
3697
|
return fail4(`${dir} is not a directory`);
|
|
2733
3698
|
}
|
|
2734
3699
|
const folder = await folderNamed(api2, name);
|
|
@@ -2743,7 +3708,7 @@ var folderCommand = async (api2, args) => {
|
|
|
2743
3708
|
files: batch.map((file) => ({
|
|
2744
3709
|
path: file.path,
|
|
2745
3710
|
// Read one batch at a time, so a large tree never sits in memory at once.
|
|
2746
|
-
contentsBase64:
|
|
3711
|
+
contentsBase64: fs8.readFileSync(path8.join(dir, file.path)).toString("base64")
|
|
2747
3712
|
})),
|
|
2748
3713
|
// --replace empties the folder with the first batch; the rest add to it.
|
|
2749
3714
|
merge: !(replace && index === 0)
|
|
@@ -2927,8 +3892,8 @@ var operatorCommand = async (api2, tryApi, baseUrl, args) => {
|
|
|
2927
3892
|
import { createHmac, randomBytes } from "node:crypto";
|
|
2928
3893
|
import { readFile } from "node:fs/promises";
|
|
2929
3894
|
import * as net2 from "node:net";
|
|
2930
|
-
import * as
|
|
2931
|
-
import * as
|
|
3895
|
+
import * as os6 from "node:os";
|
|
3896
|
+
import * as path10 from "node:path";
|
|
2932
3897
|
|
|
2933
3898
|
// src/server-docker-volumes.ts
|
|
2934
3899
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -2971,14 +3936,14 @@ var docker = async (runtime, context, args) => {
|
|
|
2971
3936
|
}
|
|
2972
3937
|
};
|
|
2973
3938
|
var listNames = async (runtime, context, kind, project2) => {
|
|
2974
|
-
const
|
|
3939
|
+
const field2 = kind === "container" ? "Names" : "Name";
|
|
2975
3940
|
const output = await docker(runtime, context, [
|
|
2976
3941
|
kind,
|
|
2977
3942
|
"ls",
|
|
2978
3943
|
...kind === "container" ? ["--all"] : [],
|
|
2979
3944
|
...project2 === void 0 ? [] : ["--filter", `label=com.docker.compose.project=${project2}`],
|
|
2980
3945
|
"--format",
|
|
2981
|
-
`{{json .${
|
|
3946
|
+
`{{json .${field2}}}`
|
|
2982
3947
|
]);
|
|
2983
3948
|
if (output._tag === "error") return output;
|
|
2984
3949
|
const names = [];
|
|
@@ -3144,157 +4109,11 @@ var ownership = async (runtime, input, mode) => {
|
|
|
3144
4109
|
var claimServerDockerVolumes = (runtime, input) => ownership(runtime, input, "claim");
|
|
3145
4110
|
var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "verify");
|
|
3146
4111
|
|
|
3147
|
-
// src/server-runtime.ts
|
|
3148
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
3149
|
-
import * as fs6 from "node:fs";
|
|
3150
|
-
import * as path6 from "node:path";
|
|
3151
|
-
var serverProcessDeadlines = {
|
|
3152
|
-
ordinary: 3e4,
|
|
3153
|
-
pull: 10 * 6e4,
|
|
3154
|
-
startup: 3 * 6e4,
|
|
3155
|
-
stop: 9e4,
|
|
3156
|
-
dump: 15 * 6e4,
|
|
3157
|
-
composeWaitSeconds: 120
|
|
3158
|
-
};
|
|
3159
|
-
var runServerProcess = (command, args, parentEnvironment, options = {}) => {
|
|
3160
|
-
const timeoutMs = options?.timeoutMs ?? serverProcessDeadlines.ordinary;
|
|
3161
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
|
|
3162
|
-
return Promise.resolve({
|
|
3163
|
-
status: null,
|
|
3164
|
-
stdout: "",
|
|
3165
|
-
stderr: "",
|
|
3166
|
-
error: "Invalid process timeout"
|
|
3167
|
-
});
|
|
3168
|
-
}
|
|
3169
|
-
if (options.stdoutFile !== void 0 && options.stdout === "inherit") {
|
|
3170
|
-
return Promise.resolve({
|
|
3171
|
-
status: null,
|
|
3172
|
-
stdout: "",
|
|
3173
|
-
stderr: "",
|
|
3174
|
-
error: "Invalid process output options"
|
|
3175
|
-
});
|
|
3176
|
-
}
|
|
3177
|
-
const env = {};
|
|
3178
|
-
for (const key of [
|
|
3179
|
-
"PATH",
|
|
3180
|
-
"HOME",
|
|
3181
|
-
"USER",
|
|
3182
|
-
"LOGNAME",
|
|
3183
|
-
"XDG_CONFIG_HOME",
|
|
3184
|
-
"XDG_RUNTIME_DIR",
|
|
3185
|
-
"DOCKER_CONFIG",
|
|
3186
|
-
"SSH_AUTH_SOCK",
|
|
3187
|
-
"TMPDIR",
|
|
3188
|
-
"TMP",
|
|
3189
|
-
"TEMP"
|
|
3190
|
-
]) {
|
|
3191
|
-
if (parentEnvironment[key] !== void 0) env[key] = parentEnvironment[key];
|
|
3192
|
-
}
|
|
3193
|
-
return new Promise((resolve6) => {
|
|
3194
|
-
let fd;
|
|
3195
|
-
try {
|
|
3196
|
-
if (options.stdoutFile !== void 0) fd = fs6.openSync(options.stdoutFile, "wx", 384);
|
|
3197
|
-
const child = spawn2(command, [...args], {
|
|
3198
|
-
env,
|
|
3199
|
-
detached: process.platform !== "win32",
|
|
3200
|
-
stdio: ["ignore", fd ?? (options.stdout === "inherit" ? "inherit" : "pipe"), "pipe"]
|
|
3201
|
-
});
|
|
3202
|
-
let stdout = "";
|
|
3203
|
-
let stderr = "";
|
|
3204
|
-
let stdoutBytes = 0;
|
|
3205
|
-
let stderrBytes = 0;
|
|
3206
|
-
let failure2;
|
|
3207
|
-
const terminate = () => {
|
|
3208
|
-
if (process.platform === "win32" || child.pid === void 0) {
|
|
3209
|
-
child.kill("SIGKILL");
|
|
3210
|
-
return;
|
|
3211
|
-
}
|
|
3212
|
-
try {
|
|
3213
|
-
process.kill(-child.pid, "SIGKILL");
|
|
3214
|
-
} catch (cause) {
|
|
3215
|
-
if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) {
|
|
3216
|
-
failure2 = "Could not terminate the process group; retain the installation lock until its processes stop.";
|
|
3217
|
-
child.kill("SIGKILL");
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
};
|
|
3221
|
-
child.stdout?.on("data", (chunk) => {
|
|
3222
|
-
stdoutBytes += chunk.length;
|
|
3223
|
-
if (stdoutBytes <= 4 * 1024 * 1024) stdout += chunk.toString();
|
|
3224
|
-
else {
|
|
3225
|
-
failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
|
|
3226
|
-
terminate();
|
|
3227
|
-
}
|
|
3228
|
-
});
|
|
3229
|
-
child.stderr?.on("data", (chunk) => {
|
|
3230
|
-
stderrBytes += chunk.length;
|
|
3231
|
-
if (stderrBytes <= 64 * 1024) stderr += chunk.toString();
|
|
3232
|
-
else {
|
|
3233
|
-
failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
|
|
3234
|
-
terminate();
|
|
3235
|
-
}
|
|
3236
|
-
});
|
|
3237
|
-
const timer = setTimeout(() => {
|
|
3238
|
-
failure2 ??= `Process timed out after ${timeoutMs}ms`;
|
|
3239
|
-
terminate();
|
|
3240
|
-
}, timeoutMs);
|
|
3241
|
-
child.once("error", (error) => {
|
|
3242
|
-
failure2 ??= error.message;
|
|
3243
|
-
});
|
|
3244
|
-
child.once("close", (status) => {
|
|
3245
|
-
globalThis.clearTimeout(timer);
|
|
3246
|
-
try {
|
|
3247
|
-
if (fd !== void 0) fs6.fsyncSync(fd);
|
|
3248
|
-
} catch {
|
|
3249
|
-
failure2 ??= "Could not durably write process output.";
|
|
3250
|
-
} finally {
|
|
3251
|
-
try {
|
|
3252
|
-
if (fd !== void 0) fs6.closeSync(fd);
|
|
3253
|
-
} catch {
|
|
3254
|
-
failure2 ??= "Could not close process output file.";
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
resolve6({
|
|
3258
|
-
status: failure2 === void 0 ? status : null,
|
|
3259
|
-
stdout,
|
|
3260
|
-
stderr,
|
|
3261
|
-
...failure2 === void 0 ? {} : { error: failure2 }
|
|
3262
|
-
});
|
|
3263
|
-
});
|
|
3264
|
-
} catch (cause) {
|
|
3265
|
-
try {
|
|
3266
|
-
if (fd !== void 0) fs6.closeSync(fd);
|
|
3267
|
-
} catch {
|
|
3268
|
-
}
|
|
3269
|
-
resolve6({
|
|
3270
|
-
status: null,
|
|
3271
|
-
stdout: "",
|
|
3272
|
-
stderr: "",
|
|
3273
|
-
error: cause instanceof Error ? cause.message : "Could not start process."
|
|
3274
|
-
});
|
|
3275
|
-
}
|
|
3276
|
-
});
|
|
3277
|
-
};
|
|
3278
|
-
var serverComposeArgs = (installation, command) => [
|
|
3279
|
-
"--context",
|
|
3280
|
-
installation.dockerContext,
|
|
3281
|
-
"compose",
|
|
3282
|
-
"--project-name",
|
|
3283
|
-
"mend",
|
|
3284
|
-
"--project-directory",
|
|
3285
|
-
installation.directory,
|
|
3286
|
-
"--env-file",
|
|
3287
|
-
path6.join(installation.directory, "server.env"),
|
|
3288
|
-
"-f",
|
|
3289
|
-
path6.join(installation.directory, "compose.yaml"),
|
|
3290
|
-
...command
|
|
3291
|
-
];
|
|
3292
|
-
|
|
3293
4112
|
// src/server-store.ts
|
|
3294
4113
|
import { randomUUID } from "node:crypto";
|
|
3295
|
-
import * as
|
|
3296
|
-
import * as
|
|
3297
|
-
import * as
|
|
4114
|
+
import * as fs9 from "node:fs";
|
|
4115
|
+
import * as os5 from "node:os";
|
|
4116
|
+
import * as path9 from "node:path";
|
|
3298
4117
|
var ServerStoreError = class extends Error {
|
|
3299
4118
|
_tag = "ServerStoreError";
|
|
3300
4119
|
};
|
|
@@ -3315,28 +4134,28 @@ var attempt = (operation) => {
|
|
|
3315
4134
|
var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
|
|
3316
4135
|
var readOptional = (file) => {
|
|
3317
4136
|
try {
|
|
3318
|
-
return
|
|
4137
|
+
return fs9.readFileSync(file, "utf8");
|
|
3319
4138
|
} catch (cause) {
|
|
3320
4139
|
if (hasCode(cause, "ENOENT")) return null;
|
|
3321
4140
|
throw cause;
|
|
3322
4141
|
}
|
|
3323
4142
|
};
|
|
3324
4143
|
var syncDirectory = (directory) => {
|
|
3325
|
-
const fd =
|
|
4144
|
+
const fd = fs9.openSync(directory, "r");
|
|
3326
4145
|
try {
|
|
3327
|
-
|
|
4146
|
+
fs9.fsyncSync(fd);
|
|
3328
4147
|
} finally {
|
|
3329
|
-
|
|
4148
|
+
fs9.closeSync(fd);
|
|
3330
4149
|
}
|
|
3331
4150
|
};
|
|
3332
4151
|
var writeDurable = (file, content, mode = 384) => {
|
|
3333
|
-
const fd =
|
|
4152
|
+
const fd = fs9.openSync(file, "wx", mode);
|
|
3334
4153
|
try {
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
4154
|
+
fs9.writeFileSync(fd, content, "utf8");
|
|
4155
|
+
fs9.fchmodSync(fd, mode);
|
|
4156
|
+
fs9.fsyncSync(fd);
|
|
3338
4157
|
} finally {
|
|
3339
|
-
|
|
4158
|
+
fs9.closeSync(fd);
|
|
3340
4159
|
}
|
|
3341
4160
|
};
|
|
3342
4161
|
var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
|
|
@@ -3356,11 +4175,11 @@ var parseLockOwner = (raw) => {
|
|
|
3356
4175
|
};
|
|
3357
4176
|
var describeOwner = (lockDir) => {
|
|
3358
4177
|
try {
|
|
3359
|
-
const raw = JSON.parse(
|
|
4178
|
+
const raw = JSON.parse(fs9.readFileSync(path9.join(lockDir, "owner.json"), "utf8"));
|
|
3360
4179
|
const owner = parseLockOwner(raw);
|
|
3361
4180
|
if (owner === null) return "owner metadata is invalid";
|
|
3362
4181
|
const label = `PID ${owner.pid} on ${owner.hostname}`;
|
|
3363
|
-
if (owner.hostname !==
|
|
4182
|
+
if (owner.hostname !== os5.hostname()) return label;
|
|
3364
4183
|
try {
|
|
3365
4184
|
process.kill(owner.pid, 0);
|
|
3366
4185
|
return `${label} is still live`;
|
|
@@ -3376,28 +4195,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
|
|
|
3376
4195
|
);
|
|
3377
4196
|
var acquireLock = (configDir, create) => {
|
|
3378
4197
|
if (create) {
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
} else if (!
|
|
4198
|
+
fs9.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
4199
|
+
fs9.chmodSync(configDir, 448);
|
|
4200
|
+
} else if (!fs9.existsSync(configDir)) {
|
|
3382
4201
|
throw new ServerStoreError(
|
|
3383
4202
|
"No Mend server is configured. Run mend server setup explicitly to install one."
|
|
3384
4203
|
);
|
|
3385
4204
|
}
|
|
3386
|
-
const lockDir =
|
|
4205
|
+
const lockDir = path9.join(configDir, "server.lock");
|
|
3387
4206
|
try {
|
|
3388
|
-
|
|
4207
|
+
fs9.mkdirSync(lockDir, { mode: 448 });
|
|
3389
4208
|
} catch (cause) {
|
|
3390
4209
|
if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
|
|
3391
4210
|
throw cause;
|
|
3392
4211
|
}
|
|
3393
|
-
const owned =
|
|
3394
|
-
const ownerFile =
|
|
3395
|
-
const owner = `${JSON.stringify({ pid: process.pid, hostname:
|
|
4212
|
+
const owned = fs9.statSync(lockDir);
|
|
4213
|
+
const ownerFile = path9.join(lockDir, "owner.json");
|
|
4214
|
+
const owner = `${JSON.stringify({ pid: process.pid, hostname: os5.hostname(), token: randomUUID() })}
|
|
3396
4215
|
`;
|
|
3397
4216
|
let open = true;
|
|
3398
4217
|
const assertOwned = () => {
|
|
3399
|
-
const current =
|
|
3400
|
-
if (!open || current.dev !== owned.dev || current.ino !== owned.ino ||
|
|
4218
|
+
const current = fs9.statSync(lockDir);
|
|
4219
|
+
if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs9.readFileSync(ownerFile, "utf8") !== owner) {
|
|
3401
4220
|
throw new ServerStoreError(
|
|
3402
4221
|
"Server lock ownership was lost. Stop and check for another server command before retrying."
|
|
3403
4222
|
);
|
|
@@ -3413,8 +4232,8 @@ var acquireLock = (configDir, create) => {
|
|
|
3413
4232
|
release: () => {
|
|
3414
4233
|
const result = attempt(() => {
|
|
3415
4234
|
assertOwned();
|
|
3416
|
-
|
|
3417
|
-
|
|
4235
|
+
fs9.unlinkSync(ownerFile);
|
|
4236
|
+
fs9.rmdirSync(lockDir);
|
|
3418
4237
|
});
|
|
3419
4238
|
open = false;
|
|
3420
4239
|
return result;
|
|
@@ -3423,7 +4242,7 @@ var acquireLock = (configDir, create) => {
|
|
|
3423
4242
|
};
|
|
3424
4243
|
var readIdentity = (paths) => {
|
|
3425
4244
|
const identity = readOptional(paths.identity);
|
|
3426
|
-
if (identity === null && (
|
|
4245
|
+
if (identity === null && (fs9.existsSync(paths.active) || fs9.existsSync(paths.generations) && fs9.readdirSync(paths.generations).length > 0)) {
|
|
3427
4246
|
throw new ServerStoreError(
|
|
3428
4247
|
"Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
|
|
3429
4248
|
);
|
|
@@ -3433,7 +4252,7 @@ var readIdentity = (paths) => {
|
|
|
3433
4252
|
var readActive = (paths) => {
|
|
3434
4253
|
let target;
|
|
3435
4254
|
try {
|
|
3436
|
-
target =
|
|
4255
|
+
target = fs9.readlinkSync(paths.active);
|
|
3437
4256
|
} catch (cause) {
|
|
3438
4257
|
if (hasCode(cause, "ENOENT")) return null;
|
|
3439
4258
|
throw cause;
|
|
@@ -3443,13 +4262,13 @@ var readActive = (paths) => {
|
|
|
3443
4262
|
"Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
|
|
3444
4263
|
);
|
|
3445
4264
|
}
|
|
3446
|
-
const directory =
|
|
4265
|
+
const directory = path9.join(paths.configDir, target);
|
|
3447
4266
|
const files = {
|
|
3448
|
-
identity:
|
|
3449
|
-
config:
|
|
3450
|
-
env:
|
|
3451
|
-
compose:
|
|
3452
|
-
postgresInit:
|
|
4267
|
+
identity: fs9.readFileSync(path9.join(directory, fileNames.identity), "utf8"),
|
|
4268
|
+
config: fs9.readFileSync(path9.join(directory, fileNames.config), "utf8"),
|
|
4269
|
+
env: fs9.readFileSync(path9.join(directory, fileNames.env), "utf8"),
|
|
4270
|
+
compose: fs9.readFileSync(path9.join(directory, fileNames.compose), "utf8"),
|
|
4271
|
+
postgresInit: fs9.readFileSync(path9.join(directory, fileNames.postgresInit), "utf8")
|
|
3453
4272
|
};
|
|
3454
4273
|
if (readIdentity(paths) !== files.identity) {
|
|
3455
4274
|
throw new ServerStoreError(
|
|
@@ -3459,20 +4278,20 @@ var readActive = (paths) => {
|
|
|
3459
4278
|
return { directory, files };
|
|
3460
4279
|
};
|
|
3461
4280
|
var publishIdentity = (paths, identity) => {
|
|
3462
|
-
const temporary =
|
|
4281
|
+
const temporary = path9.join(paths.configDir, `.identity-${randomUUID()}`);
|
|
3463
4282
|
writeDurable(temporary, identity);
|
|
3464
|
-
|
|
4283
|
+
fs9.linkSync(temporary, paths.identity);
|
|
3465
4284
|
syncDirectory(paths.configDir);
|
|
3466
|
-
|
|
4285
|
+
fs9.unlinkSync(temporary);
|
|
3467
4286
|
};
|
|
3468
4287
|
var prepareGeneration = (paths, files) => {
|
|
3469
|
-
|
|
4288
|
+
fs9.mkdirSync(paths.generations, { recursive: true, mode: 448 });
|
|
3470
4289
|
const generationName = `gen-${randomUUID()}`;
|
|
3471
|
-
const directory =
|
|
3472
|
-
|
|
4290
|
+
const directory = path9.join(paths.generations, generationName);
|
|
4291
|
+
fs9.mkdirSync(directory, { mode: 448 });
|
|
3473
4292
|
for (const key of fileKeys) {
|
|
3474
4293
|
writeDurable(
|
|
3475
|
-
|
|
4294
|
+
path9.join(directory, fileNames[key]),
|
|
3476
4295
|
files[key],
|
|
3477
4296
|
key === "postgresInit" ? 493 : 384
|
|
3478
4297
|
);
|
|
@@ -3483,18 +4302,18 @@ var prepareGeneration = (paths, files) => {
|
|
|
3483
4302
|
return { directory, files };
|
|
3484
4303
|
};
|
|
3485
4304
|
var activateGeneration = (paths, generation) => {
|
|
3486
|
-
const relative4 =
|
|
3487
|
-
if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative4) || readIdentity(paths) !==
|
|
4305
|
+
const relative4 = path9.relative(paths.configDir, generation.directory);
|
|
4306
|
+
if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative4) || readIdentity(paths) !== fs9.readFileSync(path9.join(generation.directory, "identity.env"), "utf8")) {
|
|
3488
4307
|
throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
|
|
3489
4308
|
}
|
|
3490
4309
|
for (const key of fileKeys) {
|
|
3491
|
-
if (
|
|
4310
|
+
if (fs9.readFileSync(path9.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
|
|
3492
4311
|
throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
|
|
3493
4312
|
}
|
|
3494
4313
|
}
|
|
3495
|
-
const pointer =
|
|
3496
|
-
|
|
3497
|
-
|
|
4314
|
+
const pointer = path9.join(paths.configDir, `.active-${randomUUID()}`);
|
|
4315
|
+
fs9.symlinkSync(relative4, pointer);
|
|
4316
|
+
fs9.renameSync(pointer, paths.active);
|
|
3498
4317
|
syncDirectory(paths.configDir);
|
|
3499
4318
|
};
|
|
3500
4319
|
var prepareFiles = (paths, files) => {
|
|
@@ -3503,7 +4322,7 @@ var prepareFiles = (paths, files) => {
|
|
|
3503
4322
|
throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
|
|
3504
4323
|
}
|
|
3505
4324
|
const active = readActive(paths);
|
|
3506
|
-
if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (
|
|
4325
|
+
if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs9.statSync(path9.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
|
|
3507
4326
|
return active;
|
|
3508
4327
|
if (identity === null) publishIdentity(paths, files.identity);
|
|
3509
4328
|
return prepareGeneration(paths, files);
|
|
@@ -3516,9 +4335,9 @@ var commitGeneration = (paths, files) => {
|
|
|
3516
4335
|
var createStore = (configDir, lock) => {
|
|
3517
4336
|
const paths = {
|
|
3518
4337
|
configDir,
|
|
3519
|
-
identity:
|
|
3520
|
-
generations:
|
|
3521
|
-
active:
|
|
4338
|
+
identity: path9.join(configDir, "identity.env"),
|
|
4339
|
+
generations: path9.join(configDir, "generations"),
|
|
4340
|
+
active: path9.join(configDir, "active")
|
|
3522
4341
|
};
|
|
3523
4342
|
const whileOwned = (operation) => attempt(() => {
|
|
3524
4343
|
lock.assertOwned();
|
|
@@ -3531,13 +4350,13 @@ var createStore = (configDir, lock) => {
|
|
|
3531
4350
|
prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
|
|
3532
4351
|
activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
|
|
3533
4352
|
createBackup: (previous, target) => whileOwned(() => {
|
|
3534
|
-
const backups =
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
const directory =
|
|
3538
|
-
|
|
4353
|
+
const backups = path9.join(configDir, "backups");
|
|
4354
|
+
fs9.mkdirSync(backups, { recursive: true, mode: 448 });
|
|
4355
|
+
fs9.chmodSync(backups, 448);
|
|
4356
|
+
const directory = path9.join(backups, `upgrade-${randomUUID()}`);
|
|
4357
|
+
fs9.mkdirSync(directory, { mode: 448 });
|
|
3539
4358
|
writeDurable(
|
|
3540
|
-
|
|
4359
|
+
path9.join(directory, "recovery.json"),
|
|
3541
4360
|
`${JSON.stringify(
|
|
3542
4361
|
{
|
|
3543
4362
|
previousGeneration: previous.directory,
|
|
@@ -3553,21 +4372,21 @@ var createStore = (configDir, lock) => {
|
|
|
3553
4372
|
syncDirectory(directory);
|
|
3554
4373
|
syncDirectory(backups);
|
|
3555
4374
|
syncDirectory(configDir);
|
|
3556
|
-
const partialFile =
|
|
4375
|
+
const partialFile = path9.join(directory, "database.sql.partial");
|
|
3557
4376
|
return {
|
|
3558
4377
|
directory,
|
|
3559
4378
|
partialFile,
|
|
3560
4379
|
complete: () => whileOwned(() => {
|
|
3561
|
-
if (
|
|
4380
|
+
if (fs9.statSync(partialFile).size === 0)
|
|
3562
4381
|
throw new ServerStoreError("Database backup is empty; refusing target startup.");
|
|
3563
|
-
const fd =
|
|
4382
|
+
const fd = fs9.openSync(partialFile, "r");
|
|
3564
4383
|
try {
|
|
3565
|
-
|
|
3566
|
-
|
|
4384
|
+
fs9.fchmodSync(fd, 384);
|
|
4385
|
+
fs9.fsyncSync(fd);
|
|
3567
4386
|
} finally {
|
|
3568
|
-
|
|
4387
|
+
fs9.closeSync(fd);
|
|
3569
4388
|
}
|
|
3570
|
-
|
|
4389
|
+
fs9.renameSync(partialFile, path9.join(directory, "database.sql"));
|
|
3571
4390
|
syncDirectory(directory);
|
|
3572
4391
|
})
|
|
3573
4392
|
};
|
|
@@ -3576,7 +4395,7 @@ var createStore = (configDir, lock) => {
|
|
|
3576
4395
|
};
|
|
3577
4396
|
var refuseFlatLayout = (configDir) => {
|
|
3578
4397
|
if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
|
|
3579
|
-
(name) =>
|
|
4398
|
+
(name) => fs9.existsSync(path9.join(configDir, name))
|
|
3580
4399
|
)) {
|
|
3581
4400
|
throw new ServerStoreError(
|
|
3582
4401
|
"Unreleased flat server configuration found. Preserve its credentials and volumes; migrate it to the generation layout before retrying. Setup will not replace this identity."
|
|
@@ -3584,7 +4403,7 @@ var refuseFlatLayout = (configDir) => {
|
|
|
3584
4403
|
}
|
|
3585
4404
|
};
|
|
3586
4405
|
var withServerStore = async (directory, operation, options = {}) => {
|
|
3587
|
-
const configDir =
|
|
4406
|
+
const configDir = path9.resolve(directory);
|
|
3588
4407
|
let lock;
|
|
3589
4408
|
let result;
|
|
3590
4409
|
try {
|
|
@@ -3594,16 +4413,16 @@ var withServerStore = async (directory, operation, options = {}) => {
|
|
|
3594
4413
|
} catch (cause) {
|
|
3595
4414
|
result = { _tag: "error", error: storeError(cause) };
|
|
3596
4415
|
} finally {
|
|
3597
|
-
const
|
|
3598
|
-
if (
|
|
4416
|
+
const release2 = lock?.release();
|
|
4417
|
+
if (release2?._tag === "error") result = release2;
|
|
3599
4418
|
}
|
|
3600
4419
|
return result;
|
|
3601
4420
|
};
|
|
3602
4421
|
|
|
3603
4422
|
// src/version.ts
|
|
3604
|
-
import * as
|
|
4423
|
+
import * as fs10 from "node:fs";
|
|
3605
4424
|
var cliVersion = () => {
|
|
3606
|
-
const raw =
|
|
4425
|
+
const raw = fs10.readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
3607
4426
|
const parsed = JSON.parse(raw);
|
|
3608
4427
|
if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
|
|
3609
4428
|
const version = parsed.version;
|
|
@@ -3773,7 +4592,7 @@ var parseHttpOrigin = (input, label) => {
|
|
|
3773
4592
|
return parsed.origin;
|
|
3774
4593
|
};
|
|
3775
4594
|
var isLoopbackBind = (bind) => bind === "127.0.0.1" || bind === "::1" || bind.startsWith("127.");
|
|
3776
|
-
var isLoopbackHost = (
|
|
4595
|
+
var isLoopbackHost = (hostname7) => hostname7 === "localhost" || hostname7 === "::1" || hostname7 === "[::1]" || hostname7.startsWith("127.");
|
|
3777
4596
|
var resolveAppUrl = (existing, options, appPort) => {
|
|
3778
4597
|
const oldLocalUrl = existing === null ? null : `http://localhost:${existing.appPort}`;
|
|
3779
4598
|
const portChangedOnLocalhost = options.appPort !== void 0 && existing?.appUrl === oldLocalUrl;
|
|
@@ -4235,6 +5054,30 @@ var readServerInstallation = (store) => {
|
|
|
4235
5054
|
};
|
|
4236
5055
|
}
|
|
4237
5056
|
};
|
|
5057
|
+
var envKeyNames = (env) => env.split("\n").flatMap((line) => {
|
|
5058
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/.exec(line);
|
|
5059
|
+
return match?.[1] === void 0 ? [] : [match[1]];
|
|
5060
|
+
});
|
|
5061
|
+
var readServerInstallationFacts = async (configDir) => {
|
|
5062
|
+
const result = await withServerStore(
|
|
5063
|
+
configDir,
|
|
5064
|
+
async (store) => {
|
|
5065
|
+
const installation = storeValue(readServerInstallation(store));
|
|
5066
|
+
if (installation === null) return null;
|
|
5067
|
+
const generation = storeValue(store.readActive());
|
|
5068
|
+
if (generation === null) return null;
|
|
5069
|
+
return {
|
|
5070
|
+
directory: installation.directory,
|
|
5071
|
+
config: installation.config,
|
|
5072
|
+
compose: generation.files.compose,
|
|
5073
|
+
envKeys: envKeyNames(generation.files.env)
|
|
5074
|
+
};
|
|
5075
|
+
},
|
|
5076
|
+
{ create: false }
|
|
5077
|
+
);
|
|
5078
|
+
if (result._tag === "error") throw result.error;
|
|
5079
|
+
return result.value;
|
|
5080
|
+
};
|
|
4238
5081
|
var persistSetup = (store, config, secrets, assets) => storeValue(
|
|
4239
5082
|
store.prepare({
|
|
4240
5083
|
identity: renderIdentity(secrets),
|
|
@@ -4282,7 +5125,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
|
|
|
4282
5125
|
return version;
|
|
4283
5126
|
};
|
|
4284
5127
|
var parseDockerSocketPath = (socket) => {
|
|
4285
|
-
if (!
|
|
5128
|
+
if (!path10.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
|
|
4286
5129
|
throw setupError(
|
|
4287
5130
|
"--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
|
|
4288
5131
|
);
|
|
@@ -4308,8 +5151,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
|
|
|
4308
5151
|
if (options.assetsDir !== void 0) {
|
|
4309
5152
|
try {
|
|
4310
5153
|
const [compose2, postgresInit2] = await Promise.all([
|
|
4311
|
-
readFile(
|
|
4312
|
-
readFile(
|
|
5154
|
+
readFile(path10.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
|
|
5155
|
+
readFile(path10.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
|
|
4313
5156
|
]);
|
|
4314
5157
|
validateComposeAsset(compose2);
|
|
4315
5158
|
validatePostgresAsset(postgresInit2);
|
|
@@ -4822,10 +5665,10 @@ var manageServer = async (command, args, runtime, store) => {
|
|
|
4822
5665
|
};
|
|
4823
5666
|
var nodeServerRuntime = () => {
|
|
4824
5667
|
const environment = { ...process.env };
|
|
4825
|
-
const home =
|
|
4826
|
-
const configHome = environment["XDG_CONFIG_HOME"] ??
|
|
5668
|
+
const home = os6.homedir();
|
|
5669
|
+
const configHome = environment["XDG_CONFIG_HOME"] ?? path10.join(home, ".config");
|
|
4827
5670
|
return {
|
|
4828
|
-
configDir:
|
|
5671
|
+
configDir: path10.join(configHome, "mend"),
|
|
4829
5672
|
platform: process.platform,
|
|
4830
5673
|
cliVersion: cliVersion(),
|
|
4831
5674
|
run: (command, args, options) => runServerProcess(command, args, environment, options),
|
|
@@ -4842,7 +5685,7 @@ var nodeServerRuntime = () => {
|
|
|
4842
5685
|
}
|
|
4843
5686
|
},
|
|
4844
5687
|
randomBytes,
|
|
4845
|
-
sleep: (milliseconds) => new Promise((
|
|
5688
|
+
sleep: (milliseconds) => new Promise((resolve7) => setTimeout(resolve7, milliseconds)),
|
|
4846
5689
|
writeLine: (line) => process.stdout.write(`${line}
|
|
4847
5690
|
`)
|
|
4848
5691
|
};
|
|
@@ -5064,9 +5907,9 @@ var touchesSessionServices = (payload, sessionId) => {
|
|
|
5064
5907
|
return false;
|
|
5065
5908
|
}
|
|
5066
5909
|
if (typeof parsed !== "object" || parsed === null) return false;
|
|
5067
|
-
const
|
|
5910
|
+
const type2 = "type" in parsed ? parsed.type : void 0;
|
|
5068
5911
|
const session = "sessionId" in parsed ? parsed.sessionId : void 0;
|
|
5069
|
-
return (
|
|
5912
|
+
return (type2 === "session-process" || type2 === "session") && session === sessionId;
|
|
5070
5913
|
};
|
|
5071
5914
|
var pumpConnection = (socket, url) => {
|
|
5072
5915
|
socket.pause();
|
|
@@ -5092,7 +5935,7 @@ var pumpConnection = (socket, url) => {
|
|
|
5092
5935
|
() => socket.destroy()
|
|
5093
5936
|
);
|
|
5094
5937
|
};
|
|
5095
|
-
var listenLocal = (server, port, fallback) => new Promise((
|
|
5938
|
+
var listenLocal = (server, port, fallback) => new Promise((resolve7, reject) => {
|
|
5096
5939
|
const attempt2 = (candidate, lastTry) => {
|
|
5097
5940
|
const onError = (error) => {
|
|
5098
5941
|
if (!lastTry) {
|
|
@@ -5105,7 +5948,7 @@ var listenLocal = (server, port, fallback) => new Promise((resolve6, reject) =>
|
|
|
5105
5948
|
server.listen(candidate, "127.0.0.1", () => {
|
|
5106
5949
|
server.removeListener("error", onError);
|
|
5107
5950
|
const address = server.address();
|
|
5108
|
-
|
|
5951
|
+
resolve7(typeof address === "object" && address !== null ? address.port : candidate);
|
|
5109
5952
|
});
|
|
5110
5953
|
};
|
|
5111
5954
|
attempt2(port, !fallback || port === 0);
|
|
@@ -5134,7 +5977,7 @@ var serverEvents = (config) => async (handlers, signal) => {
|
|
|
5134
5977
|
if (signal.aborted) return;
|
|
5135
5978
|
}
|
|
5136
5979
|
connected = true;
|
|
5137
|
-
await new Promise((
|
|
5980
|
+
await new Promise((resolve7) => setTimeout(resolve7, 2e3));
|
|
5138
5981
|
}
|
|
5139
5982
|
};
|
|
5140
5983
|
var createServiceTunnels = (options) => {
|
|
@@ -5277,8 +6120,8 @@ var createServiceTunnels = (options) => {
|
|
|
5277
6120
|
};
|
|
5278
6121
|
|
|
5279
6122
|
// src/skills.ts
|
|
5280
|
-
import * as
|
|
5281
|
-
import * as
|
|
6123
|
+
import * as fs11 from "node:fs";
|
|
6124
|
+
import * as path11 from "node:path";
|
|
5282
6125
|
var DEFAULT_SKILLS_DIR = ".agents/skills";
|
|
5283
6126
|
var MAX_SKILL_FILES = 64;
|
|
5284
6127
|
var MAX_SKILL_FILE_BYTES = 512 * 1024;
|
|
@@ -5301,12 +6144,12 @@ var readFrontmatterDescription = (contents) => {
|
|
|
5301
6144
|
var walkFiles = (root) => {
|
|
5302
6145
|
const found = [];
|
|
5303
6146
|
const visit = (dir) => {
|
|
5304
|
-
for (const entry of
|
|
6147
|
+
for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
|
|
5305
6148
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
5306
|
-
const absolute =
|
|
6149
|
+
const absolute = path11.join(dir, entry.name);
|
|
5307
6150
|
let stat;
|
|
5308
6151
|
try {
|
|
5309
|
-
stat =
|
|
6152
|
+
stat = fs11.statSync(absolute);
|
|
5310
6153
|
} catch {
|
|
5311
6154
|
continue;
|
|
5312
6155
|
}
|
|
@@ -5320,7 +6163,7 @@ var walkFiles = (root) => {
|
|
|
5320
6163
|
var scanSkillLibrary = (root) => {
|
|
5321
6164
|
let entries;
|
|
5322
6165
|
try {
|
|
5323
|
-
entries =
|
|
6166
|
+
entries = fs11.readdirSync(root, { withFileTypes: true });
|
|
5324
6167
|
} catch {
|
|
5325
6168
|
return { error: `${root} is not a readable directory` };
|
|
5326
6169
|
}
|
|
@@ -5328,23 +6171,23 @@ var scanSkillLibrary = (root) => {
|
|
|
5328
6171
|
const notes = [];
|
|
5329
6172
|
for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
|
|
5330
6173
|
if (entry.name.startsWith(".")) continue;
|
|
5331
|
-
const skillDir =
|
|
6174
|
+
const skillDir = path11.join(root, entry.name);
|
|
5332
6175
|
let stat;
|
|
5333
6176
|
try {
|
|
5334
|
-
stat =
|
|
6177
|
+
stat = fs11.statSync(skillDir);
|
|
5335
6178
|
} catch {
|
|
5336
6179
|
continue;
|
|
5337
6180
|
}
|
|
5338
6181
|
if (!stat.isDirectory()) continue;
|
|
5339
|
-
if (!
|
|
6182
|
+
if (!fs11.existsSync(path11.join(skillDir, "SKILL.md"))) {
|
|
5340
6183
|
notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
|
|
5341
6184
|
continue;
|
|
5342
6185
|
}
|
|
5343
6186
|
const files = [];
|
|
5344
6187
|
let bytes = 0;
|
|
5345
6188
|
for (const absolute of walkFiles(skillDir)) {
|
|
5346
|
-
const relative4 =
|
|
5347
|
-
const buffer =
|
|
6189
|
+
const relative4 = path11.relative(skillDir, absolute).split(path11.sep).join("/");
|
|
6190
|
+
const buffer = fs11.readFileSync(absolute);
|
|
5348
6191
|
if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
|
|
5349
6192
|
notes.push({ skill: entry.name, message: `${relative4} is over 512KB \u2014 skipped` });
|
|
5350
6193
|
continue;
|
|
@@ -5379,16 +6222,16 @@ var scanSkillLibrary = (root) => {
|
|
|
5379
6222
|
};
|
|
5380
6223
|
|
|
5381
6224
|
// src/ssh-setup.ts
|
|
5382
|
-
import * as
|
|
5383
|
-
import * as
|
|
6225
|
+
import * as os8 from "node:os";
|
|
6226
|
+
import * as path13 from "node:path";
|
|
5384
6227
|
|
|
5385
6228
|
// ../../packages/workspace-ssh/src/index.ts
|
|
5386
6229
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5387
6230
|
import { createHash as createHash3 } from "node:crypto";
|
|
5388
|
-
import * as
|
|
6231
|
+
import * as fs12 from "node:fs";
|
|
5389
6232
|
import { isIP as isIP2 } from "node:net";
|
|
5390
|
-
import * as
|
|
5391
|
-
import * as
|
|
6233
|
+
import * as os7 from "node:os";
|
|
6234
|
+
import * as path12 from "node:path";
|
|
5392
6235
|
var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
|
|
5393
6236
|
var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
|
|
5394
6237
|
var WorkspaceSshError = class extends Error {
|
|
@@ -5405,16 +6248,16 @@ var WorkspaceSshError = class extends Error {
|
|
|
5405
6248
|
};
|
|
5406
6249
|
var success = (value) => ({ ok: true, value });
|
|
5407
6250
|
var failure = (error) => ({ ok: false, error });
|
|
5408
|
-
var stripIpv6Brackets = (
|
|
6251
|
+
var stripIpv6Brackets = (hostname7) => hostname7.startsWith("[") && hostname7.endsWith("]") ? hostname7.slice(1, -1) : hostname7;
|
|
5409
6252
|
var hashIdentity = (identity) => createHash3("sha256").update(identity).digest("hex").slice(0, 24);
|
|
5410
|
-
var aliasLabel = (
|
|
5411
|
-
const label =
|
|
6253
|
+
var aliasLabel = (hostname7) => {
|
|
6254
|
+
const label = hostname7.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
5412
6255
|
return label === "" ? "server" : label;
|
|
5413
6256
|
};
|
|
5414
6257
|
var parseHostOverride = (raw) => {
|
|
5415
|
-
const
|
|
5416
|
-
if (
|
|
5417
|
-
if (
|
|
6258
|
+
const hostname7 = raw.trim();
|
|
6259
|
+
if (hostname7 === "") return failure(new WorkspaceSshError("target", "SSH hostname is empty."));
|
|
6260
|
+
if (hostname7.includes("://") || /[\s/@]/.test(hostname7)) {
|
|
5418
6261
|
return failure(
|
|
5419
6262
|
new WorkspaceSshError(
|
|
5420
6263
|
"target",
|
|
@@ -5422,7 +6265,7 @@ var parseHostOverride = (raw) => {
|
|
|
5422
6265
|
)
|
|
5423
6266
|
);
|
|
5424
6267
|
}
|
|
5425
|
-
if (/^[^:]+:\d+$/.test(
|
|
6268
|
+
if (/^[^:]+:\d+$/.test(hostname7)) {
|
|
5426
6269
|
return failure(
|
|
5427
6270
|
new WorkspaceSshError(
|
|
5428
6271
|
"target",
|
|
@@ -5430,11 +6273,11 @@ var parseHostOverride = (raw) => {
|
|
|
5430
6273
|
)
|
|
5431
6274
|
);
|
|
5432
6275
|
}
|
|
5433
|
-
if ((
|
|
6276
|
+
if ((hostname7.startsWith("[") || hostname7.endsWith("]")) && !/^\[[^\]]+\]$/.test(hostname7)) {
|
|
5434
6277
|
return failure(new WorkspaceSshError("target", "SSH hostname contains invalid IPv6 brackets."));
|
|
5435
6278
|
}
|
|
5436
|
-
const unbracketed = stripIpv6Brackets(
|
|
5437
|
-
if ((
|
|
6279
|
+
const unbracketed = stripIpv6Brackets(hostname7);
|
|
6280
|
+
if ((hostname7.startsWith("[") || unbracketed.includes(":")) && isIP2(unbracketed) !== 6) {
|
|
5438
6281
|
return failure(
|
|
5439
6282
|
new WorkspaceSshError("target", "SSH hostname contains an invalid IPv6 address.")
|
|
5440
6283
|
);
|
|
@@ -5475,8 +6318,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
|
|
|
5475
6318
|
var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
|
|
5476
6319
|
var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
|
|
5477
6320
|
var parseIdentityPath = (value) => {
|
|
5478
|
-
const resolved =
|
|
5479
|
-
value.startsWith("~/") ?
|
|
6321
|
+
const resolved = path12.resolve(
|
|
6322
|
+
value.startsWith("~/") ? path12.join(os7.homedir(), value.slice(2)) : value
|
|
5480
6323
|
);
|
|
5481
6324
|
if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
|
|
5482
6325
|
return failure(
|
|
@@ -5632,7 +6475,7 @@ var agentKeys = () => {
|
|
|
5632
6475
|
};
|
|
5633
6476
|
var readText = (file) => {
|
|
5634
6477
|
try {
|
|
5635
|
-
return success(
|
|
6478
|
+
return success(fs12.readFileSync(file, "utf8"));
|
|
5636
6479
|
} catch (cause) {
|
|
5637
6480
|
const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
|
|
5638
6481
|
return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
|
|
@@ -5690,19 +6533,19 @@ var readRequiredKey = (keyPath, source) => {
|
|
|
5690
6533
|
var readOptionalKey = (keyPath) => {
|
|
5691
6534
|
const parsed = parseIdentityPath(keyPath);
|
|
5692
6535
|
if (!parsed.ok) return parsed;
|
|
5693
|
-
if (!
|
|
6536
|
+
if (!fs12.existsSync(parsed.value) && !fs12.existsSync(publicKeyPath(parsed.value)))
|
|
5694
6537
|
return success(null);
|
|
5695
6538
|
return readRequiredKey(parsed.value, "existing");
|
|
5696
6539
|
};
|
|
5697
6540
|
var pinAgentKey = (configHome, publicKey) => {
|
|
5698
|
-
const identityFile =
|
|
6541
|
+
const identityFile = path12.resolve(
|
|
5699
6542
|
configHome,
|
|
5700
6543
|
"ssh",
|
|
5701
6544
|
`agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
|
|
5702
6545
|
);
|
|
5703
6546
|
try {
|
|
5704
|
-
|
|
5705
|
-
|
|
6547
|
+
fs12.mkdirSync(path12.dirname(identityFile), { recursive: true, mode: 448 });
|
|
6548
|
+
fs12.writeFileSync(identityFile, `${publicKey}
|
|
5706
6549
|
`, { mode: 384 });
|
|
5707
6550
|
return success({ publicKey, identityFile, source: "agent" });
|
|
5708
6551
|
} catch (cause) {
|
|
@@ -5713,15 +6556,15 @@ var pinAgentKey = (configHome, publicKey) => {
|
|
|
5713
6556
|
};
|
|
5714
6557
|
var generateDedicatedKey = (privatePath) => {
|
|
5715
6558
|
try {
|
|
5716
|
-
|
|
6559
|
+
fs12.mkdirSync(path12.dirname(privatePath), { recursive: true, mode: 448 });
|
|
5717
6560
|
} catch (cause) {
|
|
5718
6561
|
return failure(
|
|
5719
|
-
new WorkspaceSshError("key", `Could not create ${
|
|
6562
|
+
new WorkspaceSshError("key", `Could not create ${path12.dirname(privatePath)}.`, cause)
|
|
5720
6563
|
);
|
|
5721
6564
|
}
|
|
5722
6565
|
const generated = spawnSync3(
|
|
5723
6566
|
"ssh-keygen",
|
|
5724
|
-
["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${
|
|
6567
|
+
["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os7.hostname()}`],
|
|
5725
6568
|
{ encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
|
|
5726
6569
|
);
|
|
5727
6570
|
if (generated.status !== 0) {
|
|
@@ -5745,7 +6588,7 @@ var generateDedicatedKey = (privatePath) => {
|
|
|
5745
6588
|
return fresh;
|
|
5746
6589
|
};
|
|
5747
6590
|
var dedicatedKey = (configHome, create) => {
|
|
5748
|
-
const privatePath =
|
|
6591
|
+
const privatePath = path12.join(configHome, "ssh", "id_ed25519");
|
|
5749
6592
|
const existing = readOptionalKey(privatePath);
|
|
5750
6593
|
if (existing.ok === false) return failure(existing.error);
|
|
5751
6594
|
if (existing.value !== null || !create) return existing;
|
|
@@ -5793,10 +6636,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
|
|
|
5793
6636
|
const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
|
|
5794
6637
|
if (reconciled.ok === false) return failure(reconciled.error);
|
|
5795
6638
|
try {
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
6639
|
+
fs12.mkdirSync(path12.dirname(configFile), { recursive: true, mode: 448 });
|
|
6640
|
+
fs12.chmodSync(path12.dirname(configFile), 448);
|
|
6641
|
+
fs12.writeFileSync(configFile, reconciled.value, { mode: 384 });
|
|
6642
|
+
fs12.chmodSync(configFile, 384);
|
|
5800
6643
|
return success(void 0);
|
|
5801
6644
|
} catch (cause) {
|
|
5802
6645
|
return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
|
|
@@ -5809,7 +6652,7 @@ var dim5 = ansi("2");
|
|
|
5809
6652
|
var green5 = ansi("32");
|
|
5810
6653
|
var warn = ansi("33");
|
|
5811
6654
|
var say5 = (line) => console.log(line);
|
|
5812
|
-
var sshConfigPath = () =>
|
|
6655
|
+
var sshConfigPath = () => path13.join(os8.homedir(), ".ssh", "config");
|
|
5813
6656
|
var flagValue = (args, flag) => {
|
|
5814
6657
|
const index = args.indexOf(flag);
|
|
5815
6658
|
return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
|
|
@@ -5879,7 +6722,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
|
|
|
5879
6722
|
if (picked.value === null) return showFailure("No workspace SSH key is available.");
|
|
5880
6723
|
const registered = await api2("POST", "/workspace-ssh/keys", {
|
|
5881
6724
|
publicKey: picked.value.publicKey,
|
|
5882
|
-
name:
|
|
6725
|
+
name: os8.hostname()
|
|
5883
6726
|
});
|
|
5884
6727
|
const sourceLabel = {
|
|
5885
6728
|
explicit: "from --key",
|
|
@@ -5921,8 +6764,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
|
|
|
5921
6764
|
};
|
|
5922
6765
|
|
|
5923
6766
|
// src/uninstall.ts
|
|
5924
|
-
import * as
|
|
5925
|
-
import * as
|
|
6767
|
+
import * as fs13 from "node:fs";
|
|
6768
|
+
import * as path14 from "node:path";
|
|
5926
6769
|
var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
|
|
5927
6770
|
var parseUninstallArgs = (args) => {
|
|
5928
6771
|
let scope = null;
|
|
@@ -5944,14 +6787,14 @@ var parseUninstallArgs = (args) => {
|
|
|
5944
6787
|
var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
|
|
5945
6788
|
var countEntries = (directory) => {
|
|
5946
6789
|
try {
|
|
5947
|
-
return
|
|
6790
|
+
return fs13.readdirSync(directory).length;
|
|
5948
6791
|
} catch {
|
|
5949
6792
|
return 0;
|
|
5950
6793
|
}
|
|
5951
6794
|
};
|
|
5952
6795
|
var exists = (file) => {
|
|
5953
6796
|
try {
|
|
5954
|
-
|
|
6797
|
+
fs13.lstatSync(file);
|
|
5955
6798
|
return true;
|
|
5956
6799
|
} catch {
|
|
5957
6800
|
return false;
|
|
@@ -5960,7 +6803,7 @@ var exists = (file) => {
|
|
|
5960
6803
|
var managedBlockCount = (sshConfigFile) => {
|
|
5961
6804
|
let config;
|
|
5962
6805
|
try {
|
|
5963
|
-
config =
|
|
6806
|
+
config = fs13.readFileSync(sshConfigFile, "utf8");
|
|
5964
6807
|
} catch {
|
|
5965
6808
|
return 0;
|
|
5966
6809
|
}
|
|
@@ -5984,27 +6827,27 @@ var describeUninstall = async (runtime, scope) => {
|
|
|
5984
6827
|
if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
|
|
5985
6828
|
server = "none";
|
|
5986
6829
|
} else if (read.value === null) {
|
|
5987
|
-
server = exists(
|
|
6830
|
+
server = exists(path14.join(configDir, "identity.env")) ? {
|
|
5988
6831
|
version: "(no active generation)",
|
|
5989
6832
|
appUrl: "",
|
|
5990
6833
|
dockerContext: "",
|
|
5991
|
-
generations: countEntries(
|
|
5992
|
-
backups: countEntries(
|
|
6834
|
+
generations: countEntries(path14.join(configDir, "generations")),
|
|
6835
|
+
backups: countEntries(path14.join(configDir, "backups"))
|
|
5993
6836
|
} : "none";
|
|
5994
6837
|
} else {
|
|
5995
6838
|
server = {
|
|
5996
6839
|
version: read.value.config.serverVersion,
|
|
5997
6840
|
appUrl: read.value.config.appUrl,
|
|
5998
6841
|
dockerContext: read.value.config.dockerContext,
|
|
5999
|
-
generations: countEntries(
|
|
6000
|
-
backups: countEntries(
|
|
6842
|
+
generations: countEntries(path14.join(configDir, "generations")),
|
|
6843
|
+
backups: countEntries(path14.join(configDir, "backups"))
|
|
6001
6844
|
};
|
|
6002
6845
|
}
|
|
6003
6846
|
}
|
|
6004
6847
|
let home = null;
|
|
6005
6848
|
if (scope !== "server") {
|
|
6006
|
-
const cliConfig =
|
|
6007
|
-
const sshDirectory =
|
|
6849
|
+
const cliConfig = path14.join(runtime.cliHome, "cli.json");
|
|
6850
|
+
const sshDirectory = path14.join(runtime.cliHome, "ssh");
|
|
6008
6851
|
home = {
|
|
6009
6852
|
cliConfig: exists(cliConfig) ? cliConfig : null,
|
|
6010
6853
|
sshDirectory: exists(sshDirectory) ? sshDirectory : null,
|
|
@@ -6136,7 +6979,7 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
6136
6979
|
}
|
|
6137
6980
|
}
|
|
6138
6981
|
for (const name of SERVER_FILES) {
|
|
6139
|
-
|
|
6982
|
+
fs13.rmSync(path14.join(configDir, name), { recursive: true, force: true });
|
|
6140
6983
|
}
|
|
6141
6984
|
server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
|
|
6142
6985
|
},
|
|
@@ -6147,20 +6990,20 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
6147
6990
|
var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
6148
6991
|
const { server } = runtime;
|
|
6149
6992
|
if (plan.cliConfig !== null) {
|
|
6150
|
-
|
|
6993
|
+
fs13.rmSync(plan.cliConfig, { force: true });
|
|
6151
6994
|
server.writeLine(`removed ${plan.cliConfig}`);
|
|
6152
6995
|
}
|
|
6153
6996
|
if (plan.sshDirectory !== null) {
|
|
6154
|
-
|
|
6997
|
+
fs13.rmSync(plan.sshDirectory, { recursive: true, force: true });
|
|
6155
6998
|
server.writeLine(`removed ${plan.sshDirectory}`);
|
|
6156
6999
|
}
|
|
6157
7000
|
if (plan.managedSshBlocks > 0) {
|
|
6158
7001
|
try {
|
|
6159
|
-
const before =
|
|
7002
|
+
const before = fs13.readFileSync(runtime.sshConfigFile, "utf8");
|
|
6160
7003
|
const stripped = stripManagedWorkspaceSshBlocks(before);
|
|
6161
7004
|
if (!stripped.ok) throw stripped.error;
|
|
6162
7005
|
if (stripped.value.removed > 0) {
|
|
6163
|
-
|
|
7006
|
+
fs13.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
|
|
6164
7007
|
server.writeLine(
|
|
6165
7008
|
`removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
|
|
6166
7009
|
);
|
|
@@ -6173,12 +7016,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
|
6173
7016
|
}
|
|
6174
7017
|
let remaining = [];
|
|
6175
7018
|
try {
|
|
6176
|
-
remaining =
|
|
7019
|
+
remaining = fs13.readdirSync(runtime.cliHome);
|
|
6177
7020
|
} catch {
|
|
6178
7021
|
return;
|
|
6179
7022
|
}
|
|
6180
7023
|
if (remaining.length === 0) {
|
|
6181
|
-
|
|
7024
|
+
fs13.rmdirSync(runtime.cliHome);
|
|
6182
7025
|
server.writeLine(`removed ${runtime.cliHome}`);
|
|
6183
7026
|
} else {
|
|
6184
7027
|
leftovers.push(
|
|
@@ -6242,19 +7085,19 @@ var upgradeUrl = async (input) => {
|
|
|
6242
7085
|
// src/main.ts
|
|
6243
7086
|
var mendCliHome = () => {
|
|
6244
7087
|
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
6245
|
-
const preferred =
|
|
6246
|
-
xdg === void 0 || xdg === "" ?
|
|
7088
|
+
const preferred = path15.join(
|
|
7089
|
+
xdg === void 0 || xdg === "" ? path15.join(os9.homedir(), ".config") : xdg,
|
|
6247
7090
|
"mend"
|
|
6248
7091
|
);
|
|
6249
|
-
const legacy =
|
|
6250
|
-
return !
|
|
7092
|
+
const legacy = path15.join(os9.homedir(), ".mend");
|
|
7093
|
+
return !fs14.existsSync(preferred) && fs14.existsSync(legacy) ? legacy : preferred;
|
|
6251
7094
|
};
|
|
6252
|
-
var CONFIG_PATH =
|
|
7095
|
+
var CONFIG_PATH = path15.join(mendCliHome(), "cli.json");
|
|
6253
7096
|
var loadConfig = () => {
|
|
6254
7097
|
let fileConfig = {};
|
|
6255
|
-
if (
|
|
7098
|
+
if (fs14.existsSync(CONFIG_PATH)) {
|
|
6256
7099
|
try {
|
|
6257
|
-
fileConfig = JSON.parse(
|
|
7100
|
+
fileConfig = JSON.parse(fs14.readFileSync(CONFIG_PATH, "utf8"));
|
|
6258
7101
|
} catch {
|
|
6259
7102
|
fail6(`could not parse ${CONFIG_PATH}`);
|
|
6260
7103
|
}
|
|
@@ -6445,7 +7288,7 @@ var adopt = async (config, args) => {
|
|
|
6445
7288
|
}
|
|
6446
7289
|
const sourceIssue = repositoryCloneUrlIssue(source);
|
|
6447
7290
|
if (sourceIssue !== null) return fail6(sourceIssue);
|
|
6448
|
-
const projectName = name ?? normalizeProjectName(
|
|
7291
|
+
const projectName = name ?? normalizeProjectName(path15.basename(source, ".git"));
|
|
6449
7292
|
const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
|
|
6450
7293
|
if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
|
|
6451
7294
|
return fail6(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
|
|
@@ -6494,7 +7337,7 @@ var findProject = async (config, explicit, adoptCwd = false) => {
|
|
|
6494
7337
|
const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
|
|
6495
7338
|
if (sourceIssue !== null) return fail6(sourceIssue);
|
|
6496
7339
|
const adopted = await api(config, "POST", "/projects", {
|
|
6497
|
-
name: normalizeProjectName(
|
|
7340
|
+
name: normalizeProjectName(path15.basename(facts.repoRoot)),
|
|
6498
7341
|
source: facts.originUrl
|
|
6499
7342
|
});
|
|
6500
7343
|
say6(`${green6("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim6(adopted.storePath)}`);
|
|
@@ -6661,8 +7504,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
6661
7504
|
const ws = new WebSocket(url);
|
|
6662
7505
|
ws.binaryType = "arraybuffer";
|
|
6663
7506
|
try {
|
|
6664
|
-
await new Promise((
|
|
6665
|
-
ws.addEventListener("open", () =>
|
|
7507
|
+
await new Promise((resolve7, reject) => {
|
|
7508
|
+
ws.addEventListener("open", () => resolve7(), { once: true });
|
|
6666
7509
|
ws.addEventListener("error", () => reject(new Error("could not connect")), { once: true });
|
|
6667
7510
|
});
|
|
6668
7511
|
} catch {
|
|
@@ -6670,8 +7513,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
6670
7513
|
}
|
|
6671
7514
|
const stopHerdrHint = hintHerdrAttachment(harness);
|
|
6672
7515
|
let finishAttachment;
|
|
6673
|
-
const finished = new Promise((
|
|
6674
|
-
finishAttachment =
|
|
7516
|
+
const finished = new Promise((resolve7) => {
|
|
7517
|
+
finishAttachment = resolve7;
|
|
6675
7518
|
});
|
|
6676
7519
|
const onClose = () => finishAttachment?.();
|
|
6677
7520
|
if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
|
|
@@ -6899,6 +7742,12 @@ var attachPicked = async (config, session, tunnels) => {
|
|
|
6899
7742
|
return exitAfterSessionEnd(config, session.id);
|
|
6900
7743
|
};
|
|
6901
7744
|
var stopCommand = async (config, args) => {
|
|
7745
|
+
if (args.includes("--services")) {
|
|
7746
|
+
const prefix2 = args.find((arg) => !arg.startsWith("--"));
|
|
7747
|
+
const session = await resolveLiveSession(config, prefix2, "stop --services");
|
|
7748
|
+
await stopServicesOf(config, session);
|
|
7749
|
+
return;
|
|
7750
|
+
}
|
|
6902
7751
|
const all = args.includes("--all");
|
|
6903
7752
|
const projectFlag = args.indexOf("--project");
|
|
6904
7753
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
|
|
@@ -6944,12 +7793,33 @@ var stopSessions = async (config, targets, summarise) => {
|
|
|
6944
7793
|
say6(
|
|
6945
7794
|
`${green6("\u2713")} stopped \xB7 ${session.harness} \xB7 ${dim6(session.id.slice(0, 8))} \xB7 ${session.branch}`
|
|
6946
7795
|
);
|
|
7796
|
+
const after = await api(config, "GET", `/sessions/${session.id}`).catch(
|
|
7797
|
+
() => null
|
|
7798
|
+
);
|
|
7799
|
+
const hold = after === null ? null : servicesHoldOf(after.session, after.currentAgent, after.liveServices ?? 0);
|
|
7800
|
+
if (hold !== null) {
|
|
7801
|
+
say6(`${amber2(" " + hold)} \xB7 mend stop --services ${session.id.slice(0, 8)}`);
|
|
7802
|
+
}
|
|
6947
7803
|
say6(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
6948
7804
|
}
|
|
6949
7805
|
if (summarise) {
|
|
6950
7806
|
say6(`${green6("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
|
|
6951
7807
|
}
|
|
6952
7808
|
};
|
|
7809
|
+
var stopServicesOf = async (config, session) => {
|
|
7810
|
+
const result = await api(
|
|
7811
|
+
config,
|
|
7812
|
+
"POST",
|
|
7813
|
+
`/sessions/${session.id}/services/stop`
|
|
7814
|
+
);
|
|
7815
|
+
if (result.stopped === 0) {
|
|
7816
|
+
say6(`no live services \xB7 ${session.harness} \xB7 ${dim6(session.id.slice(0, 8))}`);
|
|
7817
|
+
return;
|
|
7818
|
+
}
|
|
7819
|
+
say6(
|
|
7820
|
+
`${green6("\u2713")} stopped ${result.stopped} service${result.stopped === 1 ? "" : "s"} \xB7 ${session.harness} \xB7 ${dim6(session.id.slice(0, 8))} \xB7 ${session.branch}`
|
|
7821
|
+
);
|
|
7822
|
+
};
|
|
6953
7823
|
var pickSessionInteractively = async (rows) => {
|
|
6954
7824
|
say6(dim6("more than one live session \u2014 pick one:"));
|
|
6955
7825
|
rows.forEach((row, index) => {
|
|
@@ -7181,11 +8051,11 @@ var autoConnect = async (config, service) => {
|
|
|
7181
8051
|
};
|
|
7182
8052
|
var serviceRun = async (config, args) => {
|
|
7183
8053
|
const dashdash = args.indexOf("--");
|
|
7184
|
-
const
|
|
8054
|
+
const usage2 = usageOf("service run");
|
|
7185
8055
|
if (dashdash === -1) {
|
|
7186
8056
|
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
7187
8057
|
const name2 = positionals.at(-1);
|
|
7188
|
-
if (name2 === void 0) return fail6(
|
|
8058
|
+
if (name2 === void 0) return fail6(usage2);
|
|
7189
8059
|
const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
|
|
7190
8060
|
const session2 = await resolveLiveSession(config, prefix2, "service run");
|
|
7191
8061
|
const recipes = await api(
|
|
@@ -7214,19 +8084,19 @@ var serviceRun = async (config, args) => {
|
|
|
7214
8084
|
return;
|
|
7215
8085
|
}
|
|
7216
8086
|
const argv = args.slice(dashdash + 1);
|
|
7217
|
-
if (argv.length === 0) return fail6(
|
|
8087
|
+
if (argv.length === 0) return fail6(usage2);
|
|
7218
8088
|
const head = args.slice(0, dashdash);
|
|
7219
8089
|
const portFlag = head.indexOf("--port");
|
|
7220
8090
|
const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
|
|
7221
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) return fail6(
|
|
8091
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return fail6(usage2);
|
|
7222
8092
|
const nameFlag = head.indexOf("--name");
|
|
7223
8093
|
const name = nameFlag !== -1 && head[nameFlag + 1] !== void 0 ? String(head[nameFlag + 1]) : null;
|
|
7224
8094
|
const protocol = head.includes("--udp") ? "udp" : "tcp";
|
|
7225
8095
|
const http = head.includes("--http");
|
|
7226
8096
|
const https = head.includes("--https");
|
|
7227
|
-
if (http && https) return fail6(
|
|
8097
|
+
if (http && https) return fail6(usage2);
|
|
7228
8098
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
7229
|
-
if (protocol === "udp" && browserScheme !== null) return fail6(
|
|
8099
|
+
if (protocol === "udp" && browserScheme !== null) return fail6(usage2);
|
|
7230
8100
|
const prefix = head.find(
|
|
7231
8101
|
(a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
|
|
7232
8102
|
);
|
|
@@ -7299,7 +8169,7 @@ var serviceLogs = async (config, args) => {
|
|
|
7299
8169
|
cursor = page.nextFrom;
|
|
7300
8170
|
const ended = page.status === "exited" || page.status === "failed";
|
|
7301
8171
|
if (ended && !advanced) break;
|
|
7302
|
-
if (!advanced) await new Promise((
|
|
8172
|
+
if (!advanced) await new Promise((resolve7) => setTimeout(resolve7, 250));
|
|
7303
8173
|
}
|
|
7304
8174
|
say6("");
|
|
7305
8175
|
say6(dim6(`stream ended \xB7 next sequence ${cursor}`));
|
|
@@ -7317,13 +8187,13 @@ var serviceRestart = async (config, args) => {
|
|
|
7317
8187
|
};
|
|
7318
8188
|
var serviceInit = async (args) => {
|
|
7319
8189
|
const root = gitTopLevel(process.cwd()) ?? process.cwd();
|
|
7320
|
-
const target =
|
|
7321
|
-
if (
|
|
8190
|
+
const target = path15.join(root, "mend.toml");
|
|
8191
|
+
if (fs14.existsSync(target)) {
|
|
7322
8192
|
return fail6(`${target} already exists \u2014 edit it directly (init never merges)`);
|
|
7323
8193
|
}
|
|
7324
|
-
const rootFiles =
|
|
8194
|
+
const rootFiles = fs14.readdirSync(root);
|
|
7325
8195
|
const proposals = [];
|
|
7326
|
-
const readRoot = (name) =>
|
|
8196
|
+
const readRoot = (name) => fs14.readFileSync(path15.join(root, name), "utf8");
|
|
7327
8197
|
if (rootFiles.includes("package.json")) {
|
|
7328
8198
|
proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
|
|
7329
8199
|
}
|
|
@@ -7333,13 +8203,13 @@ var serviceInit = async (args) => {
|
|
|
7333
8203
|
);
|
|
7334
8204
|
for (const glob of globs) {
|
|
7335
8205
|
const parent = glob.replace(/\/?\*+$/, "");
|
|
7336
|
-
const dirs = glob.endsWith("*") ?
|
|
8206
|
+
const dirs = glob.endsWith("*") ? fs14.existsSync(path15.join(root, parent)) ? fs14.readdirSync(path15.join(root, parent)).map((dir) => path15.join(parent, dir)) : [] : [glob];
|
|
7337
8207
|
for (const dir of dirs) {
|
|
7338
|
-
const manifest =
|
|
7339
|
-
if (!
|
|
8208
|
+
const manifest = path15.join(root, dir, "package.json");
|
|
8209
|
+
if (!fs14.existsSync(manifest)) continue;
|
|
7340
8210
|
for (const proposal of proposeFromWorkspacePackage(
|
|
7341
|
-
|
|
7342
|
-
|
|
8211
|
+
path15.basename(dir),
|
|
8212
|
+
fs14.readFileSync(manifest, "utf8"),
|
|
7343
8213
|
rootFiles
|
|
7344
8214
|
)) {
|
|
7345
8215
|
if (!proposals.some((existing) => existing.name === proposal.name)) {
|
|
@@ -7383,7 +8253,7 @@ var serviceInit = async (args) => {
|
|
|
7383
8253
|
return;
|
|
7384
8254
|
}
|
|
7385
8255
|
}
|
|
7386
|
-
|
|
8256
|
+
fs14.writeFileSync(target, toml);
|
|
7387
8257
|
say6(`${green6("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
|
|
7388
8258
|
};
|
|
7389
8259
|
var tunnelServices = async (config, services, portOverride) => {
|
|
@@ -7431,7 +8301,7 @@ var attachTunnels = (config, optOut) => {
|
|
|
7431
8301
|
start: async (sessionId) => {
|
|
7432
8302
|
await Promise.race([
|
|
7433
8303
|
tunnels.focus(sessionId),
|
|
7434
|
-
new Promise((
|
|
8304
|
+
new Promise((resolve7) => setTimeout(resolve7, 3e3))
|
|
7435
8305
|
]);
|
|
7436
8306
|
},
|
|
7437
8307
|
attached: (work) => {
|
|
@@ -7487,18 +8357,18 @@ var serviceCommand = async (config, args) => {
|
|
|
7487
8357
|
}
|
|
7488
8358
|
};
|
|
7489
8359
|
var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
|
|
7490
|
-
var readIfExists = (file) =>
|
|
8360
|
+
var readIfExists = (file) => fs14.existsSync(file) ? fs14.readFileSync(file, "utf8") : null;
|
|
7491
8361
|
var localCredential = (provider) => {
|
|
7492
|
-
const home =
|
|
8362
|
+
const home = os9.homedir();
|
|
7493
8363
|
switch (provider) {
|
|
7494
8364
|
case "codex":
|
|
7495
8365
|
return readIfExists(
|
|
7496
|
-
|
|
8366
|
+
path15.join(process.env["CODEX_HOME"] ?? path15.join(home, ".codex"), "auth.json")
|
|
7497
8367
|
);
|
|
7498
8368
|
case "claude":
|
|
7499
8369
|
return readIfExists(
|
|
7500
|
-
|
|
7501
|
-
process.env["CLAUDE_CONFIG_DIR"] ??
|
|
8370
|
+
path15.join(
|
|
8371
|
+
process.env["CLAUDE_CONFIG_DIR"] ?? path15.join(home, ".claude"),
|
|
7502
8372
|
".credentials.json"
|
|
7503
8373
|
)
|
|
7504
8374
|
);
|
|
@@ -7522,19 +8392,64 @@ var accountLine = (account) => {
|
|
|
7522
8392
|
].filter((fact) => fact !== null);
|
|
7523
8393
|
return `${account.provider.padEnd(8)} ${facts.join(" \xB7 ")}`;
|
|
7524
8394
|
};
|
|
7525
|
-
var
|
|
7526
|
-
const identity = await
|
|
7527
|
-
process.stdout.write(`platform user ${identity.sealantUserId}
|
|
7528
|
-
`);
|
|
8395
|
+
var accountsLines = async (config) => {
|
|
8396
|
+
const identity = await request(config, "GET", "/me/sealant");
|
|
7529
8397
|
const providers = ["claude", "codex", "github"];
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
`
|
|
7535
|
-
)
|
|
8398
|
+
return [
|
|
8399
|
+
`platform user ${identity.sealantUserId}`,
|
|
8400
|
+
...providers.map((provider) => {
|
|
8401
|
+
const account = identity.accounts.find((row) => row.provider === provider && row.name === "default") ?? identity.accounts.find((row) => row.provider === provider);
|
|
8402
|
+
return ` ${account === void 0 ? `${provider.padEnd(8)} not connected` : accountLine(account)}`;
|
|
8403
|
+
})
|
|
8404
|
+
];
|
|
8405
|
+
};
|
|
8406
|
+
var accountsCommand = async (config) => {
|
|
8407
|
+
let lines;
|
|
8408
|
+
try {
|
|
8409
|
+
lines = await accountsLines(config);
|
|
8410
|
+
} catch (error) {
|
|
8411
|
+
return fail6(error instanceof Error ? error.message : String(error));
|
|
7536
8412
|
}
|
|
8413
|
+
for (const line of lines) process.stdout.write(`${line}
|
|
8414
|
+
`);
|
|
7537
8415
|
};
|
|
8416
|
+
var claudeGrantSecret = () => {
|
|
8417
|
+
const read = readGrant(claudeGrantDir(mendCliHome()));
|
|
8418
|
+
return read.kind === "missing" ? null : read.secret;
|
|
8419
|
+
};
|
|
8420
|
+
var doctorBundle = (config, args) => doctorBundleCommand(args, {
|
|
8421
|
+
defaultDir: path15.join(mendCliHome(), "bundles"),
|
|
8422
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
8423
|
+
say: say6,
|
|
8424
|
+
warn: (line) => process.stderr.write(`${line}
|
|
8425
|
+
`),
|
|
8426
|
+
collectors: (tail) => bundleCollectors({
|
|
8427
|
+
cliVersion: cliVersion(),
|
|
8428
|
+
serverUrl: config.url,
|
|
8429
|
+
configuredUrl: config.configuredUrl,
|
|
8430
|
+
deviceId: config.deviceId,
|
|
8431
|
+
tokenSaved: config.token !== null,
|
|
8432
|
+
env: process.env,
|
|
8433
|
+
stdinTty: process.stdin.isTTY === true,
|
|
8434
|
+
stdoutTty: process.stdout.isTTY === true,
|
|
8435
|
+
get: (route) => request(config, "GET", route),
|
|
8436
|
+
doctor: async () => {
|
|
8437
|
+
const checks = await runChecks(config, {
|
|
8438
|
+
localCredential,
|
|
8439
|
+
claudeGrant: claudeGrantSecret,
|
|
8440
|
+
onPath
|
|
8441
|
+
});
|
|
8442
|
+
return `${checks.map((check) => formatCheck(check)).join("\n")}
|
|
8443
|
+
`;
|
|
8444
|
+
},
|
|
8445
|
+
accounts: async () => `${(await accountsLines(config)).join("\n")}
|
|
8446
|
+
`,
|
|
8447
|
+
run: (command, commandArgs, timeoutMs) => runServerProcess(command, commandArgs, process.env, { timeoutMs }),
|
|
8448
|
+
readServer: () => readServerInstallationFacts(nodeServerRuntime().configDir),
|
|
8449
|
+
pathOf,
|
|
8450
|
+
tail
|
|
8451
|
+
})
|
|
8452
|
+
});
|
|
7538
8453
|
var minuteOrUnknown = (at) => at === null ? "unknown" : at.toISOString().slice(0, 16);
|
|
7539
8454
|
var staleGrantReason = (secret) => {
|
|
7540
8455
|
const facts = claudeGrantFacts(secret);
|
|
@@ -7567,7 +8482,7 @@ var claudeGrant = async () => {
|
|
|
7567
8482
|
return null;
|
|
7568
8483
|
}
|
|
7569
8484
|
}
|
|
7570
|
-
if (status.configDirectory !== null &&
|
|
8485
|
+
if (status.configDirectory !== null && path15.resolve(status.configDirectory) !== path15.resolve(dir)) {
|
|
7571
8486
|
fail6(
|
|
7572
8487
|
`claude: this Claude read ${status.configDirectory} instead of ${dir}, so a separate grant is not possible \u2014 connect the shared one deliberately with --use-my-login`
|
|
7573
8488
|
);
|
|
@@ -7630,7 +8545,7 @@ var connectCommand = async (config, args) => {
|
|
|
7630
8545
|
}
|
|
7631
8546
|
let secret;
|
|
7632
8547
|
if (flags.includes("--from-stdin")) {
|
|
7633
|
-
secret =
|
|
8548
|
+
secret = fs14.readFileSync(0, "utf8").trim();
|
|
7634
8549
|
if (secret === "") return fail6("nothing on stdin");
|
|
7635
8550
|
} else if (provider === "claude" && !flags.includes("--use-my-login")) {
|
|
7636
8551
|
secret = await claudeGrant();
|
|
@@ -7676,14 +8591,14 @@ var takeFlagValue4 = (args, flag) => {
|
|
|
7676
8591
|
return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
|
|
7677
8592
|
};
|
|
7678
8593
|
var saveCliConfig = (next) => {
|
|
7679
|
-
|
|
7680
|
-
|
|
8594
|
+
fs14.mkdirSync(path15.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
|
|
8595
|
+
fs14.writeFileSync(
|
|
7681
8596
|
CONFIG_PATH,
|
|
7682
8597
|
`${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
|
|
7683
8598
|
`,
|
|
7684
8599
|
{ mode: 384 }
|
|
7685
8600
|
);
|
|
7686
|
-
|
|
8601
|
+
fs14.chmodSync(CONFIG_PATH, 384);
|
|
7687
8602
|
};
|
|
7688
8603
|
var login = async (config, args) => {
|
|
7689
8604
|
await loginCommand(args, {
|
|
@@ -7696,7 +8611,7 @@ var login = async (config, args) => {
|
|
|
7696
8611
|
});
|
|
7697
8612
|
};
|
|
7698
8613
|
var logout = async (config) => {
|
|
7699
|
-
if (!
|
|
8614
|
+
if (!fs14.existsSync(CONFIG_PATH) && config.token === null) {
|
|
7700
8615
|
say6(dim6("nothing saved \u2014 already signed out"));
|
|
7701
8616
|
return;
|
|
7702
8617
|
}
|
|
@@ -7749,8 +8664,8 @@ var uninstallCommand = async (config, args) => {
|
|
|
7749
8664
|
const runtime = {
|
|
7750
8665
|
server,
|
|
7751
8666
|
cliHome: mendCliHome(),
|
|
7752
|
-
sshConfigFile:
|
|
7753
|
-
signedIn: config.token === null || !
|
|
8667
|
+
sshConfigFile: path15.join(os9.homedir(), ".ssh", "config"),
|
|
8668
|
+
signedIn: config.token === null || !fs14.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
|
|
7754
8669
|
revokeDevice: async () => {
|
|
7755
8670
|
if (config.deviceId === null) return "no device id saved";
|
|
7756
8671
|
try {
|
|
@@ -7814,7 +8729,7 @@ var keysShare = async (config) => {
|
|
|
7814
8729
|
return fail6("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
|
|
7815
8730
|
}
|
|
7816
8731
|
await shareAgent({
|
|
7817
|
-
url: () => socketUrl(config, "keys-bridge", { host:
|
|
8732
|
+
url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
|
|
7818
8733
|
agentSock,
|
|
7819
8734
|
signal: new AbortController().signal,
|
|
7820
8735
|
onEvent: (event) => {
|
|
@@ -7845,6 +8760,34 @@ var keysShare = async (config) => {
|
|
|
7845
8760
|
}
|
|
7846
8761
|
});
|
|
7847
8762
|
};
|
|
8763
|
+
var gitAuthorCommand = async (config, args) => {
|
|
8764
|
+
const asked = parseGitAuthorArgs(args);
|
|
8765
|
+
switch (asked.kind) {
|
|
8766
|
+
case "usage":
|
|
8767
|
+
return fail6(usageOf("git-author"));
|
|
8768
|
+
case "invalid":
|
|
8769
|
+
return fail6(asked.message);
|
|
8770
|
+
case "show":
|
|
8771
|
+
say6(gitAuthorLine(await api(config, "GET", "/me/git-author")));
|
|
8772
|
+
return;
|
|
8773
|
+
case "clear": {
|
|
8774
|
+
const author = await api(config, "DELETE", "/me/git-author");
|
|
8775
|
+
say6(`${green6("\u2713")} git author \xB7 ${gitAuthorLine(author)}`);
|
|
8776
|
+
return;
|
|
8777
|
+
}
|
|
8778
|
+
case "set": {
|
|
8779
|
+
const author = await api(config, "PUT", "/me/git-author", {
|
|
8780
|
+
name: asked.name,
|
|
8781
|
+
email: asked.email
|
|
8782
|
+
});
|
|
8783
|
+
say6(`${green6("\u2713")} git author \xB7 ${gitAuthorLine(author)}`);
|
|
8784
|
+
say6(
|
|
8785
|
+
dim6(" sessions launched from now on commit as this, unless their dotfiles say otherwise")
|
|
8786
|
+
);
|
|
8787
|
+
return;
|
|
8788
|
+
}
|
|
8789
|
+
}
|
|
8790
|
+
};
|
|
7848
8791
|
var keysMode = async (config, args) => {
|
|
7849
8792
|
const [value] = args;
|
|
7850
8793
|
if (value !== void 0 && value !== "mend-key" && value !== "bridge") {
|
|
@@ -7916,7 +8859,7 @@ ${usageOf("dotfiles repo")}`);
|
|
|
7916
8859
|
var dotfilesSync = async (config, args) => {
|
|
7917
8860
|
const all = args.includes("--all");
|
|
7918
8861
|
const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
|
|
7919
|
-
const home =
|
|
8862
|
+
const home = os9.homedir();
|
|
7920
8863
|
if (!all && requestedPaths.length === 0) {
|
|
7921
8864
|
const found = scanDotfileCandidates(home);
|
|
7922
8865
|
if (found.length === 0) {
|
|
@@ -7943,13 +8886,13 @@ var dotfilesSync = async (config, args) => {
|
|
|
7943
8886
|
if ("error" in read) return fail6(read.error);
|
|
7944
8887
|
const result = await api(config, "POST", "/dotfiles/snapshot", {
|
|
7945
8888
|
files: read.files,
|
|
7946
|
-
source:
|
|
8889
|
+
source: os9.hostname(),
|
|
7947
8890
|
merge: false
|
|
7948
8891
|
});
|
|
7949
8892
|
const snapshot = result.snapshot;
|
|
7950
8893
|
if (snapshot === null) return fail6("the server accepted the sync but reports no snapshot");
|
|
7951
8894
|
say6(
|
|
7952
|
-
`${green6("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${
|
|
8895
|
+
`${green6("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os9.hostname()} \xB7 ${dim6(snapshot.sha.slice(0, 7))} ${dim6("\u2014 applies from the next session launch")}`
|
|
7953
8896
|
);
|
|
7954
8897
|
};
|
|
7955
8898
|
var envLoad = async (config, args) => {
|
|
@@ -7963,10 +8906,10 @@ var envLoad = async (config, args) => {
|
|
|
7963
8906
|
const positional = args.filter(
|
|
7964
8907
|
(arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
|
|
7965
8908
|
);
|
|
7966
|
-
const file =
|
|
8909
|
+
const file = path15.resolve(positional[0] ?? ".env");
|
|
7967
8910
|
let contents;
|
|
7968
8911
|
try {
|
|
7969
|
-
contents =
|
|
8912
|
+
contents = fs14.readFileSync(file, "utf8");
|
|
7970
8913
|
} catch {
|
|
7971
8914
|
return fail6(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
|
|
7972
8915
|
}
|
|
@@ -7982,10 +8925,10 @@ var envLoad = async (config, args) => {
|
|
|
7982
8925
|
{ contents, allSecret, secretNames }
|
|
7983
8926
|
);
|
|
7984
8927
|
if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
|
|
7985
|
-
say6(dim6(`${
|
|
8928
|
+
say6(dim6(`${path15.basename(file)} has no variables`));
|
|
7986
8929
|
return;
|
|
7987
8930
|
}
|
|
7988
|
-
say6(`${green6("\u2713")} loaded ${
|
|
8931
|
+
say6(`${green6("\u2713")} loaded ${path15.basename(file)} into ${project2.name}`);
|
|
7989
8932
|
for (const line of formatLoadReport(report, { dim: dim6, warn: amber2 })) say6(line);
|
|
7990
8933
|
const plaintextUrls = report.loaded.filter(
|
|
7991
8934
|
(entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
|
|
@@ -8059,7 +9002,7 @@ var envCluster = async (config, args) => {
|
|
|
8059
9002
|
const explicitProject = takeFlagValue4(args, "--project");
|
|
8060
9003
|
const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
|
|
8061
9004
|
const [verb, ...rest] = positional;
|
|
8062
|
-
const
|
|
9005
|
+
const usage2 = usageOf("env cluster");
|
|
8063
9006
|
const project2 = await findProject(config, explicitProject);
|
|
8064
9007
|
const route = `/projects/${project2.id}/cluster-bindings`;
|
|
8065
9008
|
const appliesLine = () => say6(
|
|
@@ -8071,7 +9014,7 @@ var envCluster = async (config, args) => {
|
|
|
8071
9014
|
const [kind, objectName] = rest;
|
|
8072
9015
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
8073
9016
|
return fail6(`env cluster add takes a kind and an object name
|
|
8074
|
-
${
|
|
9017
|
+
${usage2}`);
|
|
8075
9018
|
}
|
|
8076
9019
|
const result = await api(config, "POST", route, {
|
|
8077
9020
|
kind,
|
|
@@ -8086,7 +9029,7 @@ ${usage}`);
|
|
|
8086
9029
|
const [kind, objectName] = ref?.split("/", 2) ?? [];
|
|
8087
9030
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
8088
9031
|
return fail6(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
|
|
8089
|
-
${
|
|
9032
|
+
${usage2}`);
|
|
8090
9033
|
}
|
|
8091
9034
|
const snapshot = await api(config, "GET", route);
|
|
8092
9035
|
const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
|
|
@@ -8104,7 +9047,7 @@ ${usage}`);
|
|
|
8104
9047
|
const [name] = rest;
|
|
8105
9048
|
if (!clear && name === void 0)
|
|
8106
9049
|
return fail6(`env cluster sa takes a name or --clear
|
|
8107
|
-
${
|
|
9050
|
+
${usage2}`);
|
|
8108
9051
|
const result = await api(
|
|
8109
9052
|
config,
|
|
8110
9053
|
"PUT",
|
|
@@ -8124,7 +9067,7 @@ ${usage}`);
|
|
|
8124
9067
|
return appliesLine();
|
|
8125
9068
|
}
|
|
8126
9069
|
return fail6(`unknown env cluster command "${verb ?? ""}"
|
|
8127
|
-
${
|
|
9070
|
+
${usage2}`);
|
|
8128
9071
|
};
|
|
8129
9072
|
var envCommand = async (config, args) => {
|
|
8130
9073
|
const [verb, ...rest] = args;
|
|
@@ -8176,7 +9119,7 @@ var skillsList = async (config, args) => {
|
|
|
8176
9119
|
};
|
|
8177
9120
|
var skillsPush = async (config, args) => {
|
|
8178
9121
|
const dirFlag = takeFlagValue4(args, "--dir");
|
|
8179
|
-
const root = dirFlag === null ?
|
|
9122
|
+
const root = dirFlag === null ? path15.join(os9.homedir(), DEFAULT_SKILLS_DIR) : path15.resolve(dirFlag);
|
|
8180
9123
|
const scanned = scanSkillLibrary(root);
|
|
8181
9124
|
if ("error" in scanned) return fail6(scanned.error);
|
|
8182
9125
|
for (const note of scanned.notes) say6(dim6(` ${note.skill}: ${note.message}`));
|
|
@@ -8248,6 +9191,7 @@ _mend() {
|
|
|
8248
9191
|
'server:local server setup, lifecycle and upgrades'
|
|
8249
9192
|
'uninstall:remove the server, local Mend files, or both'
|
|
8250
9193
|
'keys:the machine Mend deploy key \u2014 init, show, share'
|
|
9194
|
+
'git-author:the name and email your workspaces commit as'
|
|
8251
9195
|
'skills:skill libraries \u2014 list, push'
|
|
8252
9196
|
'accounts:your connected accounts on the platform'
|
|
8253
9197
|
'pair:pair a phone or a second machine' 'doctor:read-only checklist of this setup'
|
|
@@ -8288,7 +9232,7 @@ _mend "$@"
|
|
|
8288
9232
|
var BASH_COMPLETIONS = `_mend() {
|
|
8289
9233
|
local cur=\${COMP_WORDS[COMP_CWORD]}
|
|
8290
9234
|
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
8291
|
-
COMPREPLY=( $(compgen -W "adopt codex claude opencode run attach stop shell service server uninstall keys skills pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
|
|
9235
|
+
COMPREPLY=( $(compgen -W "adopt codex claude opencode run attach stop shell service server uninstall keys git-author skills pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
|
|
8292
9236
|
return
|
|
8293
9237
|
fi
|
|
8294
9238
|
case \${COMP_WORDS[1]} in
|
|
@@ -8600,11 +9544,14 @@ var refreshCommand = async (config, args) => {
|
|
|
8600
9544
|
}
|
|
8601
9545
|
if (branches.length > 12) say6(dim6(` \u2026 ${branches.length - 12} more`));
|
|
8602
9546
|
};
|
|
9547
|
+
var rowHold = (row) => row.annotation === void 0 ? null : servicesHoldOf(row.session, row.annotation.currentAgent, row.annotation.liveServices ?? 0);
|
|
8603
9548
|
var printSessionRow = (row) => {
|
|
8604
9549
|
const { session, annotation } = row;
|
|
8605
9550
|
const live = ACTIVE_STATUSES.has(session.status);
|
|
8606
9551
|
const status = session.status.padEnd(9);
|
|
8607
9552
|
const facts = [];
|
|
9553
|
+
const hold = rowHold(row);
|
|
9554
|
+
if (hold !== null) facts.push(amber2(hold));
|
|
8608
9555
|
if (session.label !== null) facts.push(session.label);
|
|
8609
9556
|
if (annotation !== void 0 && annotation.openComments > 0) {
|
|
8610
9557
|
facts.push(amber2(`${annotation.openComments} open`));
|
|
@@ -8624,7 +9571,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
8624
9571
|
say6(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
|
|
8625
9572
|
return;
|
|
8626
9573
|
}
|
|
8627
|
-
const
|
|
9574
|
+
const json2 = args.includes("--json");
|
|
8628
9575
|
const projectFlag = args.indexOf("--project");
|
|
8629
9576
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
|
|
8630
9577
|
const projects = await api(config, "GET", "/projects");
|
|
@@ -8633,19 +9580,22 @@ var sessionsCommand = async (config, args) => {
|
|
|
8633
9580
|
return fail6(`no adopted project named "${projectName}"`);
|
|
8634
9581
|
}
|
|
8635
9582
|
let rows;
|
|
8636
|
-
if (all || projectName !== null) {
|
|
9583
|
+
if (all || projectName !== null || !json2) {
|
|
8637
9584
|
const details = await Promise.all(
|
|
8638
9585
|
scope.map(
|
|
8639
9586
|
(p) => api(config, "GET", `/projects/${p.id}${all ? "?deadEnds=include" : ""}`)
|
|
8640
9587
|
)
|
|
8641
9588
|
);
|
|
8642
|
-
|
|
9589
|
+
const detailed = details.flatMap(
|
|
8643
9590
|
(detail) => detail.sessions.map((session) => ({
|
|
8644
9591
|
session,
|
|
8645
9592
|
projectName: detail.project.name,
|
|
8646
9593
|
annotation: detail.annotations.find((a) => a.sessionId === session.id)
|
|
8647
9594
|
}))
|
|
8648
9595
|
);
|
|
9596
|
+
rows = all || projectName !== null ? detailed : detailed.filter(
|
|
9597
|
+
(row) => ACTIVE_STATUSES.has(row.session.status) || rowHold(row) !== null
|
|
9598
|
+
);
|
|
8649
9599
|
} else {
|
|
8650
9600
|
const active = await api(config, "GET", "/sessions");
|
|
8651
9601
|
const nameById = new Map(projects.map((p) => [p.id, p.name]));
|
|
@@ -8656,7 +9606,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
8656
9606
|
}));
|
|
8657
9607
|
}
|
|
8658
9608
|
if (rows.length === 0) {
|
|
8659
|
-
if (
|
|
9609
|
+
if (json2) {
|
|
8660
9610
|
say6(JSON.stringify({ version: 1, sessions: [] }, null, 2));
|
|
8661
9611
|
return;
|
|
8662
9612
|
}
|
|
@@ -8671,7 +9621,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
8671
9621
|
if (aLive !== bLive) return bLive - aLive;
|
|
8672
9622
|
return b.session.createdAt.localeCompare(a.session.createdAt);
|
|
8673
9623
|
});
|
|
8674
|
-
if (
|
|
9624
|
+
if (json2) {
|
|
8675
9625
|
const payload = {
|
|
8676
9626
|
version: 1,
|
|
8677
9627
|
sessions: rows.map(({ session, projectName: rowProjectName, annotation }) => ({
|
|
@@ -8770,11 +9720,11 @@ var buildWorktreesJson = async (config, projectName) => {
|
|
|
8770
9720
|
return { version: 2, worktrees };
|
|
8771
9721
|
};
|
|
8772
9722
|
var worktreesCommand = async (config, args) => {
|
|
8773
|
-
const
|
|
9723
|
+
const json2 = args.includes("--json");
|
|
8774
9724
|
const projectFlag = args.indexOf("--project");
|
|
8775
9725
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
|
|
8776
9726
|
const payload = await buildWorktreesJson(config, projectName);
|
|
8777
|
-
if (
|
|
9727
|
+
if (json2) {
|
|
8778
9728
|
say6(JSON.stringify(payload, null, 2));
|
|
8779
9729
|
return;
|
|
8780
9730
|
}
|
|
@@ -8831,7 +9781,7 @@ var dashboard = async (config, options = {}) => {
|
|
|
8831
9781
|
);
|
|
8832
9782
|
process.exit(rerun.status ?? 0);
|
|
8833
9783
|
}
|
|
8834
|
-
const { runDashboard } = await import("./dashboard-
|
|
9784
|
+
const { runDashboard } = await import("./dashboard-3HNFSQZD.js");
|
|
8835
9785
|
const agentShare = await startShareIfBridge(config);
|
|
8836
9786
|
const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
|
|
8837
9787
|
listServices: () => fetchServices(config),
|
|
@@ -8865,7 +9815,7 @@ var startShareIfBridge = async (config) => {
|
|
|
8865
9815
|
}
|
|
8866
9816
|
if (access.mode !== "bridge") return null;
|
|
8867
9817
|
return startAgentShare({
|
|
8868
|
-
url: () => socketUrl(config, "keys-bridge", { host:
|
|
9818
|
+
url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
|
|
8869
9819
|
agentSock
|
|
8870
9820
|
});
|
|
8871
9821
|
};
|
|
@@ -8905,10 +9855,10 @@ var manCommand = (words) => {
|
|
|
8905
9855
|
}
|
|
8906
9856
|
const version = cliVersion();
|
|
8907
9857
|
const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
|
|
8908
|
-
const file =
|
|
8909
|
-
|
|
9858
|
+
const file = path15.join(os9.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
|
|
9859
|
+
fs14.writeFileSync(file, page);
|
|
8910
9860
|
const result = spawnSync4("man", ["-l", file], { stdio: "inherit" });
|
|
8911
|
-
|
|
9861
|
+
fs14.rmSync(file, { force: true });
|
|
8912
9862
|
if (result.error !== void 0 || result.status !== 0) {
|
|
8913
9863
|
say6(doc === null ? renderIndex() : renderCommand(doc));
|
|
8914
9864
|
}
|
|
@@ -8949,6 +9899,8 @@ var main = async () => {
|
|
|
8949
9899
|
return uninstallCommand(config, rest);
|
|
8950
9900
|
case "keys":
|
|
8951
9901
|
return keysCommand(config, rest);
|
|
9902
|
+
case "git-author":
|
|
9903
|
+
return gitAuthorCommand(config, rest);
|
|
8952
9904
|
case "dotfiles":
|
|
8953
9905
|
return dotfilesCommand(config, rest);
|
|
8954
9906
|
case "skills":
|
|
@@ -8979,10 +9931,8 @@ var main = async () => {
|
|
|
8979
9931
|
case "qr":
|
|
8980
9932
|
return qrCommand(rest);
|
|
8981
9933
|
case "doctor":
|
|
8982
|
-
return
|
|
8983
|
-
|
|
8984
|
-
return read.kind === "missing" ? null : read.secret;
|
|
8985
|
-
});
|
|
9934
|
+
if (rest.includes("--bundle")) return doctorBundle(config, rest);
|
|
9935
|
+
return doctorCommand(config, localCredential, claudeGrantSecret);
|
|
8986
9936
|
case "env":
|
|
8987
9937
|
return envCommand(config, rest);
|
|
8988
9938
|
case "ssh":
|