@sealant/mend 0.31.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/main.js +1682 -878
- 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 +1 -1
- package/man/mend-help.1 +1 -1
- package/man/mend-invite.1 +1 -1
- package/man/mend-keys-init.1 +1 -1
- package/man/mend-keys-mode.1 +1 -1
- package/man/mend-keys-share.1 +1 -1
- package/man/mend-keys-show.1 +1 -1
- package/man/mend-land.1 +1 -1
- package/man/mend-login.1 +1 -1
- package/man/mend-logout.1 +1 -1
- package/man/mend-man.1 +1 -1
- package/man/mend-members.1 +1 -1
- package/man/mend-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 +1 -1
- package/man/mend-ui.1 +1 -1
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +2 -2
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -31,11 +31,11 @@ import {
|
|
|
31
31
|
|
|
32
32
|
// src/main.ts
|
|
33
33
|
import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
|
|
34
|
-
import * as
|
|
34
|
+
import * as fs14 from "node:fs";
|
|
35
35
|
import { createRequire } from "node:module";
|
|
36
36
|
import * as net4 from "node:net";
|
|
37
|
-
import * as
|
|
38
|
-
import * as
|
|
37
|
+
import * as os9 from "node:os";
|
|
38
|
+
import * as path15 from "node:path";
|
|
39
39
|
|
|
40
40
|
// src/agent-share.ts
|
|
41
41
|
import * as net from "node:net";
|
|
@@ -43,7 +43,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
43
43
|
var retryDelayMs = (attempt2) => Math.min(3e4, 1e3 * 2 ** Math.min(Math.max(attempt2 - 1, 0), 5));
|
|
44
44
|
var SSH_AGENTC_REQUEST_IDENTITIES = 11;
|
|
45
45
|
var SSH_AGENTC_SIGN_REQUEST = 13;
|
|
46
|
-
var askLocalAgent = (agentSock, payload) => new Promise((
|
|
46
|
+
var askLocalAgent = (agentSock, payload) => new Promise((resolve7, reject) => {
|
|
47
47
|
const connection = net.connect(agentSock, () => {
|
|
48
48
|
connection.write(payload);
|
|
49
49
|
});
|
|
@@ -57,7 +57,7 @@ var askLocalAgent = (agentSock, payload) => new Promise((resolve6, reject) => {
|
|
|
57
57
|
if (pending.length >= 4 && pending.length >= 4 + pending.readUInt32BE(0)) {
|
|
58
58
|
clearTimeout(timer);
|
|
59
59
|
connection.end();
|
|
60
|
-
|
|
60
|
+
resolve7(pending.subarray(0, 4 + pending.readUInt32BE(0)));
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
connection.on("error", (error) => {
|
|
@@ -87,12 +87,12 @@ var shareAgent = async (options) => {
|
|
|
87
87
|
const ws = new WebSocket(target);
|
|
88
88
|
const onAbort = () => ws.close();
|
|
89
89
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
90
|
-
const closed = new Promise((
|
|
91
|
-
ws.addEventListener("close", () =>
|
|
90
|
+
const closed = new Promise((resolve7) => {
|
|
91
|
+
ws.addEventListener("close", () => resolve7(), { once: true });
|
|
92
92
|
});
|
|
93
|
-
const opened = await new Promise((
|
|
94
|
-
ws.addEventListener("open", () =>
|
|
95
|
-
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 });
|
|
96
96
|
});
|
|
97
97
|
if (opened) {
|
|
98
98
|
attempt2 = 0;
|
|
@@ -110,19 +110,19 @@ var shareAgent = async (options) => {
|
|
|
110
110
|
}
|
|
111
111
|
const id = frame.id;
|
|
112
112
|
const payload = Buffer.from(frame.payload, "base64");
|
|
113
|
-
const
|
|
113
|
+
const type2 = payload[4];
|
|
114
114
|
const context = typeof frame.context === "string" ? frame.context : "mend";
|
|
115
115
|
chain = chain.then(async () => {
|
|
116
116
|
const started = Date.now();
|
|
117
|
-
if (
|
|
118
|
-
else if (
|
|
117
|
+
if (type2 === SSH_AGENTC_SIGN_REQUEST) onEvent({ kind: "sign-requested", context });
|
|
118
|
+
else if (type2 === SSH_AGENTC_REQUEST_IDENTITIES) {
|
|
119
119
|
onEvent({ kind: "identities-requested", context });
|
|
120
120
|
}
|
|
121
121
|
try {
|
|
122
122
|
const response = await askLocalAgent(agentSock, payload);
|
|
123
123
|
if (ws.readyState !== WebSocket.OPEN) return null;
|
|
124
124
|
ws.send(JSON.stringify({ t: "res", id, payload: response.toString("base64") }));
|
|
125
|
-
if (
|
|
125
|
+
if (type2 === SSH_AGENTC_SIGN_REQUEST) {
|
|
126
126
|
onEvent({ kind: "signed", context, seconds: (Date.now() - started) / 1e3 });
|
|
127
127
|
}
|
|
128
128
|
} catch (error) {
|
|
@@ -130,7 +130,7 @@ var shareAgent = async (options) => {
|
|
|
130
130
|
if (ws.readyState === WebSocket.OPEN) {
|
|
131
131
|
ws.send(JSON.stringify({ t: "err", id, message }));
|
|
132
132
|
}
|
|
133
|
-
if (
|
|
133
|
+
if (type2 === SSH_AGENTC_SIGN_REQUEST) onEvent({ kind: "not-signed", context, message });
|
|
134
134
|
}
|
|
135
135
|
return null;
|
|
136
136
|
});
|
|
@@ -243,8 +243,8 @@ var readGrant = (configDir, platform = process.platform) => {
|
|
|
243
243
|
import { execFile } from "node:child_process";
|
|
244
244
|
var IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
245
245
|
var pickImageType = (offered) => {
|
|
246
|
-
const types = offered.split(/\s+/).map((
|
|
247
|
-
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;
|
|
248
248
|
return null;
|
|
249
249
|
};
|
|
250
250
|
var parsePngfHex = (output) => {
|
|
@@ -253,13 +253,13 @@ var parsePngfHex = (output) => {
|
|
|
253
253
|
return Buffer.from(match[1], "hex");
|
|
254
254
|
};
|
|
255
255
|
var MAX_BYTES = 32 * 1024 * 1024;
|
|
256
|
-
var run = (command, args) => new Promise((
|
|
256
|
+
var run = (command, args) => new Promise((resolve7) => {
|
|
257
257
|
execFile(
|
|
258
258
|
command,
|
|
259
259
|
[...args],
|
|
260
260
|
{ encoding: "buffer", maxBuffer: MAX_BYTES, timeout: 3e3 },
|
|
261
261
|
(error, stdout) => {
|
|
262
|
-
|
|
262
|
+
resolve7(error === null ? stdout : null);
|
|
263
263
|
}
|
|
264
264
|
);
|
|
265
265
|
});
|
|
@@ -296,465 +296,736 @@ var readClipboardImage = async (platform = process.platform, env = process.env)
|
|
|
296
296
|
return null;
|
|
297
297
|
};
|
|
298
298
|
|
|
299
|
-
// 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";
|
|
300
306
|
import * as fs2 from "node:fs";
|
|
301
307
|
import * as path2 from "node:path";
|
|
302
|
-
var
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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"
|
|
312
324
|
});
|
|
313
|
-
if (!response.ok) return { value: null, status: response.status };
|
|
314
|
-
return { value: await response.json(), status: response.status };
|
|
315
|
-
} catch {
|
|
316
|
-
return { value: null, status: null };
|
|
317
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
|
+
});
|
|
318
434
|
};
|
|
319
|
-
var
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
}
|
|
334
487
|
};
|
|
488
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
489
|
+
return results;
|
|
335
490
|
};
|
|
336
|
-
var
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
];
|
|
346
|
-
var notChecked = (label) => ({
|
|
347
|
-
label,
|
|
348
|
-
state: "todo",
|
|
349
|
-
detail: "not checked",
|
|
350
|
-
fix: null
|
|
351
|
-
});
|
|
352
|
-
var identityOf = (account) => {
|
|
353
|
-
const meta = account.metadata;
|
|
354
|
-
for (const key of ["login", "email", "accountEmail", "accountId"]) {
|
|
355
|
-
const value = meta[key];
|
|
356
|
-
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
|
+
}
|
|
357
500
|
}
|
|
358
501
|
return null;
|
|
359
502
|
};
|
|
360
|
-
var
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
})
|
|
374
528
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
396
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;
|
|
397
606
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
checks.push({
|
|
412
|
-
label: "sealant",
|
|
413
|
-
state: "ok",
|
|
414
|
-
detail: `connected \xB7 ${connection.value.baseUrl}`,
|
|
415
|
-
fix: null
|
|
416
|
-
});
|
|
417
|
-
} else {
|
|
418
|
-
checks.push({
|
|
419
|
-
label: "sealant",
|
|
420
|
-
state: "failed",
|
|
421
|
-
detail: `${connection.value.status} \xB7 ${connection.value.detail ?? connection.value.baseUrl}`,
|
|
422
|
-
fix: null
|
|
423
|
-
});
|
|
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 [];
|
|
424
620
|
}
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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") ?? ""
|
|
431
631
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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);
|
|
441
662
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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());
|
|
454
747
|
}
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
const grant = probes.claudeGrant();
|
|
458
|
-
if (grant !== null) {
|
|
459
|
-
const facts = claudeGrantFacts(grant);
|
|
460
|
-
if (facts === null) {
|
|
461
|
-
checks.push({
|
|
462
|
-
label: "grant",
|
|
463
|
-
state: "failed",
|
|
464
|
-
detail: "unreadable",
|
|
465
|
-
fix: "mend connect claude"
|
|
466
|
-
});
|
|
467
|
-
} else if (!facts.hasRefreshToken) {
|
|
468
|
-
checks.push({
|
|
469
|
-
label: "grant",
|
|
470
|
-
state: "failed",
|
|
471
|
-
detail: "signed out",
|
|
472
|
-
fix: "mend connect claude"
|
|
473
|
-
});
|
|
474
|
-
} else if (facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now()) {
|
|
475
|
-
checks.push({
|
|
476
|
-
label: "grant",
|
|
477
|
-
state: "todo",
|
|
478
|
-
detail: `expired ${day(facts.refreshExpiresAt)}`,
|
|
479
|
-
fix: "mend connect claude"
|
|
480
|
-
});
|
|
481
|
-
} else {
|
|
482
|
-
checks.push({
|
|
483
|
-
label: "grant",
|
|
484
|
-
state: "ok",
|
|
485
|
-
detail: facts.refreshExpiresAt === null ? "Mend's own" : `Mend's own \xB7 expires ${day(facts.refreshExpiresAt)}`,
|
|
486
|
-
fix: null
|
|
487
|
-
});
|
|
488
748
|
}
|
|
749
|
+
return [{ path: "docker.txt", content: `${sections.join("\n")}
|
|
750
|
+
` }];
|
|
489
751
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
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"
|
|
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
|
+
];
|
|
502
768
|
}
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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;
|
|
512
783
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
var
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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 };
|
|
525
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
|
+
};
|
|
526
822
|
}
|
|
527
|
-
return false;
|
|
528
823
|
};
|
|
529
|
-
var
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
+
`;
|
|
533
850
|
};
|
|
534
|
-
var
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
];
|
|
539
983
|
};
|
|
540
984
|
|
|
541
|
-
// src/
|
|
542
|
-
import * as
|
|
543
|
-
import * as
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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 ──────────────────────────────────────────────────────────────
|
|
563
1011
|
{
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
".
|
|
579
|
-
|
|
580
|
-
]
|
|
581
|
-
}
|
|
582
|
-
];
|
|
583
|
-
var MAX_FILE_BYTES = 1024 * 1024;
|
|
584
|
-
var statFile = (home, relative4) => {
|
|
585
|
-
try {
|
|
586
|
-
const stat = fs3.statSync(path3.join(home, relative4));
|
|
587
|
-
return stat.isFile() ? stat : null;
|
|
588
|
-
} catch {
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
var scanDotfileCandidates = (home) => DOTFILE_CANDIDATES.flatMap(
|
|
593
|
-
({ group, paths }) => paths.flatMap((relative4) => {
|
|
594
|
-
const stat = statFile(home, relative4);
|
|
595
|
-
return stat === null ? [] : [{ path: relative4, group, bytes: stat.size }];
|
|
596
|
-
})
|
|
597
|
-
);
|
|
598
|
-
var homeRelativePath = (home, requested) => {
|
|
599
|
-
const relative4 = path3.relative(home, path3.resolve(home, requested));
|
|
600
|
-
const outside = relative4 === "" || relative4 === ".." || relative4.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative4);
|
|
601
|
-
return outside ? null : relative4.split(path3.sep).join("/");
|
|
602
|
-
};
|
|
603
|
-
var readSyncFiles = (home, paths_) => {
|
|
604
|
-
const files = [];
|
|
605
|
-
for (const requested of paths_) {
|
|
606
|
-
const relative4 = homeRelativePath(home, requested);
|
|
607
|
-
if (relative4 === null) {
|
|
608
|
-
return {
|
|
609
|
-
error: `${requested} is not under ${home} \u2014 only files in your home directory sync`
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
const stat = statFile(home, relative4);
|
|
613
|
-
if (stat === null) {
|
|
614
|
-
return { error: `${relative4} is not a file under ${home}` };
|
|
615
|
-
}
|
|
616
|
-
if (stat.size > MAX_FILE_BYTES) {
|
|
617
|
-
return { error: `${relative4} is over 1MB \u2014 dotfiles are text; trim the selection` };
|
|
618
|
-
}
|
|
619
|
-
files.push({
|
|
620
|
-
path: relative4,
|
|
621
|
-
contentsBase64: fs3.readFileSync(path3.join(home, relative4)).toString("base64"),
|
|
622
|
-
// eslint-disable-next-line no-bitwise -- permission bits are the point
|
|
623
|
-
mode: (stat.mode & 511).toString(8)
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
return { files };
|
|
627
|
-
};
|
|
628
|
-
var DOTFILES_MANAGERS = ["auto", "copy", "stow", "chezmoi"];
|
|
629
|
-
var VALUE_FLAGS = ["--ref", "--subdirectory", "--manager"];
|
|
630
|
-
var parseDotfilesRepoArgs = (args) => {
|
|
631
|
-
if (args.includes("--clear")) {
|
|
632
|
-
return args.length === 1 ? { kind: "clear" } : { kind: "error", error: "--clear takes no URL or other options" };
|
|
633
|
-
}
|
|
634
|
-
let url = null;
|
|
635
|
-
const values = /* @__PURE__ */ new Map();
|
|
636
|
-
let bootstrap = true;
|
|
637
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
638
|
-
const arg = args[index] ?? "";
|
|
639
|
-
if (arg === "--no-bootstrap") {
|
|
640
|
-
bootstrap = false;
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
const flag = VALUE_FLAGS.find((candidate) => candidate === arg);
|
|
644
|
-
if (flag !== void 0) {
|
|
645
|
-
const value = args[index + 1];
|
|
646
|
-
if (value === void 0 || value.trim() === "" || value.startsWith("--")) {
|
|
647
|
-
return { kind: "error", error: `${flag} needs a value` };
|
|
648
|
-
}
|
|
649
|
-
values.set(flag, value.trim());
|
|
650
|
-
index += 1;
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
653
|
-
if (arg.startsWith("-")) return { kind: "error", error: `unknown flag ${arg}` };
|
|
654
|
-
if (url !== null) return { kind: "error", error: "one repository URL only" };
|
|
655
|
-
url = arg.trim();
|
|
656
|
-
}
|
|
657
|
-
if (url === null || url === "") {
|
|
658
|
-
return { kind: "error", error: "name the repository URL, or --clear to remove it" };
|
|
659
|
-
}
|
|
660
|
-
const requested = values.get("--manager") ?? "auto";
|
|
661
|
-
const manager = DOTFILES_MANAGERS.find((candidate) => candidate === requested);
|
|
662
|
-
if (manager === void 0) {
|
|
663
|
-
return { kind: "error", error: `--manager must be one of ${DOTFILES_MANAGERS.join(", ")}` };
|
|
664
|
-
}
|
|
665
|
-
return {
|
|
666
|
-
kind: "set",
|
|
667
|
-
repository: {
|
|
668
|
-
url,
|
|
669
|
-
ref: values.get("--ref") ?? null,
|
|
670
|
-
subdirectory: values.get("--subdirectory") ?? null,
|
|
671
|
-
manager,
|
|
672
|
-
bootstrap
|
|
673
|
-
}
|
|
674
|
-
};
|
|
675
|
-
};
|
|
676
|
-
var dotfilesRepositoryFacts = (repository) => [
|
|
677
|
-
repository.ref ?? "default branch",
|
|
678
|
-
...repository.subdirectory === null ? [] : [`${repository.subdirectory}/`],
|
|
679
|
-
`manager ${repository.manager}`,
|
|
680
|
-
repository.bootstrap ? "install.sh on" : "install.sh off"
|
|
681
|
-
].join(" \xB7 ");
|
|
682
|
-
|
|
683
|
-
// src/env.ts
|
|
684
|
-
var formatLoadReport = (report, paint6) => {
|
|
685
|
-
const width = Math.max(
|
|
686
|
-
0,
|
|
687
|
-
...report.loaded.map((entry) => entry.name.length),
|
|
688
|
-
...report.rejected.map((entry) => entry.name.length)
|
|
689
|
-
);
|
|
690
|
-
const lines = [];
|
|
691
|
-
for (const entry of report.loaded) {
|
|
692
|
-
lines.push(
|
|
693
|
-
` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint6.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
for (const entry of report.rejected) {
|
|
697
|
-
lines.push(
|
|
698
|
-
` ${entry.name.padEnd(width)} ${paint6.warn("rejected")} ${paint6.dim(`\xB7 ${entry.reason}`)}`
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
return lines;
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
// src/git-author.ts
|
|
705
|
-
var parseGitAuthorArgs = (args) => {
|
|
706
|
-
const clear = args.includes("--clear");
|
|
707
|
-
const words = args.filter((arg) => !arg.startsWith("--"));
|
|
708
|
-
if (clear) return words.length === 0 ? { kind: "clear" } : { kind: "usage" };
|
|
709
|
-
if (words.length === 0) return { kind: "show" };
|
|
710
|
-
const [name, email] = words;
|
|
711
|
-
if (words.length !== 2 || name === void 0 || email === void 0) return { kind: "usage" };
|
|
712
|
-
const issue = gitAuthorIssue({ name, email });
|
|
713
|
-
if (issue !== null) return { kind: "invalid", message: `git author not saved \xB7 ${issue}` };
|
|
714
|
-
const author = normalizeGitAuthor({ name, email });
|
|
715
|
-
return { kind: "set", name: author.name, email: author.email };
|
|
716
|
-
};
|
|
717
|
-
var gitAuthorLine = (author) => `${author.name} <${author.email}> \xB7 ${author.source === "setting" ? "your setting" : "your account's name and email"}`;
|
|
718
|
-
|
|
719
|
-
// src/help.ts
|
|
720
|
-
var SECTIONS = [
|
|
721
|
-
"start",
|
|
722
|
-
"sessions",
|
|
723
|
-
"services",
|
|
724
|
-
"project setup",
|
|
725
|
-
"organization",
|
|
726
|
-
"this machine"
|
|
727
|
-
];
|
|
728
|
-
var project = (what = "the project") => ({
|
|
729
|
-
flag: "--project <p>",
|
|
730
|
-
text: `${what} by name, when the current directory is not inside it`
|
|
731
|
-
});
|
|
732
|
-
var sessionArg = "With no id, the one live session is taken; with several, a picker opens. A prefix of the id is enough.";
|
|
733
|
-
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.";
|
|
734
|
-
var noTunnel = {
|
|
735
|
-
flag: "--no-tunnel",
|
|
736
|
-
text: "do not tunnel the session's Services to this machine"
|
|
737
|
-
};
|
|
738
|
-
var COMMANDS = [
|
|
739
|
-
// ── start ──────────────────────────────────────────────────────────────
|
|
740
|
-
{
|
|
741
|
-
name: "login",
|
|
742
|
-
section: "start",
|
|
743
|
-
summary: "sign this terminal in through the browser",
|
|
744
|
-
synopsis: ["[--url <server>]"],
|
|
745
|
-
description: [
|
|
746
|
-
"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."
|
|
747
|
-
],
|
|
748
|
-
options: [
|
|
749
|
-
{
|
|
750
|
-
flag: "--url <server>",
|
|
751
|
-
text: "the Mend server. Default: MEND_URL, then http://localhost:3105"
|
|
752
|
-
}
|
|
753
|
-
],
|
|
754
|
-
examples: [
|
|
755
|
-
{ command: "mend login --url http://10.0.0.216:3105", text: "sign in to a LAN server" }
|
|
756
|
-
],
|
|
757
|
-
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"]
|
|
758
1029
|
},
|
|
759
1030
|
{
|
|
760
1031
|
name: "connect",
|
|
@@ -865,12 +1136,29 @@ var COMMANDS = [
|
|
|
865
1136
|
{
|
|
866
1137
|
name: "doctor",
|
|
867
1138
|
section: "start",
|
|
868
|
-
summary: "check this machine's setup",
|
|
869
|
-
synopsis: [],
|
|
1139
|
+
summary: "check this machine's setup, or bundle it for a bug report",
|
|
1140
|
+
synopsis: ["", "--bundle [--out <path>] [--tail <n>]"],
|
|
870
1141
|
description: [
|
|
871
|
-
"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" }
|
|
872
1160
|
],
|
|
873
|
-
see: ["login", "connect", "keys init"]
|
|
1161
|
+
see: ["login", "connect", "keys init", "server logs"]
|
|
874
1162
|
},
|
|
875
1163
|
// ── sessions ───────────────────────────────────────────────────────────
|
|
876
1164
|
{
|
|
@@ -2036,16 +2324,611 @@ var renderManIndex = (version) => {
|
|
|
2036
2324
|
COMMANDS.filter((d) => !d.hidden).map((d) => `mend-${d.name.replace(/ /g, "-")}(1)`).join(", ")
|
|
2037
2325
|
)
|
|
2038
2326
|
);
|
|
2039
|
-
return `${out.join("\n")}
|
|
2040
|
-
`;
|
|
2327
|
+
return `${out.join("\n")}
|
|
2328
|
+
`;
|
|
2329
|
+
};
|
|
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;
|
|
2041
2910
|
};
|
|
2042
|
-
|
|
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"}`;
|
|
2043
2926
|
|
|
2044
2927
|
// src/landing.ts
|
|
2045
2928
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2046
|
-
import * as
|
|
2047
|
-
import * as
|
|
2048
|
-
import * as
|
|
2929
|
+
import * as fs7 from "node:fs";
|
|
2930
|
+
import * as os3 from "node:os";
|
|
2931
|
+
import * as path7 from "node:path";
|
|
2049
2932
|
var paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
|
|
2050
2933
|
var dim = paint("2");
|
|
2051
2934
|
var green = paint("32");
|
|
@@ -2283,9 +3166,9 @@ var gitRemotes = (cwd) => {
|
|
|
2283
3166
|
var remoteForOrigin = (remotes, originUrl, cwd) => {
|
|
2284
3167
|
if (originUrl === null) return null;
|
|
2285
3168
|
const origin = normalizeRemoteUrl(originUrl);
|
|
2286
|
-
const originPath = origin === null ?
|
|
3169
|
+
const originPath = origin === null ? path7.resolve(originUrl.replace(/^file:\/\//, "")) : null;
|
|
2287
3170
|
return remotes.find(
|
|
2288
|
-
(remote) => origin === null ?
|
|
3171
|
+
(remote) => origin === null ? path7.resolve(cwd, remote.url.replace(/^file:\/\//, "")) === originPath : normalizeRemoteUrl(remote.url) === origin
|
|
2289
3172
|
) ?? null;
|
|
2290
3173
|
};
|
|
2291
3174
|
var LOG_LINES = 10;
|
|
@@ -2308,10 +3191,10 @@ var fetchBundle = (cwd, bundle) => {
|
|
|
2308
3191
|
const before = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
2309
3192
|
const previous = before.status === 0 ? before.stdout.trim() : null;
|
|
2310
3193
|
if (previous !== bundle.tip) {
|
|
2311
|
-
const dir =
|
|
3194
|
+
const dir = fs7.mkdtempSync(path7.join(os3.tmpdir(), "mend-pull-"));
|
|
2312
3195
|
try {
|
|
2313
|
-
const file =
|
|
2314
|
-
|
|
3196
|
+
const file = path7.join(dir, "change.bundle");
|
|
3197
|
+
fs7.writeFileSync(file, bundle.bytes, { mode: 384 });
|
|
2315
3198
|
const fetched = git(cwd, [
|
|
2316
3199
|
"fetch",
|
|
2317
3200
|
"--no-tags",
|
|
@@ -2321,7 +3204,7 @@ var fetchBundle = (cwd, bundle) => {
|
|
|
2321
3204
|
]);
|
|
2322
3205
|
if (fetched.status !== 0) return refusedWith(gitWords(fetched));
|
|
2323
3206
|
} finally {
|
|
2324
|
-
|
|
3207
|
+
fs7.rmSync(dir, { recursive: true, force: true });
|
|
2325
3208
|
}
|
|
2326
3209
|
}
|
|
2327
3210
|
const after = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).stdout.trim();
|
|
@@ -2359,7 +3242,7 @@ var fetchedLines = (fetched) => {
|
|
|
2359
3242
|
lines.push(`${dim(" switch to it")} git switch ${fetched.branch}`);
|
|
2360
3243
|
return lines;
|
|
2361
3244
|
};
|
|
2362
|
-
var
|
|
3245
|
+
var formatBytes2 = (bytes) => {
|
|
2363
3246
|
if (bytes < 1024) return `${bytes} bytes`;
|
|
2364
3247
|
const units = ["KiB", "MiB", "GiB"];
|
|
2365
3248
|
let value = bytes / 1024;
|
|
@@ -2372,13 +3255,13 @@ var formatBytes = (bytes) => {
|
|
|
2372
3255
|
};
|
|
2373
3256
|
var numberField = (value, key) => {
|
|
2374
3257
|
if (typeof value !== "object" || value === null || !(key in value)) return null;
|
|
2375
|
-
const
|
|
2376
|
-
return typeof
|
|
3258
|
+
const field2 = Reflect.get(value, key);
|
|
3259
|
+
return typeof field2 === "number" ? field2 : null;
|
|
2377
3260
|
};
|
|
2378
|
-
var
|
|
3261
|
+
var stringField2 = (value, key) => {
|
|
2379
3262
|
if (typeof value !== "object" || value === null || !(key in value)) return null;
|
|
2380
|
-
const
|
|
2381
|
-
return typeof
|
|
3263
|
+
const field2 = Reflect.get(value, key);
|
|
3264
|
+
return typeof field2 === "string" ? field2 : null;
|
|
2382
3265
|
};
|
|
2383
3266
|
var parseJson = (bytes) => {
|
|
2384
3267
|
try {
|
|
@@ -2392,9 +3275,9 @@ var bundleRefusal = (downloaded) => {
|
|
|
2392
3275
|
const size = numberField(body, "size");
|
|
2393
3276
|
const limit = numberField(body, "limit");
|
|
2394
3277
|
if (downloaded.status === 413 && size !== null && limit !== null) {
|
|
2395
|
-
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`;
|
|
2396
3279
|
}
|
|
2397
|
-
return
|
|
3280
|
+
return stringField2(body, "message") ?? `the bundle request answered ${downloaded.status}`;
|
|
2398
3281
|
};
|
|
2399
3282
|
var bundleFactsOf = (downloaded) => {
|
|
2400
3283
|
const branch = downloaded.header(BUNDLE_HEADERS.branch);
|
|
@@ -2439,8 +3322,8 @@ var pullCommand = async (api2, download2, args, cwd = process.cwd()) => {
|
|
|
2439
3322
|
};
|
|
2440
3323
|
|
|
2441
3324
|
// src/login.ts
|
|
2442
|
-
import { spawn } from "node:child_process";
|
|
2443
|
-
import * as
|
|
3325
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3326
|
+
import * as os4 from "node:os";
|
|
2444
3327
|
|
|
2445
3328
|
// src/pair.ts
|
|
2446
3329
|
var paint2 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
@@ -2503,23 +3386,23 @@ var qrCommand = async (args) => {
|
|
|
2503
3386
|
};
|
|
2504
3387
|
|
|
2505
3388
|
// src/login.ts
|
|
2506
|
-
var parseCliAuthStart = (
|
|
2507
|
-
if (typeof
|
|
2508
|
-
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)) {
|
|
2509
3392
|
return null;
|
|
2510
3393
|
}
|
|
2511
|
-
const { deviceCode, code, verifyPath, expiresAt, intervalSeconds } =
|
|
3394
|
+
const { deviceCode, code, verifyPath, expiresAt, intervalSeconds } = json2;
|
|
2512
3395
|
if (typeof deviceCode !== "string" || typeof code !== "string" || typeof verifyPath !== "string" || typeof expiresAt !== "string" || typeof intervalSeconds !== "number") {
|
|
2513
3396
|
return null;
|
|
2514
3397
|
}
|
|
2515
3398
|
return { deviceCode, code, verifyPath, expiresAt, intervalSeconds };
|
|
2516
3399
|
};
|
|
2517
|
-
var parseCliAuthPoll = (
|
|
2518
|
-
if (typeof
|
|
2519
|
-
if (
|
|
2520
|
-
if (
|
|
2521
|
-
if (!("token" in
|
|
2522
|
-
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;
|
|
2523
3406
|
if (typeof token !== "string") return null;
|
|
2524
3407
|
if (typeof user !== "object" || user === null) return null;
|
|
2525
3408
|
if (!("id" in user && "name" in user && "email" in user)) return null;
|
|
@@ -2579,7 +3462,7 @@ var openBrowser = (url) => {
|
|
|
2579
3462
|
const opener = browserCommand(process.platform);
|
|
2580
3463
|
if (opener === null || process.stdout.isTTY !== true) return;
|
|
2581
3464
|
try {
|
|
2582
|
-
const child =
|
|
3465
|
+
const child = spawn2(opener.command, [...opener.args, url], {
|
|
2583
3466
|
stdio: "ignore",
|
|
2584
3467
|
detached: true
|
|
2585
3468
|
});
|
|
@@ -2600,21 +3483,21 @@ var post = async (url, body) => {
|
|
|
2600
3483
|
return fail3(`cannot reach the Mend server at ${new URL(url).origin} \u2014 is it running?`);
|
|
2601
3484
|
}
|
|
2602
3485
|
const text = await response.text();
|
|
2603
|
-
let
|
|
3486
|
+
let json2 = null;
|
|
2604
3487
|
try {
|
|
2605
|
-
|
|
3488
|
+
json2 = JSON.parse(text);
|
|
2606
3489
|
} catch {
|
|
2607
3490
|
}
|
|
2608
|
-
return { status: response.status, json };
|
|
3491
|
+
return { status: response.status, json: json2 };
|
|
2609
3492
|
};
|
|
2610
|
-
var retryAfterSecondsOf = (
|
|
2611
|
-
if (typeof
|
|
2612
|
-
const seconds = Number(
|
|
3493
|
+
var retryAfterSecondsOf = (json2) => {
|
|
3494
|
+
if (typeof json2 === "object" && json2 !== null && "retryAfterSeconds" in json2) {
|
|
3495
|
+
const seconds = Number(json2.retryAfterSeconds);
|
|
2613
3496
|
if (Number.isFinite(seconds) && seconds > 0) return seconds;
|
|
2614
3497
|
}
|
|
2615
3498
|
return 5;
|
|
2616
3499
|
};
|
|
2617
|
-
var sleep2 = (ms) => new Promise((
|
|
3500
|
+
var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
2618
3501
|
var resolveServerUrl = async (args, deps) => {
|
|
2619
3502
|
const given = takeFlagValue2(args, "--url") ?? deps.configuredUrl;
|
|
2620
3503
|
if (given !== null) {
|
|
@@ -2632,7 +3515,7 @@ var resolveServerUrl = async (args, deps) => {
|
|
|
2632
3515
|
};
|
|
2633
3516
|
var loginCommand = async (args, deps) => {
|
|
2634
3517
|
const base = await resolveServerUrl(args, deps);
|
|
2635
|
-
const started = await post(`${base}/api/cli/auth`, { name:
|
|
3518
|
+
const started = await post(`${base}/api/cli/auth`, { name: os4.hostname() });
|
|
2636
3519
|
if (started.status === 429) {
|
|
2637
3520
|
return fail3(
|
|
2638
3521
|
`the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
|
|
@@ -2683,8 +3566,8 @@ var loginCommand = async (args, deps) => {
|
|
|
2683
3566
|
};
|
|
2684
3567
|
|
|
2685
3568
|
// src/organization.ts
|
|
2686
|
-
import * as
|
|
2687
|
-
import * as
|
|
3569
|
+
import * as fs8 from "node:fs";
|
|
3570
|
+
import * as path8 from "node:path";
|
|
2688
3571
|
var paint4 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
|
|
2689
3572
|
var dim4 = paint4("2");
|
|
2690
3573
|
var green4 = paint4("32");
|
|
@@ -2748,7 +3631,7 @@ var scanFolder = (dir) => {
|
|
|
2748
3631
|
const files = [];
|
|
2749
3632
|
const skipped = [];
|
|
2750
3633
|
const walk = (relative4) => {
|
|
2751
|
-
const entries =
|
|
3634
|
+
const entries = fs8.readdirSync(path8.join(dir, relative4), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
2752
3635
|
for (const entry of entries) {
|
|
2753
3636
|
const child = relative4 === "" ? entry.name : `${relative4}/${entry.name}`;
|
|
2754
3637
|
if (entry.isSymbolicLink()) {
|
|
@@ -2757,7 +3640,7 @@ var scanFolder = (dir) => {
|
|
|
2757
3640
|
if (SKIPPED_DIRECTORIES.has(entry.name)) skipped.push({ path: child, reason: "skipped" });
|
|
2758
3641
|
else walk(child);
|
|
2759
3642
|
} else if (entry.isFile()) {
|
|
2760
|
-
const size =
|
|
3643
|
+
const size = fs8.statSync(path8.join(dir, child)).size;
|
|
2761
3644
|
if (size > FOLDER_FILE_BYTES) skipped.push({ path: child, reason: "over 1 MiB" });
|
|
2762
3645
|
else files.push({ path: child, size });
|
|
2763
3646
|
}
|
|
@@ -2810,7 +3693,7 @@ var folderCommand = async (api2, args) => {
|
|
|
2810
3693
|
}
|
|
2811
3694
|
case "push": {
|
|
2812
3695
|
if (dir === void 0) return fail4("usage: mend folder push <name> <dir> [--replace]");
|
|
2813
|
-
if (!
|
|
3696
|
+
if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) {
|
|
2814
3697
|
return fail4(`${dir} is not a directory`);
|
|
2815
3698
|
}
|
|
2816
3699
|
const folder = await folderNamed(api2, name);
|
|
@@ -2825,7 +3708,7 @@ var folderCommand = async (api2, args) => {
|
|
|
2825
3708
|
files: batch.map((file) => ({
|
|
2826
3709
|
path: file.path,
|
|
2827
3710
|
// Read one batch at a time, so a large tree never sits in memory at once.
|
|
2828
|
-
contentsBase64:
|
|
3711
|
+
contentsBase64: fs8.readFileSync(path8.join(dir, file.path)).toString("base64")
|
|
2829
3712
|
})),
|
|
2830
3713
|
// --replace empties the folder with the first batch; the rest add to it.
|
|
2831
3714
|
merge: !(replace && index === 0)
|
|
@@ -3009,8 +3892,8 @@ var operatorCommand = async (api2, tryApi, baseUrl, args) => {
|
|
|
3009
3892
|
import { createHmac, randomBytes } from "node:crypto";
|
|
3010
3893
|
import { readFile } from "node:fs/promises";
|
|
3011
3894
|
import * as net2 from "node:net";
|
|
3012
|
-
import * as
|
|
3013
|
-
import * as
|
|
3895
|
+
import * as os6 from "node:os";
|
|
3896
|
+
import * as path10 from "node:path";
|
|
3014
3897
|
|
|
3015
3898
|
// src/server-docker-volumes.ts
|
|
3016
3899
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -3053,14 +3936,14 @@ var docker = async (runtime, context, args) => {
|
|
|
3053
3936
|
}
|
|
3054
3937
|
};
|
|
3055
3938
|
var listNames = async (runtime, context, kind, project2) => {
|
|
3056
|
-
const
|
|
3939
|
+
const field2 = kind === "container" ? "Names" : "Name";
|
|
3057
3940
|
const output = await docker(runtime, context, [
|
|
3058
3941
|
kind,
|
|
3059
3942
|
"ls",
|
|
3060
3943
|
...kind === "container" ? ["--all"] : [],
|
|
3061
3944
|
...project2 === void 0 ? [] : ["--filter", `label=com.docker.compose.project=${project2}`],
|
|
3062
3945
|
"--format",
|
|
3063
|
-
`{{json .${
|
|
3946
|
+
`{{json .${field2}}}`
|
|
3064
3947
|
]);
|
|
3065
3948
|
if (output._tag === "error") return output;
|
|
3066
3949
|
const names = [];
|
|
@@ -3226,157 +4109,11 @@ var ownership = async (runtime, input, mode) => {
|
|
|
3226
4109
|
var claimServerDockerVolumes = (runtime, input) => ownership(runtime, input, "claim");
|
|
3227
4110
|
var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "verify");
|
|
3228
4111
|
|
|
3229
|
-
// src/server-runtime.ts
|
|
3230
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
3231
|
-
import * as fs6 from "node:fs";
|
|
3232
|
-
import * as path6 from "node:path";
|
|
3233
|
-
var serverProcessDeadlines = {
|
|
3234
|
-
ordinary: 3e4,
|
|
3235
|
-
pull: 10 * 6e4,
|
|
3236
|
-
startup: 3 * 6e4,
|
|
3237
|
-
stop: 9e4,
|
|
3238
|
-
dump: 15 * 6e4,
|
|
3239
|
-
composeWaitSeconds: 120
|
|
3240
|
-
};
|
|
3241
|
-
var runServerProcess = (command, args, parentEnvironment, options = {}) => {
|
|
3242
|
-
const timeoutMs = options?.timeoutMs ?? serverProcessDeadlines.ordinary;
|
|
3243
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
|
|
3244
|
-
return Promise.resolve({
|
|
3245
|
-
status: null,
|
|
3246
|
-
stdout: "",
|
|
3247
|
-
stderr: "",
|
|
3248
|
-
error: "Invalid process timeout"
|
|
3249
|
-
});
|
|
3250
|
-
}
|
|
3251
|
-
if (options.stdoutFile !== void 0 && options.stdout === "inherit") {
|
|
3252
|
-
return Promise.resolve({
|
|
3253
|
-
status: null,
|
|
3254
|
-
stdout: "",
|
|
3255
|
-
stderr: "",
|
|
3256
|
-
error: "Invalid process output options"
|
|
3257
|
-
});
|
|
3258
|
-
}
|
|
3259
|
-
const env = {};
|
|
3260
|
-
for (const key of [
|
|
3261
|
-
"PATH",
|
|
3262
|
-
"HOME",
|
|
3263
|
-
"USER",
|
|
3264
|
-
"LOGNAME",
|
|
3265
|
-
"XDG_CONFIG_HOME",
|
|
3266
|
-
"XDG_RUNTIME_DIR",
|
|
3267
|
-
"DOCKER_CONFIG",
|
|
3268
|
-
"SSH_AUTH_SOCK",
|
|
3269
|
-
"TMPDIR",
|
|
3270
|
-
"TMP",
|
|
3271
|
-
"TEMP"
|
|
3272
|
-
]) {
|
|
3273
|
-
if (parentEnvironment[key] !== void 0) env[key] = parentEnvironment[key];
|
|
3274
|
-
}
|
|
3275
|
-
return new Promise((resolve6) => {
|
|
3276
|
-
let fd;
|
|
3277
|
-
try {
|
|
3278
|
-
if (options.stdoutFile !== void 0) fd = fs6.openSync(options.stdoutFile, "wx", 384);
|
|
3279
|
-
const child = spawn2(command, [...args], {
|
|
3280
|
-
env,
|
|
3281
|
-
detached: process.platform !== "win32",
|
|
3282
|
-
stdio: ["ignore", fd ?? (options.stdout === "inherit" ? "inherit" : "pipe"), "pipe"]
|
|
3283
|
-
});
|
|
3284
|
-
let stdout = "";
|
|
3285
|
-
let stderr = "";
|
|
3286
|
-
let stdoutBytes = 0;
|
|
3287
|
-
let stderrBytes = 0;
|
|
3288
|
-
let failure2;
|
|
3289
|
-
const terminate = () => {
|
|
3290
|
-
if (process.platform === "win32" || child.pid === void 0) {
|
|
3291
|
-
child.kill("SIGKILL");
|
|
3292
|
-
return;
|
|
3293
|
-
}
|
|
3294
|
-
try {
|
|
3295
|
-
process.kill(-child.pid, "SIGKILL");
|
|
3296
|
-
} catch (cause) {
|
|
3297
|
-
if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) {
|
|
3298
|
-
failure2 = "Could not terminate the process group; retain the installation lock until its processes stop.";
|
|
3299
|
-
child.kill("SIGKILL");
|
|
3300
|
-
}
|
|
3301
|
-
}
|
|
3302
|
-
};
|
|
3303
|
-
child.stdout?.on("data", (chunk) => {
|
|
3304
|
-
stdoutBytes += chunk.length;
|
|
3305
|
-
if (stdoutBytes <= 4 * 1024 * 1024) stdout += chunk.toString();
|
|
3306
|
-
else {
|
|
3307
|
-
failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
|
|
3308
|
-
terminate();
|
|
3309
|
-
}
|
|
3310
|
-
});
|
|
3311
|
-
child.stderr?.on("data", (chunk) => {
|
|
3312
|
-
stderrBytes += chunk.length;
|
|
3313
|
-
if (stderrBytes <= 64 * 1024) stderr += chunk.toString();
|
|
3314
|
-
else {
|
|
3315
|
-
failure2 ??= "Process output exceeded the capture limit. Request fewer log lines.";
|
|
3316
|
-
terminate();
|
|
3317
|
-
}
|
|
3318
|
-
});
|
|
3319
|
-
const timer = setTimeout(() => {
|
|
3320
|
-
failure2 ??= `Process timed out after ${timeoutMs}ms`;
|
|
3321
|
-
terminate();
|
|
3322
|
-
}, timeoutMs);
|
|
3323
|
-
child.once("error", (error) => {
|
|
3324
|
-
failure2 ??= error.message;
|
|
3325
|
-
});
|
|
3326
|
-
child.once("close", (status) => {
|
|
3327
|
-
globalThis.clearTimeout(timer);
|
|
3328
|
-
try {
|
|
3329
|
-
if (fd !== void 0) fs6.fsyncSync(fd);
|
|
3330
|
-
} catch {
|
|
3331
|
-
failure2 ??= "Could not durably write process output.";
|
|
3332
|
-
} finally {
|
|
3333
|
-
try {
|
|
3334
|
-
if (fd !== void 0) fs6.closeSync(fd);
|
|
3335
|
-
} catch {
|
|
3336
|
-
failure2 ??= "Could not close process output file.";
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
resolve6({
|
|
3340
|
-
status: failure2 === void 0 ? status : null,
|
|
3341
|
-
stdout,
|
|
3342
|
-
stderr,
|
|
3343
|
-
...failure2 === void 0 ? {} : { error: failure2 }
|
|
3344
|
-
});
|
|
3345
|
-
});
|
|
3346
|
-
} catch (cause) {
|
|
3347
|
-
try {
|
|
3348
|
-
if (fd !== void 0) fs6.closeSync(fd);
|
|
3349
|
-
} catch {
|
|
3350
|
-
}
|
|
3351
|
-
resolve6({
|
|
3352
|
-
status: null,
|
|
3353
|
-
stdout: "",
|
|
3354
|
-
stderr: "",
|
|
3355
|
-
error: cause instanceof Error ? cause.message : "Could not start process."
|
|
3356
|
-
});
|
|
3357
|
-
}
|
|
3358
|
-
});
|
|
3359
|
-
};
|
|
3360
|
-
var serverComposeArgs = (installation, command) => [
|
|
3361
|
-
"--context",
|
|
3362
|
-
installation.dockerContext,
|
|
3363
|
-
"compose",
|
|
3364
|
-
"--project-name",
|
|
3365
|
-
"mend",
|
|
3366
|
-
"--project-directory",
|
|
3367
|
-
installation.directory,
|
|
3368
|
-
"--env-file",
|
|
3369
|
-
path6.join(installation.directory, "server.env"),
|
|
3370
|
-
"-f",
|
|
3371
|
-
path6.join(installation.directory, "compose.yaml"),
|
|
3372
|
-
...command
|
|
3373
|
-
];
|
|
3374
|
-
|
|
3375
4112
|
// src/server-store.ts
|
|
3376
4113
|
import { randomUUID } from "node:crypto";
|
|
3377
|
-
import * as
|
|
3378
|
-
import * as
|
|
3379
|
-
import * as
|
|
4114
|
+
import * as fs9 from "node:fs";
|
|
4115
|
+
import * as os5 from "node:os";
|
|
4116
|
+
import * as path9 from "node:path";
|
|
3380
4117
|
var ServerStoreError = class extends Error {
|
|
3381
4118
|
_tag = "ServerStoreError";
|
|
3382
4119
|
};
|
|
@@ -3397,28 +4134,28 @@ var attempt = (operation) => {
|
|
|
3397
4134
|
var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
|
|
3398
4135
|
var readOptional = (file) => {
|
|
3399
4136
|
try {
|
|
3400
|
-
return
|
|
4137
|
+
return fs9.readFileSync(file, "utf8");
|
|
3401
4138
|
} catch (cause) {
|
|
3402
4139
|
if (hasCode(cause, "ENOENT")) return null;
|
|
3403
4140
|
throw cause;
|
|
3404
4141
|
}
|
|
3405
4142
|
};
|
|
3406
4143
|
var syncDirectory = (directory) => {
|
|
3407
|
-
const fd =
|
|
4144
|
+
const fd = fs9.openSync(directory, "r");
|
|
3408
4145
|
try {
|
|
3409
|
-
|
|
4146
|
+
fs9.fsyncSync(fd);
|
|
3410
4147
|
} finally {
|
|
3411
|
-
|
|
4148
|
+
fs9.closeSync(fd);
|
|
3412
4149
|
}
|
|
3413
4150
|
};
|
|
3414
4151
|
var writeDurable = (file, content, mode = 384) => {
|
|
3415
|
-
const fd =
|
|
4152
|
+
const fd = fs9.openSync(file, "wx", mode);
|
|
3416
4153
|
try {
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
4154
|
+
fs9.writeFileSync(fd, content, "utf8");
|
|
4155
|
+
fs9.fchmodSync(fd, mode);
|
|
4156
|
+
fs9.fsyncSync(fd);
|
|
3420
4157
|
} finally {
|
|
3421
|
-
|
|
4158
|
+
fs9.closeSync(fd);
|
|
3422
4159
|
}
|
|
3423
4160
|
};
|
|
3424
4161
|
var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
|
|
@@ -3438,11 +4175,11 @@ var parseLockOwner = (raw) => {
|
|
|
3438
4175
|
};
|
|
3439
4176
|
var describeOwner = (lockDir) => {
|
|
3440
4177
|
try {
|
|
3441
|
-
const raw = JSON.parse(
|
|
4178
|
+
const raw = JSON.parse(fs9.readFileSync(path9.join(lockDir, "owner.json"), "utf8"));
|
|
3442
4179
|
const owner = parseLockOwner(raw);
|
|
3443
4180
|
if (owner === null) return "owner metadata is invalid";
|
|
3444
4181
|
const label = `PID ${owner.pid} on ${owner.hostname}`;
|
|
3445
|
-
if (owner.hostname !==
|
|
4182
|
+
if (owner.hostname !== os5.hostname()) return label;
|
|
3446
4183
|
try {
|
|
3447
4184
|
process.kill(owner.pid, 0);
|
|
3448
4185
|
return `${label} is still live`;
|
|
@@ -3458,28 +4195,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
|
|
|
3458
4195
|
);
|
|
3459
4196
|
var acquireLock = (configDir, create) => {
|
|
3460
4197
|
if (create) {
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
} else if (!
|
|
4198
|
+
fs9.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
4199
|
+
fs9.chmodSync(configDir, 448);
|
|
4200
|
+
} else if (!fs9.existsSync(configDir)) {
|
|
3464
4201
|
throw new ServerStoreError(
|
|
3465
4202
|
"No Mend server is configured. Run mend server setup explicitly to install one."
|
|
3466
4203
|
);
|
|
3467
4204
|
}
|
|
3468
|
-
const lockDir =
|
|
4205
|
+
const lockDir = path9.join(configDir, "server.lock");
|
|
3469
4206
|
try {
|
|
3470
|
-
|
|
4207
|
+
fs9.mkdirSync(lockDir, { mode: 448 });
|
|
3471
4208
|
} catch (cause) {
|
|
3472
4209
|
if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
|
|
3473
4210
|
throw cause;
|
|
3474
4211
|
}
|
|
3475
|
-
const owned =
|
|
3476
|
-
const ownerFile =
|
|
3477
|
-
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() })}
|
|
3478
4215
|
`;
|
|
3479
4216
|
let open = true;
|
|
3480
4217
|
const assertOwned = () => {
|
|
3481
|
-
const current =
|
|
3482
|
-
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) {
|
|
3483
4220
|
throw new ServerStoreError(
|
|
3484
4221
|
"Server lock ownership was lost. Stop and check for another server command before retrying."
|
|
3485
4222
|
);
|
|
@@ -3495,8 +4232,8 @@ var acquireLock = (configDir, create) => {
|
|
|
3495
4232
|
release: () => {
|
|
3496
4233
|
const result = attempt(() => {
|
|
3497
4234
|
assertOwned();
|
|
3498
|
-
|
|
3499
|
-
|
|
4235
|
+
fs9.unlinkSync(ownerFile);
|
|
4236
|
+
fs9.rmdirSync(lockDir);
|
|
3500
4237
|
});
|
|
3501
4238
|
open = false;
|
|
3502
4239
|
return result;
|
|
@@ -3505,7 +4242,7 @@ var acquireLock = (configDir, create) => {
|
|
|
3505
4242
|
};
|
|
3506
4243
|
var readIdentity = (paths) => {
|
|
3507
4244
|
const identity = readOptional(paths.identity);
|
|
3508
|
-
if (identity === null && (
|
|
4245
|
+
if (identity === null && (fs9.existsSync(paths.active) || fs9.existsSync(paths.generations) && fs9.readdirSync(paths.generations).length > 0)) {
|
|
3509
4246
|
throw new ServerStoreError(
|
|
3510
4247
|
"Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
|
|
3511
4248
|
);
|
|
@@ -3515,7 +4252,7 @@ var readIdentity = (paths) => {
|
|
|
3515
4252
|
var readActive = (paths) => {
|
|
3516
4253
|
let target;
|
|
3517
4254
|
try {
|
|
3518
|
-
target =
|
|
4255
|
+
target = fs9.readlinkSync(paths.active);
|
|
3519
4256
|
} catch (cause) {
|
|
3520
4257
|
if (hasCode(cause, "ENOENT")) return null;
|
|
3521
4258
|
throw cause;
|
|
@@ -3525,13 +4262,13 @@ var readActive = (paths) => {
|
|
|
3525
4262
|
"Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
|
|
3526
4263
|
);
|
|
3527
4264
|
}
|
|
3528
|
-
const directory =
|
|
4265
|
+
const directory = path9.join(paths.configDir, target);
|
|
3529
4266
|
const files = {
|
|
3530
|
-
identity:
|
|
3531
|
-
config:
|
|
3532
|
-
env:
|
|
3533
|
-
compose:
|
|
3534
|
-
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")
|
|
3535
4272
|
};
|
|
3536
4273
|
if (readIdentity(paths) !== files.identity) {
|
|
3537
4274
|
throw new ServerStoreError(
|
|
@@ -3541,20 +4278,20 @@ var readActive = (paths) => {
|
|
|
3541
4278
|
return { directory, files };
|
|
3542
4279
|
};
|
|
3543
4280
|
var publishIdentity = (paths, identity) => {
|
|
3544
|
-
const temporary =
|
|
4281
|
+
const temporary = path9.join(paths.configDir, `.identity-${randomUUID()}`);
|
|
3545
4282
|
writeDurable(temporary, identity);
|
|
3546
|
-
|
|
4283
|
+
fs9.linkSync(temporary, paths.identity);
|
|
3547
4284
|
syncDirectory(paths.configDir);
|
|
3548
|
-
|
|
4285
|
+
fs9.unlinkSync(temporary);
|
|
3549
4286
|
};
|
|
3550
4287
|
var prepareGeneration = (paths, files) => {
|
|
3551
|
-
|
|
4288
|
+
fs9.mkdirSync(paths.generations, { recursive: true, mode: 448 });
|
|
3552
4289
|
const generationName = `gen-${randomUUID()}`;
|
|
3553
|
-
const directory =
|
|
3554
|
-
|
|
4290
|
+
const directory = path9.join(paths.generations, generationName);
|
|
4291
|
+
fs9.mkdirSync(directory, { mode: 448 });
|
|
3555
4292
|
for (const key of fileKeys) {
|
|
3556
4293
|
writeDurable(
|
|
3557
|
-
|
|
4294
|
+
path9.join(directory, fileNames[key]),
|
|
3558
4295
|
files[key],
|
|
3559
4296
|
key === "postgresInit" ? 493 : 384
|
|
3560
4297
|
);
|
|
@@ -3565,18 +4302,18 @@ var prepareGeneration = (paths, files) => {
|
|
|
3565
4302
|
return { directory, files };
|
|
3566
4303
|
};
|
|
3567
4304
|
var activateGeneration = (paths, generation) => {
|
|
3568
|
-
const relative4 =
|
|
3569
|
-
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")) {
|
|
3570
4307
|
throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
|
|
3571
4308
|
}
|
|
3572
4309
|
for (const key of fileKeys) {
|
|
3573
|
-
if (
|
|
4310
|
+
if (fs9.readFileSync(path9.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
|
|
3574
4311
|
throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
|
|
3575
4312
|
}
|
|
3576
4313
|
}
|
|
3577
|
-
const pointer =
|
|
3578
|
-
|
|
3579
|
-
|
|
4314
|
+
const pointer = path9.join(paths.configDir, `.active-${randomUUID()}`);
|
|
4315
|
+
fs9.symlinkSync(relative4, pointer);
|
|
4316
|
+
fs9.renameSync(pointer, paths.active);
|
|
3580
4317
|
syncDirectory(paths.configDir);
|
|
3581
4318
|
};
|
|
3582
4319
|
var prepareFiles = (paths, files) => {
|
|
@@ -3585,7 +4322,7 @@ var prepareFiles = (paths, files) => {
|
|
|
3585
4322
|
throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
|
|
3586
4323
|
}
|
|
3587
4324
|
const active = readActive(paths);
|
|
3588
|
-
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)
|
|
3589
4326
|
return active;
|
|
3590
4327
|
if (identity === null) publishIdentity(paths, files.identity);
|
|
3591
4328
|
return prepareGeneration(paths, files);
|
|
@@ -3598,9 +4335,9 @@ var commitGeneration = (paths, files) => {
|
|
|
3598
4335
|
var createStore = (configDir, lock) => {
|
|
3599
4336
|
const paths = {
|
|
3600
4337
|
configDir,
|
|
3601
|
-
identity:
|
|
3602
|
-
generations:
|
|
3603
|
-
active:
|
|
4338
|
+
identity: path9.join(configDir, "identity.env"),
|
|
4339
|
+
generations: path9.join(configDir, "generations"),
|
|
4340
|
+
active: path9.join(configDir, "active")
|
|
3604
4341
|
};
|
|
3605
4342
|
const whileOwned = (operation) => attempt(() => {
|
|
3606
4343
|
lock.assertOwned();
|
|
@@ -3613,13 +4350,13 @@ var createStore = (configDir, lock) => {
|
|
|
3613
4350
|
prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
|
|
3614
4351
|
activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
|
|
3615
4352
|
createBackup: (previous, target) => whileOwned(() => {
|
|
3616
|
-
const backups =
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
const directory =
|
|
3620
|
-
|
|
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 });
|
|
3621
4358
|
writeDurable(
|
|
3622
|
-
|
|
4359
|
+
path9.join(directory, "recovery.json"),
|
|
3623
4360
|
`${JSON.stringify(
|
|
3624
4361
|
{
|
|
3625
4362
|
previousGeneration: previous.directory,
|
|
@@ -3635,21 +4372,21 @@ var createStore = (configDir, lock) => {
|
|
|
3635
4372
|
syncDirectory(directory);
|
|
3636
4373
|
syncDirectory(backups);
|
|
3637
4374
|
syncDirectory(configDir);
|
|
3638
|
-
const partialFile =
|
|
4375
|
+
const partialFile = path9.join(directory, "database.sql.partial");
|
|
3639
4376
|
return {
|
|
3640
4377
|
directory,
|
|
3641
4378
|
partialFile,
|
|
3642
4379
|
complete: () => whileOwned(() => {
|
|
3643
|
-
if (
|
|
4380
|
+
if (fs9.statSync(partialFile).size === 0)
|
|
3644
4381
|
throw new ServerStoreError("Database backup is empty; refusing target startup.");
|
|
3645
|
-
const fd =
|
|
4382
|
+
const fd = fs9.openSync(partialFile, "r");
|
|
3646
4383
|
try {
|
|
3647
|
-
|
|
3648
|
-
|
|
4384
|
+
fs9.fchmodSync(fd, 384);
|
|
4385
|
+
fs9.fsyncSync(fd);
|
|
3649
4386
|
} finally {
|
|
3650
|
-
|
|
4387
|
+
fs9.closeSync(fd);
|
|
3651
4388
|
}
|
|
3652
|
-
|
|
4389
|
+
fs9.renameSync(partialFile, path9.join(directory, "database.sql"));
|
|
3653
4390
|
syncDirectory(directory);
|
|
3654
4391
|
})
|
|
3655
4392
|
};
|
|
@@ -3658,7 +4395,7 @@ var createStore = (configDir, lock) => {
|
|
|
3658
4395
|
};
|
|
3659
4396
|
var refuseFlatLayout = (configDir) => {
|
|
3660
4397
|
if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
|
|
3661
|
-
(name) =>
|
|
4398
|
+
(name) => fs9.existsSync(path9.join(configDir, name))
|
|
3662
4399
|
)) {
|
|
3663
4400
|
throw new ServerStoreError(
|
|
3664
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."
|
|
@@ -3666,7 +4403,7 @@ var refuseFlatLayout = (configDir) => {
|
|
|
3666
4403
|
}
|
|
3667
4404
|
};
|
|
3668
4405
|
var withServerStore = async (directory, operation, options = {}) => {
|
|
3669
|
-
const configDir =
|
|
4406
|
+
const configDir = path9.resolve(directory);
|
|
3670
4407
|
let lock;
|
|
3671
4408
|
let result;
|
|
3672
4409
|
try {
|
|
@@ -3676,16 +4413,16 @@ var withServerStore = async (directory, operation, options = {}) => {
|
|
|
3676
4413
|
} catch (cause) {
|
|
3677
4414
|
result = { _tag: "error", error: storeError(cause) };
|
|
3678
4415
|
} finally {
|
|
3679
|
-
const
|
|
3680
|
-
if (
|
|
4416
|
+
const release2 = lock?.release();
|
|
4417
|
+
if (release2?._tag === "error") result = release2;
|
|
3681
4418
|
}
|
|
3682
4419
|
return result;
|
|
3683
4420
|
};
|
|
3684
4421
|
|
|
3685
4422
|
// src/version.ts
|
|
3686
|
-
import * as
|
|
4423
|
+
import * as fs10 from "node:fs";
|
|
3687
4424
|
var cliVersion = () => {
|
|
3688
|
-
const raw =
|
|
4425
|
+
const raw = fs10.readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
3689
4426
|
const parsed = JSON.parse(raw);
|
|
3690
4427
|
if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
|
|
3691
4428
|
const version = parsed.version;
|
|
@@ -3855,7 +4592,7 @@ var parseHttpOrigin = (input, label) => {
|
|
|
3855
4592
|
return parsed.origin;
|
|
3856
4593
|
};
|
|
3857
4594
|
var isLoopbackBind = (bind) => bind === "127.0.0.1" || bind === "::1" || bind.startsWith("127.");
|
|
3858
|
-
var isLoopbackHost = (
|
|
4595
|
+
var isLoopbackHost = (hostname7) => hostname7 === "localhost" || hostname7 === "::1" || hostname7 === "[::1]" || hostname7.startsWith("127.");
|
|
3859
4596
|
var resolveAppUrl = (existing, options, appPort) => {
|
|
3860
4597
|
const oldLocalUrl = existing === null ? null : `http://localhost:${existing.appPort}`;
|
|
3861
4598
|
const portChangedOnLocalhost = options.appPort !== void 0 && existing?.appUrl === oldLocalUrl;
|
|
@@ -4317,6 +5054,30 @@ var readServerInstallation = (store) => {
|
|
|
4317
5054
|
};
|
|
4318
5055
|
}
|
|
4319
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
|
+
};
|
|
4320
5081
|
var persistSetup = (store, config, secrets, assets) => storeValue(
|
|
4321
5082
|
store.prepare({
|
|
4322
5083
|
identity: renderIdentity(secrets),
|
|
@@ -4364,7 +5125,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
|
|
|
4364
5125
|
return version;
|
|
4365
5126
|
};
|
|
4366
5127
|
var parseDockerSocketPath = (socket) => {
|
|
4367
|
-
if (!
|
|
5128
|
+
if (!path10.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
|
|
4368
5129
|
throw setupError(
|
|
4369
5130
|
"--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
|
|
4370
5131
|
);
|
|
@@ -4390,8 +5151,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
|
|
|
4390
5151
|
if (options.assetsDir !== void 0) {
|
|
4391
5152
|
try {
|
|
4392
5153
|
const [compose2, postgresInit2] = await Promise.all([
|
|
4393
|
-
readFile(
|
|
4394
|
-
readFile(
|
|
5154
|
+
readFile(path10.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
|
|
5155
|
+
readFile(path10.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
|
|
4395
5156
|
]);
|
|
4396
5157
|
validateComposeAsset(compose2);
|
|
4397
5158
|
validatePostgresAsset(postgresInit2);
|
|
@@ -4904,10 +5665,10 @@ var manageServer = async (command, args, runtime, store) => {
|
|
|
4904
5665
|
};
|
|
4905
5666
|
var nodeServerRuntime = () => {
|
|
4906
5667
|
const environment = { ...process.env };
|
|
4907
|
-
const home =
|
|
4908
|
-
const configHome = environment["XDG_CONFIG_HOME"] ??
|
|
5668
|
+
const home = os6.homedir();
|
|
5669
|
+
const configHome = environment["XDG_CONFIG_HOME"] ?? path10.join(home, ".config");
|
|
4909
5670
|
return {
|
|
4910
|
-
configDir:
|
|
5671
|
+
configDir: path10.join(configHome, "mend"),
|
|
4911
5672
|
platform: process.platform,
|
|
4912
5673
|
cliVersion: cliVersion(),
|
|
4913
5674
|
run: (command, args, options) => runServerProcess(command, args, environment, options),
|
|
@@ -4924,7 +5685,7 @@ var nodeServerRuntime = () => {
|
|
|
4924
5685
|
}
|
|
4925
5686
|
},
|
|
4926
5687
|
randomBytes,
|
|
4927
|
-
sleep: (milliseconds) => new Promise((
|
|
5688
|
+
sleep: (milliseconds) => new Promise((resolve7) => setTimeout(resolve7, milliseconds)),
|
|
4928
5689
|
writeLine: (line) => process.stdout.write(`${line}
|
|
4929
5690
|
`)
|
|
4930
5691
|
};
|
|
@@ -5146,9 +5907,9 @@ var touchesSessionServices = (payload, sessionId) => {
|
|
|
5146
5907
|
return false;
|
|
5147
5908
|
}
|
|
5148
5909
|
if (typeof parsed !== "object" || parsed === null) return false;
|
|
5149
|
-
const
|
|
5910
|
+
const type2 = "type" in parsed ? parsed.type : void 0;
|
|
5150
5911
|
const session = "sessionId" in parsed ? parsed.sessionId : void 0;
|
|
5151
|
-
return (
|
|
5912
|
+
return (type2 === "session-process" || type2 === "session") && session === sessionId;
|
|
5152
5913
|
};
|
|
5153
5914
|
var pumpConnection = (socket, url) => {
|
|
5154
5915
|
socket.pause();
|
|
@@ -5174,7 +5935,7 @@ var pumpConnection = (socket, url) => {
|
|
|
5174
5935
|
() => socket.destroy()
|
|
5175
5936
|
);
|
|
5176
5937
|
};
|
|
5177
|
-
var listenLocal = (server, port, fallback) => new Promise((
|
|
5938
|
+
var listenLocal = (server, port, fallback) => new Promise((resolve7, reject) => {
|
|
5178
5939
|
const attempt2 = (candidate, lastTry) => {
|
|
5179
5940
|
const onError = (error) => {
|
|
5180
5941
|
if (!lastTry) {
|
|
@@ -5187,7 +5948,7 @@ var listenLocal = (server, port, fallback) => new Promise((resolve6, reject) =>
|
|
|
5187
5948
|
server.listen(candidate, "127.0.0.1", () => {
|
|
5188
5949
|
server.removeListener("error", onError);
|
|
5189
5950
|
const address = server.address();
|
|
5190
|
-
|
|
5951
|
+
resolve7(typeof address === "object" && address !== null ? address.port : candidate);
|
|
5191
5952
|
});
|
|
5192
5953
|
};
|
|
5193
5954
|
attempt2(port, !fallback || port === 0);
|
|
@@ -5216,7 +5977,7 @@ var serverEvents = (config) => async (handlers, signal) => {
|
|
|
5216
5977
|
if (signal.aborted) return;
|
|
5217
5978
|
}
|
|
5218
5979
|
connected = true;
|
|
5219
|
-
await new Promise((
|
|
5980
|
+
await new Promise((resolve7) => setTimeout(resolve7, 2e3));
|
|
5220
5981
|
}
|
|
5221
5982
|
};
|
|
5222
5983
|
var createServiceTunnels = (options) => {
|
|
@@ -5359,8 +6120,8 @@ var createServiceTunnels = (options) => {
|
|
|
5359
6120
|
};
|
|
5360
6121
|
|
|
5361
6122
|
// src/skills.ts
|
|
5362
|
-
import * as
|
|
5363
|
-
import * as
|
|
6123
|
+
import * as fs11 from "node:fs";
|
|
6124
|
+
import * as path11 from "node:path";
|
|
5364
6125
|
var DEFAULT_SKILLS_DIR = ".agents/skills";
|
|
5365
6126
|
var MAX_SKILL_FILES = 64;
|
|
5366
6127
|
var MAX_SKILL_FILE_BYTES = 512 * 1024;
|
|
@@ -5383,12 +6144,12 @@ var readFrontmatterDescription = (contents) => {
|
|
|
5383
6144
|
var walkFiles = (root) => {
|
|
5384
6145
|
const found = [];
|
|
5385
6146
|
const visit = (dir) => {
|
|
5386
|
-
for (const entry of
|
|
6147
|
+
for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
|
|
5387
6148
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
5388
|
-
const absolute =
|
|
6149
|
+
const absolute = path11.join(dir, entry.name);
|
|
5389
6150
|
let stat;
|
|
5390
6151
|
try {
|
|
5391
|
-
stat =
|
|
6152
|
+
stat = fs11.statSync(absolute);
|
|
5392
6153
|
} catch {
|
|
5393
6154
|
continue;
|
|
5394
6155
|
}
|
|
@@ -5402,7 +6163,7 @@ var walkFiles = (root) => {
|
|
|
5402
6163
|
var scanSkillLibrary = (root) => {
|
|
5403
6164
|
let entries;
|
|
5404
6165
|
try {
|
|
5405
|
-
entries =
|
|
6166
|
+
entries = fs11.readdirSync(root, { withFileTypes: true });
|
|
5406
6167
|
} catch {
|
|
5407
6168
|
return { error: `${root} is not a readable directory` };
|
|
5408
6169
|
}
|
|
@@ -5410,23 +6171,23 @@ var scanSkillLibrary = (root) => {
|
|
|
5410
6171
|
const notes = [];
|
|
5411
6172
|
for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
|
|
5412
6173
|
if (entry.name.startsWith(".")) continue;
|
|
5413
|
-
const skillDir =
|
|
6174
|
+
const skillDir = path11.join(root, entry.name);
|
|
5414
6175
|
let stat;
|
|
5415
6176
|
try {
|
|
5416
|
-
stat =
|
|
6177
|
+
stat = fs11.statSync(skillDir);
|
|
5417
6178
|
} catch {
|
|
5418
6179
|
continue;
|
|
5419
6180
|
}
|
|
5420
6181
|
if (!stat.isDirectory()) continue;
|
|
5421
|
-
if (!
|
|
6182
|
+
if (!fs11.existsSync(path11.join(skillDir, "SKILL.md"))) {
|
|
5422
6183
|
notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
|
|
5423
6184
|
continue;
|
|
5424
6185
|
}
|
|
5425
6186
|
const files = [];
|
|
5426
6187
|
let bytes = 0;
|
|
5427
6188
|
for (const absolute of walkFiles(skillDir)) {
|
|
5428
|
-
const relative4 =
|
|
5429
|
-
const buffer =
|
|
6189
|
+
const relative4 = path11.relative(skillDir, absolute).split(path11.sep).join("/");
|
|
6190
|
+
const buffer = fs11.readFileSync(absolute);
|
|
5430
6191
|
if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
|
|
5431
6192
|
notes.push({ skill: entry.name, message: `${relative4} is over 512KB \u2014 skipped` });
|
|
5432
6193
|
continue;
|
|
@@ -5461,16 +6222,16 @@ var scanSkillLibrary = (root) => {
|
|
|
5461
6222
|
};
|
|
5462
6223
|
|
|
5463
6224
|
// src/ssh-setup.ts
|
|
5464
|
-
import * as
|
|
5465
|
-
import * as
|
|
6225
|
+
import * as os8 from "node:os";
|
|
6226
|
+
import * as path13 from "node:path";
|
|
5466
6227
|
|
|
5467
6228
|
// ../../packages/workspace-ssh/src/index.ts
|
|
5468
6229
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5469
6230
|
import { createHash as createHash3 } from "node:crypto";
|
|
5470
|
-
import * as
|
|
6231
|
+
import * as fs12 from "node:fs";
|
|
5471
6232
|
import { isIP as isIP2 } from "node:net";
|
|
5472
|
-
import * as
|
|
5473
|
-
import * as
|
|
6233
|
+
import * as os7 from "node:os";
|
|
6234
|
+
import * as path12 from "node:path";
|
|
5474
6235
|
var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
|
|
5475
6236
|
var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
|
|
5476
6237
|
var WorkspaceSshError = class extends Error {
|
|
@@ -5487,16 +6248,16 @@ var WorkspaceSshError = class extends Error {
|
|
|
5487
6248
|
};
|
|
5488
6249
|
var success = (value) => ({ ok: true, value });
|
|
5489
6250
|
var failure = (error) => ({ ok: false, error });
|
|
5490
|
-
var stripIpv6Brackets = (
|
|
6251
|
+
var stripIpv6Brackets = (hostname7) => hostname7.startsWith("[") && hostname7.endsWith("]") ? hostname7.slice(1, -1) : hostname7;
|
|
5491
6252
|
var hashIdentity = (identity) => createHash3("sha256").update(identity).digest("hex").slice(0, 24);
|
|
5492
|
-
var aliasLabel = (
|
|
5493
|
-
const label =
|
|
6253
|
+
var aliasLabel = (hostname7) => {
|
|
6254
|
+
const label = hostname7.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
5494
6255
|
return label === "" ? "server" : label;
|
|
5495
6256
|
};
|
|
5496
6257
|
var parseHostOverride = (raw) => {
|
|
5497
|
-
const
|
|
5498
|
-
if (
|
|
5499
|
-
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)) {
|
|
5500
6261
|
return failure(
|
|
5501
6262
|
new WorkspaceSshError(
|
|
5502
6263
|
"target",
|
|
@@ -5504,7 +6265,7 @@ var parseHostOverride = (raw) => {
|
|
|
5504
6265
|
)
|
|
5505
6266
|
);
|
|
5506
6267
|
}
|
|
5507
|
-
if (/^[^:]+:\d+$/.test(
|
|
6268
|
+
if (/^[^:]+:\d+$/.test(hostname7)) {
|
|
5508
6269
|
return failure(
|
|
5509
6270
|
new WorkspaceSshError(
|
|
5510
6271
|
"target",
|
|
@@ -5512,11 +6273,11 @@ var parseHostOverride = (raw) => {
|
|
|
5512
6273
|
)
|
|
5513
6274
|
);
|
|
5514
6275
|
}
|
|
5515
|
-
if ((
|
|
6276
|
+
if ((hostname7.startsWith("[") || hostname7.endsWith("]")) && !/^\[[^\]]+\]$/.test(hostname7)) {
|
|
5516
6277
|
return failure(new WorkspaceSshError("target", "SSH hostname contains invalid IPv6 brackets."));
|
|
5517
6278
|
}
|
|
5518
|
-
const unbracketed = stripIpv6Brackets(
|
|
5519
|
-
if ((
|
|
6279
|
+
const unbracketed = stripIpv6Brackets(hostname7);
|
|
6280
|
+
if ((hostname7.startsWith("[") || unbracketed.includes(":")) && isIP2(unbracketed) !== 6) {
|
|
5520
6281
|
return failure(
|
|
5521
6282
|
new WorkspaceSshError("target", "SSH hostname contains an invalid IPv6 address.")
|
|
5522
6283
|
);
|
|
@@ -5557,8 +6318,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
|
|
|
5557
6318
|
var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
|
|
5558
6319
|
var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
|
|
5559
6320
|
var parseIdentityPath = (value) => {
|
|
5560
|
-
const resolved =
|
|
5561
|
-
value.startsWith("~/") ?
|
|
6321
|
+
const resolved = path12.resolve(
|
|
6322
|
+
value.startsWith("~/") ? path12.join(os7.homedir(), value.slice(2)) : value
|
|
5562
6323
|
);
|
|
5563
6324
|
if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
|
|
5564
6325
|
return failure(
|
|
@@ -5714,7 +6475,7 @@ var agentKeys = () => {
|
|
|
5714
6475
|
};
|
|
5715
6476
|
var readText = (file) => {
|
|
5716
6477
|
try {
|
|
5717
|
-
return success(
|
|
6478
|
+
return success(fs12.readFileSync(file, "utf8"));
|
|
5718
6479
|
} catch (cause) {
|
|
5719
6480
|
const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
|
|
5720
6481
|
return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
|
|
@@ -5772,19 +6533,19 @@ var readRequiredKey = (keyPath, source) => {
|
|
|
5772
6533
|
var readOptionalKey = (keyPath) => {
|
|
5773
6534
|
const parsed = parseIdentityPath(keyPath);
|
|
5774
6535
|
if (!parsed.ok) return parsed;
|
|
5775
|
-
if (!
|
|
6536
|
+
if (!fs12.existsSync(parsed.value) && !fs12.existsSync(publicKeyPath(parsed.value)))
|
|
5776
6537
|
return success(null);
|
|
5777
6538
|
return readRequiredKey(parsed.value, "existing");
|
|
5778
6539
|
};
|
|
5779
6540
|
var pinAgentKey = (configHome, publicKey) => {
|
|
5780
|
-
const identityFile =
|
|
6541
|
+
const identityFile = path12.resolve(
|
|
5781
6542
|
configHome,
|
|
5782
6543
|
"ssh",
|
|
5783
6544
|
`agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
|
|
5784
6545
|
);
|
|
5785
6546
|
try {
|
|
5786
|
-
|
|
5787
|
-
|
|
6547
|
+
fs12.mkdirSync(path12.dirname(identityFile), { recursive: true, mode: 448 });
|
|
6548
|
+
fs12.writeFileSync(identityFile, `${publicKey}
|
|
5788
6549
|
`, { mode: 384 });
|
|
5789
6550
|
return success({ publicKey, identityFile, source: "agent" });
|
|
5790
6551
|
} catch (cause) {
|
|
@@ -5795,15 +6556,15 @@ var pinAgentKey = (configHome, publicKey) => {
|
|
|
5795
6556
|
};
|
|
5796
6557
|
var generateDedicatedKey = (privatePath) => {
|
|
5797
6558
|
try {
|
|
5798
|
-
|
|
6559
|
+
fs12.mkdirSync(path12.dirname(privatePath), { recursive: true, mode: 448 });
|
|
5799
6560
|
} catch (cause) {
|
|
5800
6561
|
return failure(
|
|
5801
|
-
new WorkspaceSshError("key", `Could not create ${
|
|
6562
|
+
new WorkspaceSshError("key", `Could not create ${path12.dirname(privatePath)}.`, cause)
|
|
5802
6563
|
);
|
|
5803
6564
|
}
|
|
5804
6565
|
const generated = spawnSync3(
|
|
5805
6566
|
"ssh-keygen",
|
|
5806
|
-
["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${
|
|
6567
|
+
["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os7.hostname()}`],
|
|
5807
6568
|
{ encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
|
|
5808
6569
|
);
|
|
5809
6570
|
if (generated.status !== 0) {
|
|
@@ -5827,7 +6588,7 @@ var generateDedicatedKey = (privatePath) => {
|
|
|
5827
6588
|
return fresh;
|
|
5828
6589
|
};
|
|
5829
6590
|
var dedicatedKey = (configHome, create) => {
|
|
5830
|
-
const privatePath =
|
|
6591
|
+
const privatePath = path12.join(configHome, "ssh", "id_ed25519");
|
|
5831
6592
|
const existing = readOptionalKey(privatePath);
|
|
5832
6593
|
if (existing.ok === false) return failure(existing.error);
|
|
5833
6594
|
if (existing.value !== null || !create) return existing;
|
|
@@ -5875,10 +6636,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
|
|
|
5875
6636
|
const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
|
|
5876
6637
|
if (reconciled.ok === false) return failure(reconciled.error);
|
|
5877
6638
|
try {
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
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);
|
|
5882
6643
|
return success(void 0);
|
|
5883
6644
|
} catch (cause) {
|
|
5884
6645
|
return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
|
|
@@ -5891,7 +6652,7 @@ var dim5 = ansi("2");
|
|
|
5891
6652
|
var green5 = ansi("32");
|
|
5892
6653
|
var warn = ansi("33");
|
|
5893
6654
|
var say5 = (line) => console.log(line);
|
|
5894
|
-
var sshConfigPath = () =>
|
|
6655
|
+
var sshConfigPath = () => path13.join(os8.homedir(), ".ssh", "config");
|
|
5895
6656
|
var flagValue = (args, flag) => {
|
|
5896
6657
|
const index = args.indexOf(flag);
|
|
5897
6658
|
return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
|
|
@@ -5961,7 +6722,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
|
|
|
5961
6722
|
if (picked.value === null) return showFailure("No workspace SSH key is available.");
|
|
5962
6723
|
const registered = await api2("POST", "/workspace-ssh/keys", {
|
|
5963
6724
|
publicKey: picked.value.publicKey,
|
|
5964
|
-
name:
|
|
6725
|
+
name: os8.hostname()
|
|
5965
6726
|
});
|
|
5966
6727
|
const sourceLabel = {
|
|
5967
6728
|
explicit: "from --key",
|
|
@@ -6003,8 +6764,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
|
|
|
6003
6764
|
};
|
|
6004
6765
|
|
|
6005
6766
|
// src/uninstall.ts
|
|
6006
|
-
import * as
|
|
6007
|
-
import * as
|
|
6767
|
+
import * as fs13 from "node:fs";
|
|
6768
|
+
import * as path14 from "node:path";
|
|
6008
6769
|
var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
|
|
6009
6770
|
var parseUninstallArgs = (args) => {
|
|
6010
6771
|
let scope = null;
|
|
@@ -6026,14 +6787,14 @@ var parseUninstallArgs = (args) => {
|
|
|
6026
6787
|
var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
|
|
6027
6788
|
var countEntries = (directory) => {
|
|
6028
6789
|
try {
|
|
6029
|
-
return
|
|
6790
|
+
return fs13.readdirSync(directory).length;
|
|
6030
6791
|
} catch {
|
|
6031
6792
|
return 0;
|
|
6032
6793
|
}
|
|
6033
6794
|
};
|
|
6034
6795
|
var exists = (file) => {
|
|
6035
6796
|
try {
|
|
6036
|
-
|
|
6797
|
+
fs13.lstatSync(file);
|
|
6037
6798
|
return true;
|
|
6038
6799
|
} catch {
|
|
6039
6800
|
return false;
|
|
@@ -6042,7 +6803,7 @@ var exists = (file) => {
|
|
|
6042
6803
|
var managedBlockCount = (sshConfigFile) => {
|
|
6043
6804
|
let config;
|
|
6044
6805
|
try {
|
|
6045
|
-
config =
|
|
6806
|
+
config = fs13.readFileSync(sshConfigFile, "utf8");
|
|
6046
6807
|
} catch {
|
|
6047
6808
|
return 0;
|
|
6048
6809
|
}
|
|
@@ -6066,27 +6827,27 @@ var describeUninstall = async (runtime, scope) => {
|
|
|
6066
6827
|
if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
|
|
6067
6828
|
server = "none";
|
|
6068
6829
|
} else if (read.value === null) {
|
|
6069
|
-
server = exists(
|
|
6830
|
+
server = exists(path14.join(configDir, "identity.env")) ? {
|
|
6070
6831
|
version: "(no active generation)",
|
|
6071
6832
|
appUrl: "",
|
|
6072
6833
|
dockerContext: "",
|
|
6073
|
-
generations: countEntries(
|
|
6074
|
-
backups: countEntries(
|
|
6834
|
+
generations: countEntries(path14.join(configDir, "generations")),
|
|
6835
|
+
backups: countEntries(path14.join(configDir, "backups"))
|
|
6075
6836
|
} : "none";
|
|
6076
6837
|
} else {
|
|
6077
6838
|
server = {
|
|
6078
6839
|
version: read.value.config.serverVersion,
|
|
6079
6840
|
appUrl: read.value.config.appUrl,
|
|
6080
6841
|
dockerContext: read.value.config.dockerContext,
|
|
6081
|
-
generations: countEntries(
|
|
6082
|
-
backups: countEntries(
|
|
6842
|
+
generations: countEntries(path14.join(configDir, "generations")),
|
|
6843
|
+
backups: countEntries(path14.join(configDir, "backups"))
|
|
6083
6844
|
};
|
|
6084
6845
|
}
|
|
6085
6846
|
}
|
|
6086
6847
|
let home = null;
|
|
6087
6848
|
if (scope !== "server") {
|
|
6088
|
-
const cliConfig =
|
|
6089
|
-
const sshDirectory =
|
|
6849
|
+
const cliConfig = path14.join(runtime.cliHome, "cli.json");
|
|
6850
|
+
const sshDirectory = path14.join(runtime.cliHome, "ssh");
|
|
6090
6851
|
home = {
|
|
6091
6852
|
cliConfig: exists(cliConfig) ? cliConfig : null,
|
|
6092
6853
|
sshDirectory: exists(sshDirectory) ? sshDirectory : null,
|
|
@@ -6218,7 +6979,7 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
6218
6979
|
}
|
|
6219
6980
|
}
|
|
6220
6981
|
for (const name of SERVER_FILES) {
|
|
6221
|
-
|
|
6982
|
+
fs13.rmSync(path14.join(configDir, name), { recursive: true, force: true });
|
|
6222
6983
|
}
|
|
6223
6984
|
server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
|
|
6224
6985
|
},
|
|
@@ -6229,20 +6990,20 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
6229
6990
|
var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
6230
6991
|
const { server } = runtime;
|
|
6231
6992
|
if (plan.cliConfig !== null) {
|
|
6232
|
-
|
|
6993
|
+
fs13.rmSync(plan.cliConfig, { force: true });
|
|
6233
6994
|
server.writeLine(`removed ${plan.cliConfig}`);
|
|
6234
6995
|
}
|
|
6235
6996
|
if (plan.sshDirectory !== null) {
|
|
6236
|
-
|
|
6997
|
+
fs13.rmSync(plan.sshDirectory, { recursive: true, force: true });
|
|
6237
6998
|
server.writeLine(`removed ${plan.sshDirectory}`);
|
|
6238
6999
|
}
|
|
6239
7000
|
if (plan.managedSshBlocks > 0) {
|
|
6240
7001
|
try {
|
|
6241
|
-
const before =
|
|
7002
|
+
const before = fs13.readFileSync(runtime.sshConfigFile, "utf8");
|
|
6242
7003
|
const stripped = stripManagedWorkspaceSshBlocks(before);
|
|
6243
7004
|
if (!stripped.ok) throw stripped.error;
|
|
6244
7005
|
if (stripped.value.removed > 0) {
|
|
6245
|
-
|
|
7006
|
+
fs13.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
|
|
6246
7007
|
server.writeLine(
|
|
6247
7008
|
`removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
|
|
6248
7009
|
);
|
|
@@ -6255,12 +7016,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
|
6255
7016
|
}
|
|
6256
7017
|
let remaining = [];
|
|
6257
7018
|
try {
|
|
6258
|
-
remaining =
|
|
7019
|
+
remaining = fs13.readdirSync(runtime.cliHome);
|
|
6259
7020
|
} catch {
|
|
6260
7021
|
return;
|
|
6261
7022
|
}
|
|
6262
7023
|
if (remaining.length === 0) {
|
|
6263
|
-
|
|
7024
|
+
fs13.rmdirSync(runtime.cliHome);
|
|
6264
7025
|
server.writeLine(`removed ${runtime.cliHome}`);
|
|
6265
7026
|
} else {
|
|
6266
7027
|
leftovers.push(
|
|
@@ -6324,19 +7085,19 @@ var upgradeUrl = async (input) => {
|
|
|
6324
7085
|
// src/main.ts
|
|
6325
7086
|
var mendCliHome = () => {
|
|
6326
7087
|
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
6327
|
-
const preferred =
|
|
6328
|
-
xdg === void 0 || xdg === "" ?
|
|
7088
|
+
const preferred = path15.join(
|
|
7089
|
+
xdg === void 0 || xdg === "" ? path15.join(os9.homedir(), ".config") : xdg,
|
|
6329
7090
|
"mend"
|
|
6330
7091
|
);
|
|
6331
|
-
const legacy =
|
|
6332
|
-
return !
|
|
7092
|
+
const legacy = path15.join(os9.homedir(), ".mend");
|
|
7093
|
+
return !fs14.existsSync(preferred) && fs14.existsSync(legacy) ? legacy : preferred;
|
|
6333
7094
|
};
|
|
6334
|
-
var CONFIG_PATH =
|
|
7095
|
+
var CONFIG_PATH = path15.join(mendCliHome(), "cli.json");
|
|
6335
7096
|
var loadConfig = () => {
|
|
6336
7097
|
let fileConfig = {};
|
|
6337
|
-
if (
|
|
7098
|
+
if (fs14.existsSync(CONFIG_PATH)) {
|
|
6338
7099
|
try {
|
|
6339
|
-
fileConfig = JSON.parse(
|
|
7100
|
+
fileConfig = JSON.parse(fs14.readFileSync(CONFIG_PATH, "utf8"));
|
|
6340
7101
|
} catch {
|
|
6341
7102
|
fail6(`could not parse ${CONFIG_PATH}`);
|
|
6342
7103
|
}
|
|
@@ -6527,7 +7288,7 @@ var adopt = async (config, args) => {
|
|
|
6527
7288
|
}
|
|
6528
7289
|
const sourceIssue = repositoryCloneUrlIssue(source);
|
|
6529
7290
|
if (sourceIssue !== null) return fail6(sourceIssue);
|
|
6530
|
-
const projectName = name ?? normalizeProjectName(
|
|
7291
|
+
const projectName = name ?? normalizeProjectName(path15.basename(source, ".git"));
|
|
6531
7292
|
const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
|
|
6532
7293
|
if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
|
|
6533
7294
|
return fail6(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
|
|
@@ -6576,7 +7337,7 @@ var findProject = async (config, explicit, adoptCwd = false) => {
|
|
|
6576
7337
|
const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
|
|
6577
7338
|
if (sourceIssue !== null) return fail6(sourceIssue);
|
|
6578
7339
|
const adopted = await api(config, "POST", "/projects", {
|
|
6579
|
-
name: normalizeProjectName(
|
|
7340
|
+
name: normalizeProjectName(path15.basename(facts.repoRoot)),
|
|
6580
7341
|
source: facts.originUrl
|
|
6581
7342
|
});
|
|
6582
7343
|
say6(`${green6("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim6(adopted.storePath)}`);
|
|
@@ -6743,8 +7504,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
6743
7504
|
const ws = new WebSocket(url);
|
|
6744
7505
|
ws.binaryType = "arraybuffer";
|
|
6745
7506
|
try {
|
|
6746
|
-
await new Promise((
|
|
6747
|
-
ws.addEventListener("open", () =>
|
|
7507
|
+
await new Promise((resolve7, reject) => {
|
|
7508
|
+
ws.addEventListener("open", () => resolve7(), { once: true });
|
|
6748
7509
|
ws.addEventListener("error", () => reject(new Error("could not connect")), { once: true });
|
|
6749
7510
|
});
|
|
6750
7511
|
} catch {
|
|
@@ -6752,8 +7513,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
6752
7513
|
}
|
|
6753
7514
|
const stopHerdrHint = hintHerdrAttachment(harness);
|
|
6754
7515
|
let finishAttachment;
|
|
6755
|
-
const finished = new Promise((
|
|
6756
|
-
finishAttachment =
|
|
7516
|
+
const finished = new Promise((resolve7) => {
|
|
7517
|
+
finishAttachment = resolve7;
|
|
6757
7518
|
});
|
|
6758
7519
|
const onClose = () => finishAttachment?.();
|
|
6759
7520
|
if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
|
|
@@ -7290,11 +8051,11 @@ var autoConnect = async (config, service) => {
|
|
|
7290
8051
|
};
|
|
7291
8052
|
var serviceRun = async (config, args) => {
|
|
7292
8053
|
const dashdash = args.indexOf("--");
|
|
7293
|
-
const
|
|
8054
|
+
const usage2 = usageOf("service run");
|
|
7294
8055
|
if (dashdash === -1) {
|
|
7295
8056
|
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
7296
8057
|
const name2 = positionals.at(-1);
|
|
7297
|
-
if (name2 === void 0) return fail6(
|
|
8058
|
+
if (name2 === void 0) return fail6(usage2);
|
|
7298
8059
|
const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
|
|
7299
8060
|
const session2 = await resolveLiveSession(config, prefix2, "service run");
|
|
7300
8061
|
const recipes = await api(
|
|
@@ -7323,19 +8084,19 @@ var serviceRun = async (config, args) => {
|
|
|
7323
8084
|
return;
|
|
7324
8085
|
}
|
|
7325
8086
|
const argv = args.slice(dashdash + 1);
|
|
7326
|
-
if (argv.length === 0) return fail6(
|
|
8087
|
+
if (argv.length === 0) return fail6(usage2);
|
|
7327
8088
|
const head = args.slice(0, dashdash);
|
|
7328
8089
|
const portFlag = head.indexOf("--port");
|
|
7329
8090
|
const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
|
|
7330
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) return fail6(
|
|
8091
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return fail6(usage2);
|
|
7331
8092
|
const nameFlag = head.indexOf("--name");
|
|
7332
8093
|
const name = nameFlag !== -1 && head[nameFlag + 1] !== void 0 ? String(head[nameFlag + 1]) : null;
|
|
7333
8094
|
const protocol = head.includes("--udp") ? "udp" : "tcp";
|
|
7334
8095
|
const http = head.includes("--http");
|
|
7335
8096
|
const https = head.includes("--https");
|
|
7336
|
-
if (http && https) return fail6(
|
|
8097
|
+
if (http && https) return fail6(usage2);
|
|
7337
8098
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
7338
|
-
if (protocol === "udp" && browserScheme !== null) return fail6(
|
|
8099
|
+
if (protocol === "udp" && browserScheme !== null) return fail6(usage2);
|
|
7339
8100
|
const prefix = head.find(
|
|
7340
8101
|
(a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
|
|
7341
8102
|
);
|
|
@@ -7408,7 +8169,7 @@ var serviceLogs = async (config, args) => {
|
|
|
7408
8169
|
cursor = page.nextFrom;
|
|
7409
8170
|
const ended = page.status === "exited" || page.status === "failed";
|
|
7410
8171
|
if (ended && !advanced) break;
|
|
7411
|
-
if (!advanced) await new Promise((
|
|
8172
|
+
if (!advanced) await new Promise((resolve7) => setTimeout(resolve7, 250));
|
|
7412
8173
|
}
|
|
7413
8174
|
say6("");
|
|
7414
8175
|
say6(dim6(`stream ended \xB7 next sequence ${cursor}`));
|
|
@@ -7426,13 +8187,13 @@ var serviceRestart = async (config, args) => {
|
|
|
7426
8187
|
};
|
|
7427
8188
|
var serviceInit = async (args) => {
|
|
7428
8189
|
const root = gitTopLevel(process.cwd()) ?? process.cwd();
|
|
7429
|
-
const target =
|
|
7430
|
-
if (
|
|
8190
|
+
const target = path15.join(root, "mend.toml");
|
|
8191
|
+
if (fs14.existsSync(target)) {
|
|
7431
8192
|
return fail6(`${target} already exists \u2014 edit it directly (init never merges)`);
|
|
7432
8193
|
}
|
|
7433
|
-
const rootFiles =
|
|
8194
|
+
const rootFiles = fs14.readdirSync(root);
|
|
7434
8195
|
const proposals = [];
|
|
7435
|
-
const readRoot = (name) =>
|
|
8196
|
+
const readRoot = (name) => fs14.readFileSync(path15.join(root, name), "utf8");
|
|
7436
8197
|
if (rootFiles.includes("package.json")) {
|
|
7437
8198
|
proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
|
|
7438
8199
|
}
|
|
@@ -7442,13 +8203,13 @@ var serviceInit = async (args) => {
|
|
|
7442
8203
|
);
|
|
7443
8204
|
for (const glob of globs) {
|
|
7444
8205
|
const parent = glob.replace(/\/?\*+$/, "");
|
|
7445
|
-
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];
|
|
7446
8207
|
for (const dir of dirs) {
|
|
7447
|
-
const manifest =
|
|
7448
|
-
if (!
|
|
8208
|
+
const manifest = path15.join(root, dir, "package.json");
|
|
8209
|
+
if (!fs14.existsSync(manifest)) continue;
|
|
7449
8210
|
for (const proposal of proposeFromWorkspacePackage(
|
|
7450
|
-
|
|
7451
|
-
|
|
8211
|
+
path15.basename(dir),
|
|
8212
|
+
fs14.readFileSync(manifest, "utf8"),
|
|
7452
8213
|
rootFiles
|
|
7453
8214
|
)) {
|
|
7454
8215
|
if (!proposals.some((existing) => existing.name === proposal.name)) {
|
|
@@ -7492,7 +8253,7 @@ var serviceInit = async (args) => {
|
|
|
7492
8253
|
return;
|
|
7493
8254
|
}
|
|
7494
8255
|
}
|
|
7495
|
-
|
|
8256
|
+
fs14.writeFileSync(target, toml);
|
|
7496
8257
|
say6(`${green6("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
|
|
7497
8258
|
};
|
|
7498
8259
|
var tunnelServices = async (config, services, portOverride) => {
|
|
@@ -7540,7 +8301,7 @@ var attachTunnels = (config, optOut) => {
|
|
|
7540
8301
|
start: async (sessionId) => {
|
|
7541
8302
|
await Promise.race([
|
|
7542
8303
|
tunnels.focus(sessionId),
|
|
7543
|
-
new Promise((
|
|
8304
|
+
new Promise((resolve7) => setTimeout(resolve7, 3e3))
|
|
7544
8305
|
]);
|
|
7545
8306
|
},
|
|
7546
8307
|
attached: (work) => {
|
|
@@ -7596,18 +8357,18 @@ var serviceCommand = async (config, args) => {
|
|
|
7596
8357
|
}
|
|
7597
8358
|
};
|
|
7598
8359
|
var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
|
|
7599
|
-
var readIfExists = (file) =>
|
|
8360
|
+
var readIfExists = (file) => fs14.existsSync(file) ? fs14.readFileSync(file, "utf8") : null;
|
|
7600
8361
|
var localCredential = (provider) => {
|
|
7601
|
-
const home =
|
|
8362
|
+
const home = os9.homedir();
|
|
7602
8363
|
switch (provider) {
|
|
7603
8364
|
case "codex":
|
|
7604
8365
|
return readIfExists(
|
|
7605
|
-
|
|
8366
|
+
path15.join(process.env["CODEX_HOME"] ?? path15.join(home, ".codex"), "auth.json")
|
|
7606
8367
|
);
|
|
7607
8368
|
case "claude":
|
|
7608
8369
|
return readIfExists(
|
|
7609
|
-
|
|
7610
|
-
process.env["CLAUDE_CONFIG_DIR"] ??
|
|
8370
|
+
path15.join(
|
|
8371
|
+
process.env["CLAUDE_CONFIG_DIR"] ?? path15.join(home, ".claude"),
|
|
7611
8372
|
".credentials.json"
|
|
7612
8373
|
)
|
|
7613
8374
|
);
|
|
@@ -7631,19 +8392,64 @@ var accountLine = (account) => {
|
|
|
7631
8392
|
].filter((fact) => fact !== null);
|
|
7632
8393
|
return `${account.provider.padEnd(8)} ${facts.join(" \xB7 ")}`;
|
|
7633
8394
|
};
|
|
7634
|
-
var
|
|
7635
|
-
const identity = await
|
|
7636
|
-
process.stdout.write(`platform user ${identity.sealantUserId}
|
|
7637
|
-
`);
|
|
8395
|
+
var accountsLines = async (config) => {
|
|
8396
|
+
const identity = await request(config, "GET", "/me/sealant");
|
|
7638
8397
|
const providers = ["claude", "codex", "github"];
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
`
|
|
7644
|
-
)
|
|
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));
|
|
7645
8412
|
}
|
|
8413
|
+
for (const line of lines) process.stdout.write(`${line}
|
|
8414
|
+
`);
|
|
7646
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
|
+
});
|
|
7647
8453
|
var minuteOrUnknown = (at) => at === null ? "unknown" : at.toISOString().slice(0, 16);
|
|
7648
8454
|
var staleGrantReason = (secret) => {
|
|
7649
8455
|
const facts = claudeGrantFacts(secret);
|
|
@@ -7676,7 +8482,7 @@ var claudeGrant = async () => {
|
|
|
7676
8482
|
return null;
|
|
7677
8483
|
}
|
|
7678
8484
|
}
|
|
7679
|
-
if (status.configDirectory !== null &&
|
|
8485
|
+
if (status.configDirectory !== null && path15.resolve(status.configDirectory) !== path15.resolve(dir)) {
|
|
7680
8486
|
fail6(
|
|
7681
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`
|
|
7682
8488
|
);
|
|
@@ -7739,7 +8545,7 @@ var connectCommand = async (config, args) => {
|
|
|
7739
8545
|
}
|
|
7740
8546
|
let secret;
|
|
7741
8547
|
if (flags.includes("--from-stdin")) {
|
|
7742
|
-
secret =
|
|
8548
|
+
secret = fs14.readFileSync(0, "utf8").trim();
|
|
7743
8549
|
if (secret === "") return fail6("nothing on stdin");
|
|
7744
8550
|
} else if (provider === "claude" && !flags.includes("--use-my-login")) {
|
|
7745
8551
|
secret = await claudeGrant();
|
|
@@ -7785,14 +8591,14 @@ var takeFlagValue4 = (args, flag) => {
|
|
|
7785
8591
|
return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
|
|
7786
8592
|
};
|
|
7787
8593
|
var saveCliConfig = (next) => {
|
|
7788
|
-
|
|
7789
|
-
|
|
8594
|
+
fs14.mkdirSync(path15.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
|
|
8595
|
+
fs14.writeFileSync(
|
|
7790
8596
|
CONFIG_PATH,
|
|
7791
8597
|
`${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
|
|
7792
8598
|
`,
|
|
7793
8599
|
{ mode: 384 }
|
|
7794
8600
|
);
|
|
7795
|
-
|
|
8601
|
+
fs14.chmodSync(CONFIG_PATH, 384);
|
|
7796
8602
|
};
|
|
7797
8603
|
var login = async (config, args) => {
|
|
7798
8604
|
await loginCommand(args, {
|
|
@@ -7805,7 +8611,7 @@ var login = async (config, args) => {
|
|
|
7805
8611
|
});
|
|
7806
8612
|
};
|
|
7807
8613
|
var logout = async (config) => {
|
|
7808
|
-
if (!
|
|
8614
|
+
if (!fs14.existsSync(CONFIG_PATH) && config.token === null) {
|
|
7809
8615
|
say6(dim6("nothing saved \u2014 already signed out"));
|
|
7810
8616
|
return;
|
|
7811
8617
|
}
|
|
@@ -7858,8 +8664,8 @@ var uninstallCommand = async (config, args) => {
|
|
|
7858
8664
|
const runtime = {
|
|
7859
8665
|
server,
|
|
7860
8666
|
cliHome: mendCliHome(),
|
|
7861
|
-
sshConfigFile:
|
|
7862
|
-
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 },
|
|
7863
8669
|
revokeDevice: async () => {
|
|
7864
8670
|
if (config.deviceId === null) return "no device id saved";
|
|
7865
8671
|
try {
|
|
@@ -7923,7 +8729,7 @@ var keysShare = async (config) => {
|
|
|
7923
8729
|
return fail6("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
|
|
7924
8730
|
}
|
|
7925
8731
|
await shareAgent({
|
|
7926
|
-
url: () => socketUrl(config, "keys-bridge", { host:
|
|
8732
|
+
url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
|
|
7927
8733
|
agentSock,
|
|
7928
8734
|
signal: new AbortController().signal,
|
|
7929
8735
|
onEvent: (event) => {
|
|
@@ -8053,7 +8859,7 @@ ${usageOf("dotfiles repo")}`);
|
|
|
8053
8859
|
var dotfilesSync = async (config, args) => {
|
|
8054
8860
|
const all = args.includes("--all");
|
|
8055
8861
|
const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
|
|
8056
|
-
const home =
|
|
8862
|
+
const home = os9.homedir();
|
|
8057
8863
|
if (!all && requestedPaths.length === 0) {
|
|
8058
8864
|
const found = scanDotfileCandidates(home);
|
|
8059
8865
|
if (found.length === 0) {
|
|
@@ -8080,13 +8886,13 @@ var dotfilesSync = async (config, args) => {
|
|
|
8080
8886
|
if ("error" in read) return fail6(read.error);
|
|
8081
8887
|
const result = await api(config, "POST", "/dotfiles/snapshot", {
|
|
8082
8888
|
files: read.files,
|
|
8083
|
-
source:
|
|
8889
|
+
source: os9.hostname(),
|
|
8084
8890
|
merge: false
|
|
8085
8891
|
});
|
|
8086
8892
|
const snapshot = result.snapshot;
|
|
8087
8893
|
if (snapshot === null) return fail6("the server accepted the sync but reports no snapshot");
|
|
8088
8894
|
say6(
|
|
8089
|
-
`${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")}`
|
|
8090
8896
|
);
|
|
8091
8897
|
};
|
|
8092
8898
|
var envLoad = async (config, args) => {
|
|
@@ -8100,10 +8906,10 @@ var envLoad = async (config, args) => {
|
|
|
8100
8906
|
const positional = args.filter(
|
|
8101
8907
|
(arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
|
|
8102
8908
|
);
|
|
8103
|
-
const file =
|
|
8909
|
+
const file = path15.resolve(positional[0] ?? ".env");
|
|
8104
8910
|
let contents;
|
|
8105
8911
|
try {
|
|
8106
|
-
contents =
|
|
8912
|
+
contents = fs14.readFileSync(file, "utf8");
|
|
8107
8913
|
} catch {
|
|
8108
8914
|
return fail6(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
|
|
8109
8915
|
}
|
|
@@ -8119,10 +8925,10 @@ var envLoad = async (config, args) => {
|
|
|
8119
8925
|
{ contents, allSecret, secretNames }
|
|
8120
8926
|
);
|
|
8121
8927
|
if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
|
|
8122
|
-
say6(dim6(`${
|
|
8928
|
+
say6(dim6(`${path15.basename(file)} has no variables`));
|
|
8123
8929
|
return;
|
|
8124
8930
|
}
|
|
8125
|
-
say6(`${green6("\u2713")} loaded ${
|
|
8931
|
+
say6(`${green6("\u2713")} loaded ${path15.basename(file)} into ${project2.name}`);
|
|
8126
8932
|
for (const line of formatLoadReport(report, { dim: dim6, warn: amber2 })) say6(line);
|
|
8127
8933
|
const plaintextUrls = report.loaded.filter(
|
|
8128
8934
|
(entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
|
|
@@ -8196,7 +9002,7 @@ var envCluster = async (config, args) => {
|
|
|
8196
9002
|
const explicitProject = takeFlagValue4(args, "--project");
|
|
8197
9003
|
const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
|
|
8198
9004
|
const [verb, ...rest] = positional;
|
|
8199
|
-
const
|
|
9005
|
+
const usage2 = usageOf("env cluster");
|
|
8200
9006
|
const project2 = await findProject(config, explicitProject);
|
|
8201
9007
|
const route = `/projects/${project2.id}/cluster-bindings`;
|
|
8202
9008
|
const appliesLine = () => say6(
|
|
@@ -8208,7 +9014,7 @@ var envCluster = async (config, args) => {
|
|
|
8208
9014
|
const [kind, objectName] = rest;
|
|
8209
9015
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
8210
9016
|
return fail6(`env cluster add takes a kind and an object name
|
|
8211
|
-
${
|
|
9017
|
+
${usage2}`);
|
|
8212
9018
|
}
|
|
8213
9019
|
const result = await api(config, "POST", route, {
|
|
8214
9020
|
kind,
|
|
@@ -8223,7 +9029,7 @@ ${usage}`);
|
|
|
8223
9029
|
const [kind, objectName] = ref?.split("/", 2) ?? [];
|
|
8224
9030
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
8225
9031
|
return fail6(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
|
|
8226
|
-
${
|
|
9032
|
+
${usage2}`);
|
|
8227
9033
|
}
|
|
8228
9034
|
const snapshot = await api(config, "GET", route);
|
|
8229
9035
|
const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
|
|
@@ -8241,7 +9047,7 @@ ${usage}`);
|
|
|
8241
9047
|
const [name] = rest;
|
|
8242
9048
|
if (!clear && name === void 0)
|
|
8243
9049
|
return fail6(`env cluster sa takes a name or --clear
|
|
8244
|
-
${
|
|
9050
|
+
${usage2}`);
|
|
8245
9051
|
const result = await api(
|
|
8246
9052
|
config,
|
|
8247
9053
|
"PUT",
|
|
@@ -8261,7 +9067,7 @@ ${usage}`);
|
|
|
8261
9067
|
return appliesLine();
|
|
8262
9068
|
}
|
|
8263
9069
|
return fail6(`unknown env cluster command "${verb ?? ""}"
|
|
8264
|
-
${
|
|
9070
|
+
${usage2}`);
|
|
8265
9071
|
};
|
|
8266
9072
|
var envCommand = async (config, args) => {
|
|
8267
9073
|
const [verb, ...rest] = args;
|
|
@@ -8313,7 +9119,7 @@ var skillsList = async (config, args) => {
|
|
|
8313
9119
|
};
|
|
8314
9120
|
var skillsPush = async (config, args) => {
|
|
8315
9121
|
const dirFlag = takeFlagValue4(args, "--dir");
|
|
8316
|
-
const root = dirFlag === null ?
|
|
9122
|
+
const root = dirFlag === null ? path15.join(os9.homedir(), DEFAULT_SKILLS_DIR) : path15.resolve(dirFlag);
|
|
8317
9123
|
const scanned = scanSkillLibrary(root);
|
|
8318
9124
|
if ("error" in scanned) return fail6(scanned.error);
|
|
8319
9125
|
for (const note of scanned.notes) say6(dim6(` ${note.skill}: ${note.message}`));
|
|
@@ -8765,7 +9571,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
8765
9571
|
say6(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
|
|
8766
9572
|
return;
|
|
8767
9573
|
}
|
|
8768
|
-
const
|
|
9574
|
+
const json2 = args.includes("--json");
|
|
8769
9575
|
const projectFlag = args.indexOf("--project");
|
|
8770
9576
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
|
|
8771
9577
|
const projects = await api(config, "GET", "/projects");
|
|
@@ -8774,7 +9580,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
8774
9580
|
return fail6(`no adopted project named "${projectName}"`);
|
|
8775
9581
|
}
|
|
8776
9582
|
let rows;
|
|
8777
|
-
if (all || projectName !== null || !
|
|
9583
|
+
if (all || projectName !== null || !json2) {
|
|
8778
9584
|
const details = await Promise.all(
|
|
8779
9585
|
scope.map(
|
|
8780
9586
|
(p) => api(config, "GET", `/projects/${p.id}${all ? "?deadEnds=include" : ""}`)
|
|
@@ -8800,7 +9606,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
8800
9606
|
}));
|
|
8801
9607
|
}
|
|
8802
9608
|
if (rows.length === 0) {
|
|
8803
|
-
if (
|
|
9609
|
+
if (json2) {
|
|
8804
9610
|
say6(JSON.stringify({ version: 1, sessions: [] }, null, 2));
|
|
8805
9611
|
return;
|
|
8806
9612
|
}
|
|
@@ -8815,7 +9621,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
8815
9621
|
if (aLive !== bLive) return bLive - aLive;
|
|
8816
9622
|
return b.session.createdAt.localeCompare(a.session.createdAt);
|
|
8817
9623
|
});
|
|
8818
|
-
if (
|
|
9624
|
+
if (json2) {
|
|
8819
9625
|
const payload = {
|
|
8820
9626
|
version: 1,
|
|
8821
9627
|
sessions: rows.map(({ session, projectName: rowProjectName, annotation }) => ({
|
|
@@ -8914,11 +9720,11 @@ var buildWorktreesJson = async (config, projectName) => {
|
|
|
8914
9720
|
return { version: 2, worktrees };
|
|
8915
9721
|
};
|
|
8916
9722
|
var worktreesCommand = async (config, args) => {
|
|
8917
|
-
const
|
|
9723
|
+
const json2 = args.includes("--json");
|
|
8918
9724
|
const projectFlag = args.indexOf("--project");
|
|
8919
9725
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
|
|
8920
9726
|
const payload = await buildWorktreesJson(config, projectName);
|
|
8921
|
-
if (
|
|
9727
|
+
if (json2) {
|
|
8922
9728
|
say6(JSON.stringify(payload, null, 2));
|
|
8923
9729
|
return;
|
|
8924
9730
|
}
|
|
@@ -9009,7 +9815,7 @@ var startShareIfBridge = async (config) => {
|
|
|
9009
9815
|
}
|
|
9010
9816
|
if (access.mode !== "bridge") return null;
|
|
9011
9817
|
return startAgentShare({
|
|
9012
|
-
url: () => socketUrl(config, "keys-bridge", { host:
|
|
9818
|
+
url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
|
|
9013
9819
|
agentSock
|
|
9014
9820
|
});
|
|
9015
9821
|
};
|
|
@@ -9049,10 +9855,10 @@ var manCommand = (words) => {
|
|
|
9049
9855
|
}
|
|
9050
9856
|
const version = cliVersion();
|
|
9051
9857
|
const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
|
|
9052
|
-
const file =
|
|
9053
|
-
|
|
9858
|
+
const file = path15.join(os9.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
|
|
9859
|
+
fs14.writeFileSync(file, page);
|
|
9054
9860
|
const result = spawnSync4("man", ["-l", file], { stdio: "inherit" });
|
|
9055
|
-
|
|
9861
|
+
fs14.rmSync(file, { force: true });
|
|
9056
9862
|
if (result.error !== void 0 || result.status !== 0) {
|
|
9057
9863
|
say6(doc === null ? renderIndex() : renderCommand(doc));
|
|
9058
9864
|
}
|
|
@@ -9125,10 +9931,8 @@ var main = async () => {
|
|
|
9125
9931
|
case "qr":
|
|
9126
9932
|
return qrCommand(rest);
|
|
9127
9933
|
case "doctor":
|
|
9128
|
-
return
|
|
9129
|
-
|
|
9130
|
-
return read.kind === "missing" ? null : read.secret;
|
|
9131
|
-
});
|
|
9934
|
+
if (rest.includes("--bundle")) return doctorBundle(config, rest);
|
|
9935
|
+
return doctorCommand(config, localCredential, claudeGrantSecret);
|
|
9132
9936
|
case "env":
|
|
9133
9937
|
return envCommand(config, rest);
|
|
9134
9938
|
case "ssh":
|