@sealant/mend 0.27.5 → 0.29.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/README.md +36 -13
- package/dist/{chunk-A5I5EW2Y.js → chunk-F6AJNFVE.js} +238 -8
- package/dist/{dashboard-G52NHJCZ.js → dashboard-ZKCPU2QJ.js} +1388 -602
- package/dist/main.js +1473 -663
- package/man/mend-accounts.1 +1 -1
- package/man/mend-adopt.1 +8 -2
- 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 +9 -4
- package/man/mend-continue.1 +1 -1
- package/man/mend-doctor.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 +11 -0
- package/man/mend-folder-list.1 +11 -0
- package/man/mend-folder-push.1 +18 -0
- package/man/mend-folder-rm.1 +11 -0
- package/man/mend-help.1 +1 -1
- package/man/mend-invite.1 +26 -0
- 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-login.1 +1 -1
- package/man/mend-logout.1 +1 -1
- package/man/mend-man.1 +1 -1
- package/man/mend-members.1 +11 -0
- package/man/mend-operator-exposure.1 +11 -0
- package/man/mend-operator-gate.1 +11 -0
- package/man/mend-operator-grant-owner.1 +11 -0
- package/man/mend-operator-org-create.1 +11 -0
- package/man/mend-operator-org-invite-owner.1 +15 -0
- package/man/mend-operator-org-list.1 +11 -0
- package/man/mend-operator-org-rename.1 +11 -0
- package/man/mend-operator-reset-link.1 +11 -0
- package/man/mend-pair.1 +1 -1
- package/man/mend-projects.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 +2 -2
- 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 +14 -0
- 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-ssh-setup.1 +1 -1
- package/man/mend-ssh.1 +1 -1
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +6 -2
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +48 -2
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
LIVE_STATUSES,
|
|
5
5
|
agentIsLive,
|
|
6
6
|
agentOutcome,
|
|
7
|
+
claudeGrantFacts,
|
|
7
8
|
cwdFacts,
|
|
8
9
|
gitCurrentBranch,
|
|
9
10
|
gitOriginUrl,
|
|
@@ -11,32 +12,27 @@ import {
|
|
|
11
12
|
isDetachChunk,
|
|
12
13
|
isPasteChunk,
|
|
13
14
|
matchProjectByCwd,
|
|
15
|
+
narrowCredential,
|
|
14
16
|
normalizeProjectName,
|
|
15
17
|
parseLaunchArgs,
|
|
16
18
|
pasteBytes,
|
|
17
19
|
repositoryCloneUrlIssue,
|
|
20
|
+
sameGrant,
|
|
18
21
|
sessionDisplayName,
|
|
19
22
|
trackBracketedPaste
|
|
20
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-F6AJNFVE.js";
|
|
21
24
|
|
|
22
25
|
// src/main.ts
|
|
23
|
-
import { spawn as spawn3, spawnSync as
|
|
24
|
-
import * as
|
|
26
|
+
import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
|
|
27
|
+
import * as fs11 from "node:fs";
|
|
25
28
|
import { createRequire } from "node:module";
|
|
26
29
|
import * as net3 from "node:net";
|
|
27
|
-
import * as
|
|
28
|
-
import * as
|
|
30
|
+
import * as os7 from "node:os";
|
|
31
|
+
import * as path12 from "node:path";
|
|
29
32
|
|
|
30
33
|
// src/agent-share.ts
|
|
31
34
|
import * as net from "node:net";
|
|
32
35
|
import { setTimeout as delay } from "node:timers/promises";
|
|
33
|
-
var bridgeUrlOf = (serverUrl, token, host) => {
|
|
34
|
-
const url = new URL(`${serverUrl}/api/keys/bridge/ws`);
|
|
35
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
36
|
-
url.searchParams.set("host", host);
|
|
37
|
-
if (token !== null) url.searchParams.set("token", token);
|
|
38
|
-
return url;
|
|
39
|
-
};
|
|
40
36
|
var retryDelayMs = (attempt2) => Math.min(3e4, 1e3 * 2 ** Math.min(Math.max(attempt2 - 1, 0), 5));
|
|
41
37
|
var SSH_AGENTC_REQUEST_IDENTITIES = 11;
|
|
42
38
|
var SSH_AGENTC_SIGN_REQUEST = 13;
|
|
@@ -71,7 +67,17 @@ var shareAgent = async (options) => {
|
|
|
71
67
|
let chain = Promise.resolve();
|
|
72
68
|
let attempt2 = 0;
|
|
73
69
|
while (!signal.aborted) {
|
|
74
|
-
|
|
70
|
+
let target;
|
|
71
|
+
try {
|
|
72
|
+
target = await url();
|
|
73
|
+
} catch {
|
|
74
|
+
attempt2 += 1;
|
|
75
|
+
const retryMs2 = retryDelayMs(attempt2);
|
|
76
|
+
onEvent({ kind: "disconnected", retryMs: retryMs2 });
|
|
77
|
+
await sleep(retryMs2, signal);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const ws = new WebSocket(target);
|
|
75
81
|
const onAbort = () => ws.close();
|
|
76
82
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
77
83
|
const closed = new Promise((resolve4) => {
|
|
@@ -157,6 +163,75 @@ var startAgentShare = (options) => {
|
|
|
157
163
|
};
|
|
158
164
|
};
|
|
159
165
|
|
|
166
|
+
// src/claude-grant.ts
|
|
167
|
+
import { spawnSync } from "node:child_process";
|
|
168
|
+
import { createHash } from "node:crypto";
|
|
169
|
+
import * as fs from "node:fs";
|
|
170
|
+
import * as os from "node:os";
|
|
171
|
+
import * as path from "node:path";
|
|
172
|
+
var claudeGrantDir = (home) => path.join(home, "claude-grant");
|
|
173
|
+
var personalClaudeDir = (environment = process.env) => {
|
|
174
|
+
const configured = environment["CLAUDE_CONFIG_DIR"];
|
|
175
|
+
return configured === void 0 || configured === "" ? path.join(os.homedir(), ".claude") : configured;
|
|
176
|
+
};
|
|
177
|
+
var keychainService = (configDir) => {
|
|
178
|
+
const suffix = createHash("sha256").update(configDir).digest("hex").slice(0, 8);
|
|
179
|
+
return `Claude Code-credentials-${suffix}`;
|
|
180
|
+
};
|
|
181
|
+
var claudeCli = (environment = process.env) => ({
|
|
182
|
+
bin: environment["MEND_CLAUDE_BIN"] ?? "claude",
|
|
183
|
+
env: environment
|
|
184
|
+
});
|
|
185
|
+
var grantStatus = (cli, configDir) => {
|
|
186
|
+
const result = spawnSync(cli.bin, ["auth", "status", "--json"], {
|
|
187
|
+
encoding: "utf8",
|
|
188
|
+
env: { ...cli.env, CLAUDE_CONFIG_DIR: configDir },
|
|
189
|
+
timeout: 6e4
|
|
190
|
+
});
|
|
191
|
+
if (result.error !== void 0 || typeof result.stdout !== "string") return null;
|
|
192
|
+
const start = result.stdout.indexOf("{");
|
|
193
|
+
if (start === -1) return null;
|
|
194
|
+
try {
|
|
195
|
+
const parsed = JSON.parse(result.stdout.slice(start));
|
|
196
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
197
|
+
const record = { ...parsed };
|
|
198
|
+
const text = (key) => typeof record[key] === "string" ? record[key] : null;
|
|
199
|
+
return {
|
|
200
|
+
loggedIn: record["loggedIn"] === true,
|
|
201
|
+
authMethod: text("authMethod"),
|
|
202
|
+
configDirectory: text("configDirectory"),
|
|
203
|
+
email: text("email"),
|
|
204
|
+
subscriptionType: text("subscriptionType")
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
var runClaudeLogin = (cli, configDir) => {
|
|
211
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
212
|
+
const result = spawnSync(cli.bin, ["auth", "login", "--claudeai"], {
|
|
213
|
+
stdio: "inherit",
|
|
214
|
+
env: { ...cli.env, CLAUDE_CONFIG_DIR: configDir }
|
|
215
|
+
});
|
|
216
|
+
return result.error === void 0 && result.status === 0;
|
|
217
|
+
};
|
|
218
|
+
var readGrant = (configDir, platform = process.platform) => {
|
|
219
|
+
const file = path.join(configDir, ".credentials.json");
|
|
220
|
+
if (fs.existsSync(file)) {
|
|
221
|
+
const secret2 = fs.readFileSync(file, "utf8").trim();
|
|
222
|
+
if (secret2 !== "") return { kind: "file", secret: secret2, path: file };
|
|
223
|
+
}
|
|
224
|
+
if (platform !== "darwin") return { kind: "missing", triedPath: file, triedService: null };
|
|
225
|
+
const service = keychainService(configDir);
|
|
226
|
+
const result = spawnSync(
|
|
227
|
+
"security",
|
|
228
|
+
["find-generic-password", "-a", os.userInfo().username, "-w", "-s", service],
|
|
229
|
+
{ encoding: "utf8", timeout: 6e4 }
|
|
230
|
+
);
|
|
231
|
+
const secret = typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
232
|
+
return result.status === 0 && secret !== "" ? { kind: "keychain", secret, service } : { kind: "missing", triedPath: file, triedService: service };
|
|
233
|
+
};
|
|
234
|
+
|
|
160
235
|
// src/clipboard.ts
|
|
161
236
|
import { execFile } from "node:child_process";
|
|
162
237
|
var IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
@@ -215,12 +290,13 @@ var readClipboardImage = async (platform = process.platform, env = process.env)
|
|
|
215
290
|
};
|
|
216
291
|
|
|
217
292
|
// src/doctor.ts
|
|
218
|
-
import * as
|
|
219
|
-
import * as
|
|
293
|
+
import * as fs2 from "node:fs";
|
|
294
|
+
import * as path2 from "node:path";
|
|
220
295
|
var TIMEOUT_MS = 3e3;
|
|
221
296
|
var MARKS = { ok: "\u2713", todo: "\u25CB", failed: "\u2717" };
|
|
222
297
|
var LABEL_WIDTH = 11;
|
|
223
|
-
var
|
|
298
|
+
var day = (at) => at.toISOString().slice(0, 10);
|
|
299
|
+
var formatCheck = (check, paint5 = (_state, mark) => mark) => `${paint5(check.state, MARKS[check.state])} ${check.label.padEnd(LABEL_WIDTH)} ${check.detail}${check.fix === null ? "" : ` \u2192 ${check.fix}`}`;
|
|
224
300
|
var getJson = async (config, route) => {
|
|
225
301
|
try {
|
|
226
302
|
const response = await fetch(`${config.url}/api${route}`, {
|
|
@@ -233,6 +309,23 @@ var getJson = async (config, route) => {
|
|
|
233
309
|
return { value: null, status: null };
|
|
234
310
|
}
|
|
235
311
|
};
|
|
312
|
+
var exposureCheck = (exposure) => {
|
|
313
|
+
const beyondMachine = exposure.declared !== "loopback" && exposure.originOnMachine !== true;
|
|
314
|
+
const gateOpen = beyondMachine ? exposure.gateOpen : 0;
|
|
315
|
+
const facts = [
|
|
316
|
+
`declared ${exposure.declared}`,
|
|
317
|
+
`${exposure.originScheme} origin`,
|
|
318
|
+
...exposure.arrivedVia === "trusted-proxy" ? ["arrived via a trusted proxy"] : [],
|
|
319
|
+
...gateOpen === 0 ? [] : [`${gateOpen} gate items open`]
|
|
320
|
+
].join(" \xB7 ");
|
|
321
|
+
const plainBeyondMachine = beyondMachine && exposure.originScheme === "http";
|
|
322
|
+
return {
|
|
323
|
+
label: "exposure",
|
|
324
|
+
state: plainBeyondMachine ? "todo" : "ok",
|
|
325
|
+
detail: facts,
|
|
326
|
+
fix: plainBeyondMachine ? "serve it over https and set APP_URL to that origin" : gateOpen === 0 ? null : "mend operator exposure"
|
|
327
|
+
};
|
|
328
|
+
};
|
|
236
329
|
var LOGIN_COMMANDS = {
|
|
237
330
|
claude: "claude setup-token",
|
|
238
331
|
codex: "codex login",
|
|
@@ -354,6 +447,39 @@ var runChecks = async (config, probes) => {
|
|
|
354
447
|
}
|
|
355
448
|
);
|
|
356
449
|
}
|
|
450
|
+
const grant = probes.claudeGrant();
|
|
451
|
+
if (grant !== null) {
|
|
452
|
+
const facts = claudeGrantFacts(grant);
|
|
453
|
+
if (facts === null) {
|
|
454
|
+
checks.push({
|
|
455
|
+
label: "grant",
|
|
456
|
+
state: "failed",
|
|
457
|
+
detail: "unreadable",
|
|
458
|
+
fix: "mend connect claude"
|
|
459
|
+
});
|
|
460
|
+
} else if (!facts.hasRefreshToken) {
|
|
461
|
+
checks.push({
|
|
462
|
+
label: "grant",
|
|
463
|
+
state: "failed",
|
|
464
|
+
detail: "signed out",
|
|
465
|
+
fix: "mend connect claude"
|
|
466
|
+
});
|
|
467
|
+
} else if (facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now()) {
|
|
468
|
+
checks.push({
|
|
469
|
+
label: "grant",
|
|
470
|
+
state: "todo",
|
|
471
|
+
detail: `expired ${day(facts.refreshExpiresAt)}`,
|
|
472
|
+
fix: "mend connect claude"
|
|
473
|
+
});
|
|
474
|
+
} else {
|
|
475
|
+
checks.push({
|
|
476
|
+
label: "grant",
|
|
477
|
+
state: "ok",
|
|
478
|
+
detail: facts.refreshExpiresAt === null ? "Mend's own" : `Mend's own \xB7 expires ${day(facts.refreshExpiresAt)}`,
|
|
479
|
+
fix: null
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
357
483
|
const adopted = projects === null ? null : projects.value;
|
|
358
484
|
if (adopted === null) checks.push(notChecked("projects"));
|
|
359
485
|
else if (adopted.length === 0) {
|
|
@@ -378,20 +504,15 @@ var runChecks = async (config, probes) => {
|
|
|
378
504
|
);
|
|
379
505
|
}
|
|
380
506
|
const machine = signedIn ? await getJson(config, "/machine") : null;
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
else if (tailnet.status === "reachable" && tailnet.address !== null) {
|
|
384
|
-
checks.push({ label: "tailnet", state: "ok", detail: tailnet.address, fix: null });
|
|
385
|
-
} else {
|
|
386
|
-
checks.push({ label: "tailnet", state: "todo", detail: "not detected", fix: null });
|
|
387
|
-
}
|
|
507
|
+
const exposure = machine === null || machine.value === null ? void 0 : machine.value.exposure;
|
|
508
|
+
checks.push(exposure === void 0 ? notChecked("exposure") : exposureCheck(exposure));
|
|
388
509
|
return checks;
|
|
389
510
|
};
|
|
390
511
|
var onPath = (command) => {
|
|
391
|
-
for (const directory of (process.env["PATH"] ?? "").split(
|
|
512
|
+
for (const directory of (process.env["PATH"] ?? "").split(path2.delimiter)) {
|
|
392
513
|
if (directory === "") continue;
|
|
393
514
|
try {
|
|
394
|
-
|
|
515
|
+
fs2.accessSync(path2.join(directory, command), fs2.constants.X_OK);
|
|
395
516
|
return true;
|
|
396
517
|
} catch {
|
|
397
518
|
}
|
|
@@ -403,16 +524,16 @@ var paintMark = (state, mark) => {
|
|
|
403
524
|
const code = state === "ok" ? "32" : state === "todo" ? "33" : "31";
|
|
404
525
|
return `\x1B[${code}m${mark}\x1B[0m`;
|
|
405
526
|
};
|
|
406
|
-
var doctorCommand = async (config, localCredential2) => {
|
|
407
|
-
const checks = await runChecks(config, { localCredential: localCredential2, onPath });
|
|
527
|
+
var doctorCommand = async (config, localCredential2, claudeGrant2) => {
|
|
528
|
+
const checks = await runChecks(config, { localCredential: localCredential2, claudeGrant: claudeGrant2, onPath });
|
|
408
529
|
for (const check of checks) process.stdout.write(`${formatCheck(check, paintMark)}
|
|
409
530
|
`);
|
|
410
531
|
if (checks.some((check) => check.state === "failed")) process.exitCode = 1;
|
|
411
532
|
};
|
|
412
533
|
|
|
413
534
|
// src/dotfiles.ts
|
|
414
|
-
import * as
|
|
415
|
-
import * as
|
|
535
|
+
import * as fs3 from "node:fs";
|
|
536
|
+
import * as path3 from "node:path";
|
|
416
537
|
var DOTFILE_CANDIDATES = [
|
|
417
538
|
{
|
|
418
539
|
group: "shell",
|
|
@@ -455,7 +576,7 @@ var DOTFILE_CANDIDATES = [
|
|
|
455
576
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
456
577
|
var statFile = (home, relative3) => {
|
|
457
578
|
try {
|
|
458
|
-
const stat =
|
|
579
|
+
const stat = fs3.statSync(path3.join(home, relative3));
|
|
459
580
|
return stat.isFile() ? stat : null;
|
|
460
581
|
} catch {
|
|
461
582
|
return null;
|
|
@@ -479,7 +600,7 @@ var readSyncFiles = (home, paths_) => {
|
|
|
479
600
|
}
|
|
480
601
|
files.push({
|
|
481
602
|
path: relative3,
|
|
482
|
-
contentsBase64:
|
|
603
|
+
contentsBase64: fs3.readFileSync(path3.join(home, relative3)).toString("base64"),
|
|
483
604
|
// eslint-disable-next-line no-bitwise -- permission bits are the point
|
|
484
605
|
mode: (stat.mode & 511).toString(8)
|
|
485
606
|
});
|
|
@@ -488,7 +609,7 @@ var readSyncFiles = (home, paths_) => {
|
|
|
488
609
|
};
|
|
489
610
|
|
|
490
611
|
// src/env.ts
|
|
491
|
-
var formatLoadReport = (report,
|
|
612
|
+
var formatLoadReport = (report, paint5) => {
|
|
492
613
|
const width = Math.max(
|
|
493
614
|
0,
|
|
494
615
|
...report.loaded.map((entry) => entry.name.length),
|
|
@@ -497,12 +618,12 @@ var formatLoadReport = (report, paint4) => {
|
|
|
497
618
|
const lines = [];
|
|
498
619
|
for (const entry of report.loaded) {
|
|
499
620
|
lines.push(
|
|
500
|
-
` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${
|
|
621
|
+
` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint5.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
|
|
501
622
|
);
|
|
502
623
|
}
|
|
503
624
|
for (const entry of report.rejected) {
|
|
504
625
|
lines.push(
|
|
505
|
-
` ${entry.name.padEnd(width)} ${
|
|
626
|
+
` ${entry.name.padEnd(width)} ${paint5.warn("rejected")} ${paint5.dim(`\xB7 ${entry.reason}`)}`
|
|
506
627
|
);
|
|
507
628
|
}
|
|
508
629
|
return lines;
|
|
@@ -514,6 +635,7 @@ var SECTIONS = [
|
|
|
514
635
|
"sessions",
|
|
515
636
|
"services",
|
|
516
637
|
"project setup",
|
|
638
|
+
"organization",
|
|
517
639
|
"this machine"
|
|
518
640
|
];
|
|
519
641
|
var project = (what = "the project") => ({
|
|
@@ -546,16 +668,21 @@ var COMMANDS = [
|
|
|
546
668
|
name: "connect",
|
|
547
669
|
section: "start",
|
|
548
670
|
summary: "send this machine's provider credential to the platform",
|
|
549
|
-
synopsis: ["<claude|codex|github> [--from-stdin] [--remove]"],
|
|
671
|
+
synopsis: ["<claude|codex|github> [--use-my-login] [--from-stdin] [--remove]"],
|
|
550
672
|
description: [
|
|
551
|
-
"Sessions run on the platform, so the platform needs your provider login. This reads the file the provider's own CLI wrote when you logged in (
|
|
673
|
+
"Sessions run on the platform, so the platform needs your provider login. This reads the file the provider's own CLI wrote when you logged in (codex) or asks gh for its token (github) and stores it under your own user. Nothing is shared with other users.",
|
|
674
|
+
"Claude is different: Mend gets a login of its own. Claude rotates its refresh token, so two copies of one login fight and the one that refreshes second is signed out, and Mend refreshes on a schedule. So this logs in once against a directory Mend keeps, and your own Claude login stays as it is. Run it again whenever Mend says the grant expired."
|
|
552
675
|
],
|
|
553
676
|
options: [
|
|
677
|
+
{
|
|
678
|
+
flag: "--use-my-login",
|
|
679
|
+
text: "claude: send the login this machine already uses; both sides then share one grant"
|
|
680
|
+
},
|
|
554
681
|
{ flag: "--from-stdin", text: "paste a credential instead of reading the provider's file" },
|
|
555
682
|
{ flag: "--remove", text: "disconnect the provider" }
|
|
556
683
|
],
|
|
557
684
|
examples: [
|
|
558
|
-
{ command: "mend connect claude", text: "
|
|
685
|
+
{ command: "mend connect claude", text: "a Claude login of Mend's own, through the browser" },
|
|
559
686
|
{ command: "mend connect github --remove", text: "" }
|
|
560
687
|
],
|
|
561
688
|
see: ["accounts"]
|
|
@@ -564,14 +691,19 @@ var COMMANDS = [
|
|
|
564
691
|
name: "adopt",
|
|
565
692
|
section: "start",
|
|
566
693
|
summary: "adopt a repository into the store",
|
|
567
|
-
synopsis: ["[git-url] [--name <name>] [--auth ambient|mend-key|bridge]"],
|
|
694
|
+
synopsis: ["[git-url] [--name <name>] [--auth ambient|mend-key|bridge] [--private|--shared]"],
|
|
568
695
|
description: [
|
|
569
696
|
"Clones a network Git repository into Mend's store. Every session then gets its own worktree of it. With no argument, Mend uses the current checkout's origin URL. HTTP(S), SSH, git://, and SCP-style URLs work; local paths and file:// URLs do not.",
|
|
570
697
|
"--auth says how the store fetches from the remote. Default: your mode from mend keys mode. mend-key signs with your Mend key on the server (see mend keys). bridge relays this machine's ssh-agent while a mend command runs here, so hardware keys stay on your desk. ambient uses the server's own credentials."
|
|
571
698
|
],
|
|
572
699
|
options: [
|
|
573
700
|
{ flag: "--name <name>", text: "the project's name in Mend. Default: the repository's" },
|
|
574
|
-
{ flag: "--auth <mode>", text: "mend-key, bridge, or ambient. Default: mend keys mode" }
|
|
701
|
+
{ flag: "--auth <mode>", text: "mend-key, bridge, or ambient. Default: mend keys mode" },
|
|
702
|
+
{ flag: "--private", text: "only you see the project. Default" },
|
|
703
|
+
{
|
|
704
|
+
flag: "--shared",
|
|
705
|
+
text: "everyone in your organization sees it and can start sessions in it"
|
|
706
|
+
}
|
|
575
707
|
],
|
|
576
708
|
examples: [
|
|
577
709
|
{ command: "mend adopt", text: "the current repository's origin URL" },
|
|
@@ -648,7 +780,9 @@ var COMMANDS = [
|
|
|
648
780
|
summary: "the dashboard: every project and session, live",
|
|
649
781
|
synopsis: [],
|
|
650
782
|
description: [
|
|
651
|
-
"A full-screen view of every project and session, updating live.
|
|
783
|
+
"A full-screen view of every project and session, updating live. The session pane takes three quarters of the screen: a read-only detail for the selected session with the conversation record it has written so far. The remaining quarter is a sidebar of three stacked sections, projects then worktrees then sessions, where the section you are in stands open and the other two fold to the line that says what is selected. Bare mend with no command opens the same thing.",
|
|
784
|
+
"Moving the selection only ever navigates: arrows or j/k move inside the open section, enter and the arrows move between the sidebar and the session pane, and nothing takes this terminal until you ask. Reading the record leaves the sidebar as it was. The verbs are a to attach a live session, r to resume a settled one, n for another session in the selected worktree, w for a new worktree, e to rename, v to review the change, o to open it in the browser, Shift+K to stop, Shift+D to remove, and Shift+R to refresh.",
|
|
785
|
+
"Nothing is ever squeezed. A terminal too narrow for both gives the whole width to the side you are on, a terminal too short for three drawn panes shows the open section alone, and a one-line breadcrumb states whatever did not fit.",
|
|
652
786
|
"The dashboard needs Node 26 or newer for its terminal. Every other command works on Node 22."
|
|
653
787
|
],
|
|
654
788
|
see: ["sessions", "attach"]
|
|
@@ -1013,7 +1147,166 @@ var COMMANDS = [
|
|
|
1013
1147
|
],
|
|
1014
1148
|
see: ["keys mode", "adopt"]
|
|
1015
1149
|
},
|
|
1150
|
+
// ── organization ───────────────────────────────────────────────────────
|
|
1151
|
+
{
|
|
1152
|
+
name: "members",
|
|
1153
|
+
section: "organization",
|
|
1154
|
+
summary: "who belongs to your organization, and their roles",
|
|
1155
|
+
synopsis: [],
|
|
1156
|
+
description: [
|
|
1157
|
+
"Prints the organization's name and one row per member: name, email, role and the day they joined. The row marked with an arrow is you. Owners change roles and remove members in Settings on the web."
|
|
1158
|
+
],
|
|
1159
|
+
see: ["invite"]
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
name: "invite",
|
|
1163
|
+
section: "organization",
|
|
1164
|
+
summary: "print a one-time link that adds someone to your organization",
|
|
1165
|
+
synopsis: ["[--role member|owner] [--email <address>] [--days <n>]"],
|
|
1166
|
+
description: [
|
|
1167
|
+
"Owners only. Prints a link that works once. Whoever opens it creates an account and joins with the role you pick. Mend sends no email: you share the link yourself. Bind it to an email when it must not be forwarded."
|
|
1168
|
+
],
|
|
1169
|
+
options: [
|
|
1170
|
+
{ flag: "--role <role>", text: "member or owner. Default: member" },
|
|
1171
|
+
{ flag: "--email <address>", text: "only an account with this email may accept it" },
|
|
1172
|
+
{ flag: "--days <n>", text: "days until the link expires. Default: 7, at most 30" }
|
|
1173
|
+
],
|
|
1174
|
+
examples: [{ command: "mend invite --email sam@acme.dev", text: "a member link for Sam only" }],
|
|
1175
|
+
see: ["members"]
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
name: "folder list",
|
|
1179
|
+
section: "organization",
|
|
1180
|
+
summary: "list your organization's folders",
|
|
1181
|
+
synopsis: [],
|
|
1182
|
+
description: [
|
|
1183
|
+
"Folders are directories Mend keeps for the organization. A project mounts the ones it selects at /workspace/home/<name>, read-only unless chosen otherwise, for its next sessions."
|
|
1184
|
+
],
|
|
1185
|
+
see: ["folder create", "folder push"]
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
name: "folder create",
|
|
1189
|
+
section: "organization",
|
|
1190
|
+
summary: "create an organization folder",
|
|
1191
|
+
synopsis: ["<name>"],
|
|
1192
|
+
description: [
|
|
1193
|
+
"Owners only. Names use lowercase letters, digits, dots, underscores and dashes. A project picks the folder in its setup on the web."
|
|
1194
|
+
],
|
|
1195
|
+
see: ["folder push", "folder list"]
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
name: "folder push",
|
|
1199
|
+
section: "organization",
|
|
1200
|
+
summary: "upload a local directory into a folder",
|
|
1201
|
+
synopsis: ["<name> <dir> [--replace]"],
|
|
1202
|
+
description: [
|
|
1203
|
+
"Owners only. Sends every file under <dir>, keeping paths relative to it. .git and node_modules directories, symlinks and files over 1 MiB are skipped and counted. Without --replace the files are added beside what the folder holds."
|
|
1204
|
+
],
|
|
1205
|
+
options: [{ flag: "--replace", text: "empty the folder first, so it holds exactly <dir>" }],
|
|
1206
|
+
examples: [{ command: "mend folder push fixtures ./test/fixtures --replace", text: "" }],
|
|
1207
|
+
see: ["folder create", "folder rm"]
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
name: "folder rm",
|
|
1211
|
+
section: "organization",
|
|
1212
|
+
summary: "remove an organization folder",
|
|
1213
|
+
synopsis: ["<name>"],
|
|
1214
|
+
description: [
|
|
1215
|
+
"Owners only. A folder a project still mounts is refused: deselect it in that project's setup first."
|
|
1216
|
+
],
|
|
1217
|
+
see: ["folder list"]
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
name: "session share",
|
|
1221
|
+
section: "organization",
|
|
1222
|
+
summary: "let everyone who can see a session steer it, or stop",
|
|
1223
|
+
synopsis: ["<session> on|off"],
|
|
1224
|
+
description: [
|
|
1225
|
+
"The session's owner turns shared control on or off; an organization owner may turn it off. While it is on, anyone who can see the project sends turns, answers approvals, interrupts and types in the terminal, using your provider logins and Git access. Every act is recorded with who did it. A prefix of the session id is enough."
|
|
1226
|
+
],
|
|
1227
|
+
examples: [{ command: "mend session share 3f2a on", text: "" }],
|
|
1228
|
+
see: ["sessions"]
|
|
1229
|
+
},
|
|
1016
1230
|
// ── this machine ───────────────────────────────────────────────────────
|
|
1231
|
+
{
|
|
1232
|
+
name: "operator org list",
|
|
1233
|
+
section: "this machine",
|
|
1234
|
+
summary: "list organizations with their member and owner counts",
|
|
1235
|
+
synopsis: [],
|
|
1236
|
+
description: [
|
|
1237
|
+
"Operator only. One line per organization. An organization with no owner says so: bring one in with mend operator org invite-owner or mend operator grant-owner. The operator administers the instance and reads no organization content."
|
|
1238
|
+
],
|
|
1239
|
+
see: ["operator org invite-owner", "operator grant-owner"]
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
name: "operator exposure",
|
|
1243
|
+
section: "this machine",
|
|
1244
|
+
summary: "how this instance is exposed, as declared and as observed",
|
|
1245
|
+
synopsis: [],
|
|
1246
|
+
description: [
|
|
1247
|
+
"Operator only. States MEND_EXPOSURE as declared (loopback, private or public) and one line per item of the public exposure gate, with how it was established: observed (this server read it), carried (this build contains it, and the server cannot see it in effect), declared (you stated it and the server cannot check it), or open. MEND_EXPOSURE=public refuses to start while an item the server can observe is open. Items it cannot observe say what would verify them; once you have verified core-private or edge-tls from outside, name it in MEND_EXPOSURE_DECLARED. The report is what was observed; it is not a statement that the instance is fit to expose."
|
|
1248
|
+
],
|
|
1249
|
+
see: ["operator gate", "doctor"]
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
name: "operator gate",
|
|
1253
|
+
section: "this machine",
|
|
1254
|
+
summary: "what MEND_TENANCY=multi still needs on this instance",
|
|
1255
|
+
synopsis: [],
|
|
1256
|
+
description: [
|
|
1257
|
+
"Operator only. One line per item of the multi mode gate: what this instance shows, and what would satisfy an open item. Multi tenancy refuses to start while any item is open."
|
|
1258
|
+
],
|
|
1259
|
+
see: ["operator org list"]
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
name: "operator org create",
|
|
1263
|
+
section: "this machine",
|
|
1264
|
+
summary: "create an organization (multi tenancy only)",
|
|
1265
|
+
synopsis: ["<name>"],
|
|
1266
|
+
description: [
|
|
1267
|
+
"Operator only, and only when MEND_TENANCY=multi. The organization starts empty: invite its first owner next."
|
|
1268
|
+
],
|
|
1269
|
+
see: ["operator org invite-owner"]
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
name: "operator org rename",
|
|
1273
|
+
section: "this machine",
|
|
1274
|
+
summary: "rename an organization",
|
|
1275
|
+
synopsis: ["<org> <name>"],
|
|
1276
|
+
description: ["Operator only. Recorded in the organization's audit log."],
|
|
1277
|
+
see: ["operator org list"]
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
name: "operator org invite-owner",
|
|
1281
|
+
section: "this machine",
|
|
1282
|
+
summary: "print a one-time owner invitation for an organization",
|
|
1283
|
+
synopsis: ["<org> [--email <address>]"],
|
|
1284
|
+
description: [
|
|
1285
|
+
"Operator only. Prints a link that works once and makes whoever opens it an owner of the organization. Recorded in its audit log."
|
|
1286
|
+
],
|
|
1287
|
+
options: [{ flag: "--email <address>", text: "only an account with this email may accept it" }],
|
|
1288
|
+
see: ["operator grant-owner"]
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
name: "operator grant-owner",
|
|
1292
|
+
section: "this machine",
|
|
1293
|
+
summary: "make an existing member an owner",
|
|
1294
|
+
synopsis: ["<org> <email>"],
|
|
1295
|
+
description: [
|
|
1296
|
+
"Operator only. For an organization whose owners are gone or locked out. The account must already be a member. Recorded in the organization's audit log."
|
|
1297
|
+
],
|
|
1298
|
+
see: ["operator org invite-owner"]
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
name: "operator reset-link",
|
|
1302
|
+
section: "this machine",
|
|
1303
|
+
summary: "print a one-time password reset link for an account",
|
|
1304
|
+
synopsis: ["<email>"],
|
|
1305
|
+
description: [
|
|
1306
|
+
"Operator only. Mend sends no email, so you hand the link over. It works once, expires after a day, and setting a password with it signs the account out everywhere. Owners reset their members' passwords from Settings."
|
|
1307
|
+
],
|
|
1308
|
+
see: ["operator grant-owner"]
|
|
1309
|
+
},
|
|
1017
1310
|
{
|
|
1018
1311
|
name: "server",
|
|
1019
1312
|
section: "this machine",
|
|
@@ -1079,7 +1372,7 @@ var COMMANDS = [
|
|
|
1079
1372
|
{ command: "mend server setup", text: "localhost, using the current local Docker context" },
|
|
1080
1373
|
{
|
|
1081
1374
|
command: "mend server setup --context orbstack --bind 0.0.0.0 --url http://100.70.80.90:3105",
|
|
1082
|
-
text: "explicit tailnet
|
|
1375
|
+
text: "explicit private-network exposure (a tailnet, a LAN, a VPN)"
|
|
1083
1376
|
},
|
|
1084
1377
|
{
|
|
1085
1378
|
command: "mend server setup --version 0.25.0 --assets-dir ./release-assets --offline",
|
|
@@ -1516,7 +1809,7 @@ var manFileName = (doc) => doc === null ? "mend.1" : `mend-${doc.name.replace(/
|
|
|
1516
1809
|
|
|
1517
1810
|
// src/login.ts
|
|
1518
1811
|
import { spawn } from "node:child_process";
|
|
1519
|
-
import * as
|
|
1812
|
+
import * as os2 from "node:os";
|
|
1520
1813
|
|
|
1521
1814
|
// src/pair.ts
|
|
1522
1815
|
var paint = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
@@ -1708,7 +2001,7 @@ var resolveServerUrl = async (args, deps) => {
|
|
|
1708
2001
|
};
|
|
1709
2002
|
var loginCommand = async (args, deps) => {
|
|
1710
2003
|
const base = await resolveServerUrl(args, deps);
|
|
1711
|
-
const started = await post(`${base}/api/cli/auth`, { name:
|
|
2004
|
+
const started = await post(`${base}/api/cli/auth`, { name: os2.hostname() });
|
|
1712
2005
|
if (started.status === 429) {
|
|
1713
2006
|
return fail2(
|
|
1714
2007
|
`the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
|
|
@@ -1758,15 +2051,338 @@ var loginCommand = async (args, deps) => {
|
|
|
1758
2051
|
return fail2("the authorize request expired before anyone approved \u2014 run mend login again");
|
|
1759
2052
|
};
|
|
1760
2053
|
|
|
2054
|
+
// src/organization.ts
|
|
2055
|
+
import * as fs4 from "node:fs";
|
|
2056
|
+
import * as path4 from "node:path";
|
|
2057
|
+
var paint3 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
|
|
2058
|
+
var dim3 = paint3("2");
|
|
2059
|
+
var green3 = paint3("32");
|
|
2060
|
+
var say3 = (line) => process.stdout.write(`${line}
|
|
2061
|
+
`);
|
|
2062
|
+
var fail3 = (message) => {
|
|
2063
|
+
process.stderr.write(`mend: ${message}
|
|
2064
|
+
`);
|
|
2065
|
+
process.exit(1);
|
|
2066
|
+
};
|
|
2067
|
+
var takeFlagValue3 = (args, flag) => {
|
|
2068
|
+
const at = args.indexOf(flag);
|
|
2069
|
+
return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
|
|
2070
|
+
};
|
|
2071
|
+
var day2 = (iso) => iso.slice(0, 10);
|
|
2072
|
+
var organizationOf = (api2) => api2("GET", "/organization");
|
|
2073
|
+
var renderMembers = (view, members) => {
|
|
2074
|
+
const nameWidth = Math.max(4, ...members.map((member) => member.name.length));
|
|
2075
|
+
const emailWidth = Math.max(5, ...members.map((member) => member.email.length));
|
|
2076
|
+
return [
|
|
2077
|
+
`${view.organization.name} \xB7 ${members.length} ${members.length === 1 ? "member" : "members"}`,
|
|
2078
|
+
...members.map(
|
|
2079
|
+
(member) => `${member.userId === view.userId ? "\u25B8" : " "} ${member.name.padEnd(nameWidth)} ${member.email.padEnd(emailWidth)} ${member.role.padEnd(6)} joined ${day2(member.joinedAt)}`
|
|
2080
|
+
)
|
|
2081
|
+
];
|
|
2082
|
+
};
|
|
2083
|
+
var membersCommand = async (api2) => {
|
|
2084
|
+
const view = await organizationOf(api2);
|
|
2085
|
+
const members = await api2("GET", "/organization/members");
|
|
2086
|
+
for (const line of renderMembers(view, members)) say3(line);
|
|
2087
|
+
};
|
|
2088
|
+
var inviteCommand = async (api2, baseUrl, args) => {
|
|
2089
|
+
const role = takeFlagValue3(args, "--role") ?? "member";
|
|
2090
|
+
if (role !== "member" && role !== "owner") {
|
|
2091
|
+
return fail3(`--role takes "member" or "owner", not "${role}"`);
|
|
2092
|
+
}
|
|
2093
|
+
const email = takeFlagValue3(args, "--email");
|
|
2094
|
+
const daysValue = takeFlagValue3(args, "--days");
|
|
2095
|
+
const days = daysValue === null ? null : Number.parseInt(daysValue, 10);
|
|
2096
|
+
if (days !== null && (Number.isNaN(days) || days < 1)) {
|
|
2097
|
+
return fail3(`--days takes a whole number of days, not "${daysValue}"`);
|
|
2098
|
+
}
|
|
2099
|
+
const view = await organizationOf(api2);
|
|
2100
|
+
if (view.role !== "owner") return fail3("only an owner can invite; mend members shows who");
|
|
2101
|
+
const created = await api2("POST", "/organization/invitations", {
|
|
2102
|
+
role,
|
|
2103
|
+
...email === null ? {} : { email },
|
|
2104
|
+
...days === null ? {} : { expiresInDays: days }
|
|
2105
|
+
});
|
|
2106
|
+
say3(`${baseUrl.replace(/\/+$/, "")}${created.path}`);
|
|
2107
|
+
say3(
|
|
2108
|
+
dim3(
|
|
2109
|
+
`works once \xB7 ${role}${email === null ? "" : ` \xB7 for ${email}`} \xB7 expires ${day2(created.invitation.expiresAt)}`
|
|
2110
|
+
)
|
|
2111
|
+
);
|
|
2112
|
+
};
|
|
2113
|
+
var FOLDER_FILE_BYTES = 1024 * 1024;
|
|
2114
|
+
var FOLDER_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
2115
|
+
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2116
|
+
var scanFolder = (dir) => {
|
|
2117
|
+
const files = [];
|
|
2118
|
+
const skipped = [];
|
|
2119
|
+
const walk = (relative3) => {
|
|
2120
|
+
const entries = fs4.readdirSync(path4.join(dir, relative3), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
2121
|
+
for (const entry of entries) {
|
|
2122
|
+
const child = relative3 === "" ? entry.name : `${relative3}/${entry.name}`;
|
|
2123
|
+
if (entry.isSymbolicLink()) {
|
|
2124
|
+
skipped.push({ path: child, reason: "symlink" });
|
|
2125
|
+
} else if (entry.isDirectory()) {
|
|
2126
|
+
if (SKIPPED_DIRECTORIES.has(entry.name)) skipped.push({ path: child, reason: "skipped" });
|
|
2127
|
+
else walk(child);
|
|
2128
|
+
} else if (entry.isFile()) {
|
|
2129
|
+
const size = fs4.statSync(path4.join(dir, child)).size;
|
|
2130
|
+
if (size > FOLDER_FILE_BYTES) skipped.push({ path: child, reason: "over 1 MiB" });
|
|
2131
|
+
else files.push({ path: child, size });
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
walk("");
|
|
2136
|
+
return { files, skipped };
|
|
2137
|
+
};
|
|
2138
|
+
var uploadBatches = (files, cap = FOLDER_REQUEST_BYTES) => {
|
|
2139
|
+
const batches = [];
|
|
2140
|
+
let current = [];
|
|
2141
|
+
let size = 0;
|
|
2142
|
+
for (const file of files) {
|
|
2143
|
+
if (current.length > 0 && size + file.size > cap) {
|
|
2144
|
+
batches.push(current);
|
|
2145
|
+
current = [];
|
|
2146
|
+
size = 0;
|
|
2147
|
+
}
|
|
2148
|
+
current.push(file);
|
|
2149
|
+
size += file.size;
|
|
2150
|
+
}
|
|
2151
|
+
if (current.length > 0) batches.push(current);
|
|
2152
|
+
return batches;
|
|
2153
|
+
};
|
|
2154
|
+
var folderNamed = async (api2, name) => {
|
|
2155
|
+
if (name === void 0) return fail3("name the folder \xB7 mend folder list shows them");
|
|
2156
|
+
const folders = await api2("GET", "/organization/folders");
|
|
2157
|
+
const folder = folders.find((candidate) => candidate.name === name);
|
|
2158
|
+
return folder ?? fail3(`no folder named "${name}" \xB7 mend folder list shows them`);
|
|
2159
|
+
};
|
|
2160
|
+
var folderCommand = async (api2, args) => {
|
|
2161
|
+
const [verb, name, dir] = args.filter((arg) => !arg.startsWith("--"));
|
|
2162
|
+
switch (verb) {
|
|
2163
|
+
case void 0:
|
|
2164
|
+
case "list": {
|
|
2165
|
+
const folders = await api2("GET", "/organization/folders");
|
|
2166
|
+
if (folders.length === 0) {
|
|
2167
|
+
say3(dim3("no folders yet \xB7 an owner creates one with mend folder create <name>"));
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
for (const folder of folders)
|
|
2171
|
+
say3(`${folder.name} ${dim3(`created ${day2(folder.createdAt)}`)}`);
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
case "create": {
|
|
2175
|
+
if (name === void 0) return fail3("usage: mend folder create <name>");
|
|
2176
|
+
const folder = await api2("POST", "/organization/folders", { name });
|
|
2177
|
+
say3(`${green3("\u2713")} created folder ${folder.name}`);
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
case "push": {
|
|
2181
|
+
if (dir === void 0) return fail3("usage: mend folder push <name> <dir> [--replace]");
|
|
2182
|
+
if (!fs4.existsSync(dir) || !fs4.statSync(dir).isDirectory()) {
|
|
2183
|
+
return fail3(`${dir} is not a directory`);
|
|
2184
|
+
}
|
|
2185
|
+
const folder = await folderNamed(api2, name);
|
|
2186
|
+
const scanned = scanFolder(dir);
|
|
2187
|
+
const replace = args.includes("--replace");
|
|
2188
|
+
const batches = uploadBatches(scanned.files);
|
|
2189
|
+
if (batches.length === 0 && replace) {
|
|
2190
|
+
await api2("POST", `/organization/folders/${folder.id}/files`, { files: [], merge: false });
|
|
2191
|
+
}
|
|
2192
|
+
for (const [index, batch] of batches.entries()) {
|
|
2193
|
+
await api2("POST", `/organization/folders/${folder.id}/files`, {
|
|
2194
|
+
files: batch.map((file) => ({
|
|
2195
|
+
path: file.path,
|
|
2196
|
+
// Read one batch at a time, so a large tree never sits in memory at once.
|
|
2197
|
+
contentsBase64: fs4.readFileSync(path4.join(dir, file.path)).toString("base64")
|
|
2198
|
+
})),
|
|
2199
|
+
// --replace empties the folder with the first batch; the rest add to it.
|
|
2200
|
+
merge: !(replace && index === 0)
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
const reasons = [...new Set(scanned.skipped.map((entry) => entry.reason))];
|
|
2204
|
+
say3(
|
|
2205
|
+
`${green3("\u2713")} pushed ${scanned.files.length} ${scanned.files.length === 1 ? "file" : "files"} to ${folder.name}${scanned.skipped.length === 0 ? "" : dim3(` \xB7 ${scanned.skipped.length} skipped (${reasons.join(", ")})`)}`
|
|
2206
|
+
);
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
case "rm": {
|
|
2210
|
+
const folder = await folderNamed(api2, name);
|
|
2211
|
+
await api2("DELETE", `/organization/folders/${folder.id}`);
|
|
2212
|
+
say3(`${green3("\u2713")} removed folder ${folder.name}`);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
default:
|
|
2216
|
+
return fail3(`unknown folder command "${verb}" \xB7 mend help folder`);
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
var sessionShareCommand = async (api2, args) => {
|
|
2220
|
+
const [prefix, state] = args.filter((arg) => !arg.startsWith("--"));
|
|
2221
|
+
if (prefix === void 0 || state !== "on" && state !== "off") {
|
|
2222
|
+
return fail3("usage: mend session share <session> on|off");
|
|
2223
|
+
}
|
|
2224
|
+
const sessions = await api2(
|
|
2225
|
+
"GET",
|
|
2226
|
+
"/sessions?retained=1"
|
|
2227
|
+
);
|
|
2228
|
+
const matches = sessions.filter((session2) => session2.id.startsWith(prefix));
|
|
2229
|
+
if (matches.length === 0) return fail3(`no live session starts with "${prefix}"`);
|
|
2230
|
+
if (matches.length > 1)
|
|
2231
|
+
return fail3(`"${prefix}" matches ${matches.length} sessions; type more of the id`);
|
|
2232
|
+
const [session] = matches;
|
|
2233
|
+
if (session === void 0) return fail3(`no live session starts with "${prefix}"`);
|
|
2234
|
+
await api2("PUT", `/sessions/${session.id}/shared-control`, { enabled: state === "on" });
|
|
2235
|
+
say3(
|
|
2236
|
+
state === "on" ? `${green3("\u2713")} shared control on \xB7 ${session.harness} ${dim3(session.id.slice(0, 8))} \xB7 everyone who can see the project steers it on your credentials` : `${green3("\u2713")} shared control off \xB7 ${session.harness} ${dim3(session.id.slice(0, 8))}`
|
|
2237
|
+
);
|
|
2238
|
+
};
|
|
2239
|
+
var renderGate = (items) => {
|
|
2240
|
+
const width = Math.max(...items.map((item) => item.id.length));
|
|
2241
|
+
const open = items.filter((item) => !item.ok).length;
|
|
2242
|
+
return [
|
|
2243
|
+
...items.map(
|
|
2244
|
+
(item) => `${item.ok ? green3("\u2713") : "\xB7"} ${item.id.padEnd(width)} ${item.detail}${item.fix === null ? "" : dim3(` \xB7 ${item.fix}`)}`
|
|
2245
|
+
),
|
|
2246
|
+
open === 0 ? "every item is in place; MEND_TENANCY=multi may start" : `${open} of ${items.length} items open; MEND_TENANCY=multi refuses to start`
|
|
2247
|
+
];
|
|
2248
|
+
};
|
|
2249
|
+
var EXPOSURE_WORDS = {
|
|
2250
|
+
loopback: "reached from this machine only",
|
|
2251
|
+
private: "reached over a network you control admission to",
|
|
2252
|
+
public: "reachable from the Internet"
|
|
2253
|
+
};
|
|
2254
|
+
var renderExposure = (report) => {
|
|
2255
|
+
const width = Math.max(...report.items.map((item) => item.id.length));
|
|
2256
|
+
const marks = {
|
|
2257
|
+
observed: "\u25CF",
|
|
2258
|
+
carried: "\u25D0",
|
|
2259
|
+
declared: "\u25CB",
|
|
2260
|
+
open: "\xB7"
|
|
2261
|
+
};
|
|
2262
|
+
const mark = (item) => marks[item.established];
|
|
2263
|
+
const open = report.items.filter((item) => item.established === "open");
|
|
2264
|
+
const blocking = open.filter((item) => item.blocksStart).length;
|
|
2265
|
+
const unobservable = open.length - blocking;
|
|
2266
|
+
return [
|
|
2267
|
+
`exposure \xB7 declared ${report.declared} \xB7 ${EXPOSURE_WORDS[report.declared]}`,
|
|
2268
|
+
...report.items.map(
|
|
2269
|
+
(item) => `${mark(item)} ${item.id.padEnd(width)} ${item.established.padEnd(8)} ${item.detail}${item.fix === null ? "" : dim3(` \xB7 ${item.fix}`)}`
|
|
2270
|
+
),
|
|
2271
|
+
open.length === 0 ? "nothing open \xB7 every item was observed here, is carried by this build, or was declared by the operator" : `${open.length} of ${report.items.length} items open \xB7 ${blocking} this build can observe${blocking === 0 ? "" : "; MEND_EXPOSURE=public refuses to start"} \xB7 ${unobservable} it cannot`
|
|
2272
|
+
];
|
|
2273
|
+
};
|
|
2274
|
+
var renderOrganizations = (rows) => {
|
|
2275
|
+
if (rows.length === 0) return ["no organizations"];
|
|
2276
|
+
const width = Math.max(...rows.map((row) => row.organization.name.length));
|
|
2277
|
+
return rows.map(
|
|
2278
|
+
(row) => `${row.organization.name.padEnd(width)} ${row.memberCount} ${row.memberCount === 1 ? "member" : "members"} \xB7 ${row.ownerCount === 0 ? "no owner" : `${row.ownerCount} ${row.ownerCount === 1 ? "owner" : "owners"}`}`
|
|
2279
|
+
);
|
|
2280
|
+
};
|
|
2281
|
+
var printLink = (baseUrl, link, what) => {
|
|
2282
|
+
say3(`${baseUrl.replace(/\/+$/, "")}${link.path}`);
|
|
2283
|
+
say3(dim3(`${what} \xB7 works once \xB7 expires ${day2(link.expiresAt)}`));
|
|
2284
|
+
};
|
|
2285
|
+
var operatorCommand = async (api2, tryApi, baseUrl, args) => {
|
|
2286
|
+
const words = args.filter((arg, index) => !arg.startsWith("--") && args[index - 1] !== "--email");
|
|
2287
|
+
const refused = await tryApi("GET", "/operator/organizations").then(
|
|
2288
|
+
() => null,
|
|
2289
|
+
(error) => error instanceof Error ? error.message : String(error)
|
|
2290
|
+
);
|
|
2291
|
+
if (refused !== null) {
|
|
2292
|
+
return fail3(
|
|
2293
|
+
refused.endsWith("\u2192 404") ? "this account is not the operator of this Mend" : refused
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
const organizationNamed = async (name) => {
|
|
2297
|
+
if (name === void 0)
|
|
2298
|
+
return fail3("name the organization \xB7 mend operator org list shows them");
|
|
2299
|
+
const rows = await api2("GET", "/operator/organizations");
|
|
2300
|
+
const row = rows.find((candidate) => candidate.organization.name === name);
|
|
2301
|
+
return row?.organization ?? fail3(`no organization named "${name}"`);
|
|
2302
|
+
};
|
|
2303
|
+
const [first, second, third, fourth] = words;
|
|
2304
|
+
if (first === "org") {
|
|
2305
|
+
switch (second) {
|
|
2306
|
+
case void 0:
|
|
2307
|
+
case "list": {
|
|
2308
|
+
const rows = await api2(
|
|
2309
|
+
"GET",
|
|
2310
|
+
"/operator/organizations"
|
|
2311
|
+
);
|
|
2312
|
+
for (const line of renderOrganizations(rows)) say3(line);
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
case "create": {
|
|
2316
|
+
if (third === void 0) return fail3("usage: mend operator org create <name>");
|
|
2317
|
+
const created = await api2("POST", "/operator/organizations", {
|
|
2318
|
+
name: third
|
|
2319
|
+
});
|
|
2320
|
+
say3(
|
|
2321
|
+
`${green3("\u2713")} created ${created.name} \xB7 mend operator org invite-owner ${created.name}`
|
|
2322
|
+
);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
case "rename": {
|
|
2326
|
+
if (fourth === void 0) return fail3("usage: mend operator org rename <org> <name>");
|
|
2327
|
+
const organization = await organizationNamed(third);
|
|
2328
|
+
await api2("PUT", `/operator/organizations/${organization.id}/name`, { name: fourth });
|
|
2329
|
+
say3(`${green3("\u2713")} renamed ${organization.name} to ${fourth}`);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
case "invite-owner": {
|
|
2333
|
+
const organization = await organizationNamed(third);
|
|
2334
|
+
const email = takeFlagValue3(args, "--email");
|
|
2335
|
+
const link = await api2(
|
|
2336
|
+
"POST",
|
|
2337
|
+
`/operator/organizations/${organization.id}/invitations`,
|
|
2338
|
+
email === null ? {} : { email }
|
|
2339
|
+
);
|
|
2340
|
+
printLink(
|
|
2341
|
+
baseUrl,
|
|
2342
|
+
link,
|
|
2343
|
+
`owner of ${organization.name}${email === null ? "" : ` \xB7 for ${email}`}`
|
|
2344
|
+
);
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
default:
|
|
2348
|
+
return fail3(`unknown operator org command "${second}" \xB7 mend help operator org list`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
if (first === "exposure") {
|
|
2352
|
+
const report = await api2("GET", "/operator/exposure");
|
|
2353
|
+
for (const line of renderExposure(report)) say3(line);
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
if (first === "gate") {
|
|
2357
|
+
const items = await api2("GET", "/operator/gate");
|
|
2358
|
+
for (const line of renderGate(items)) say3(line);
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
if (first === "grant-owner") {
|
|
2362
|
+
if (third === void 0) return fail3("usage: mend operator grant-owner <org> <email>");
|
|
2363
|
+
const organization = await organizationNamed(second);
|
|
2364
|
+
await api2("POST", `/operator/organizations/${organization.id}/owners`, { email: third });
|
|
2365
|
+
say3(`${green3("\u2713")} ${third} is an owner of ${organization.name}`);
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (first === "reset-link") {
|
|
2369
|
+
if (second === void 0) return fail3("usage: mend operator reset-link <email>");
|
|
2370
|
+
const link = await api2("POST", "/operator/password-resets", { email: second });
|
|
2371
|
+
printLink(baseUrl, link, `password reset for ${second}`);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
return fail3(`unknown operator command "${first ?? ""}" \xB7 mend help operator org list`);
|
|
2375
|
+
};
|
|
2376
|
+
|
|
1761
2377
|
// src/server-setup.ts
|
|
1762
2378
|
import { createHmac, randomBytes } from "node:crypto";
|
|
1763
2379
|
import { readFile } from "node:fs/promises";
|
|
1764
2380
|
import * as net2 from "node:net";
|
|
1765
|
-
import * as
|
|
1766
|
-
import * as
|
|
2381
|
+
import * as os4 from "node:os";
|
|
2382
|
+
import * as path7 from "node:path";
|
|
1767
2383
|
|
|
1768
2384
|
// src/server-docker-volumes.ts
|
|
1769
|
-
import { createHash } from "node:crypto";
|
|
2385
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1770
2386
|
var MEND_DOCKER_NAMESPACE = {
|
|
1771
2387
|
project: "mend",
|
|
1772
2388
|
store: "mend-store",
|
|
@@ -1794,15 +2410,15 @@ var ServerVolumeOwnershipError = class extends Error {
|
|
|
1794
2410
|
this.operation = operation;
|
|
1795
2411
|
}
|
|
1796
2412
|
};
|
|
1797
|
-
var
|
|
2413
|
+
var fail4 = (reason, operation) => ({ _tag: "error", error: new ServerVolumeOwnershipError(reason, operation) });
|
|
1798
2414
|
var docker = async (runtime, context, args) => {
|
|
1799
2415
|
try {
|
|
1800
2416
|
const output = await runtime.run("docker", ["--context", context, ...args]);
|
|
1801
2417
|
if (output.status !== 0 || output.error !== void 0)
|
|
1802
|
-
return
|
|
2418
|
+
return fail4("docker", args.slice(0, 2).join(" "));
|
|
1803
2419
|
return { _tag: "ok", value: output.stdout };
|
|
1804
2420
|
} catch {
|
|
1805
|
-
return
|
|
2421
|
+
return fail4("docker", args.slice(0, 2).join(" "));
|
|
1806
2422
|
}
|
|
1807
2423
|
};
|
|
1808
2424
|
var listNames = async (runtime, context, kind, project2) => {
|
|
@@ -1821,11 +2437,11 @@ var listNames = async (runtime, context, kind, project2) => {
|
|
|
1821
2437
|
for (const line of output.value.trim().split("\n").filter(Boolean)) {
|
|
1822
2438
|
const value = JSON.parse(line);
|
|
1823
2439
|
if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value))
|
|
1824
|
-
return
|
|
2440
|
+
return fail4("inspection", `${kind} list`);
|
|
1825
2441
|
names.push(value);
|
|
1826
2442
|
}
|
|
1827
2443
|
} catch {
|
|
1828
|
-
return
|
|
2444
|
+
return fail4("inspection", `${kind} list`);
|
|
1829
2445
|
}
|
|
1830
2446
|
return { _tag: "ok", value: names };
|
|
1831
2447
|
};
|
|
@@ -1841,16 +2457,16 @@ var inspectOwner = async (runtime, context, name, owner) => {
|
|
|
1841
2457
|
try {
|
|
1842
2458
|
const volume = JSON.parse(output.value);
|
|
1843
2459
|
if (typeof volume !== "object" || volume === null || !("Name" in volume) || volume.Name !== name || !("Labels" in volume))
|
|
1844
|
-
return
|
|
2460
|
+
return fail4("inspection", "volume inspect");
|
|
1845
2461
|
const labels = volume.Labels;
|
|
1846
|
-
if (labels === null) return
|
|
2462
|
+
if (labels === null) return fail4("conflict", "volume owner");
|
|
1847
2463
|
if (typeof labels !== "object" || Array.isArray(labels) || !Object.values(labels).every((v) => typeof v === "string"))
|
|
1848
|
-
return
|
|
2464
|
+
return fail4("inspection", "volume labels");
|
|
1849
2465
|
if (!(SERVER_VOLUME_OWNER_LABEL in labels) || labels[SERVER_VOLUME_OWNER_LABEL] !== owner)
|
|
1850
|
-
return
|
|
2466
|
+
return fail4("conflict", "volume owner");
|
|
1851
2467
|
return { _tag: "ok", value: void 0 };
|
|
1852
2468
|
} catch {
|
|
1853
|
-
return
|
|
2469
|
+
return fail4("inspection", "volume inspect");
|
|
1854
2470
|
}
|
|
1855
2471
|
};
|
|
1856
2472
|
var ensureVolume = async (runtime, context, name, owner) => {
|
|
@@ -1889,38 +2505,38 @@ var inspectForeignProject = async (runtime, context, kind, name, project2) => {
|
|
|
1889
2505
|
try {
|
|
1890
2506
|
const resource = JSON.parse(output.value);
|
|
1891
2507
|
if (typeof resource !== "object" || resource === null || !("Name" in resource) || resource.Name !== (kind === "container" ? `/${name}` : name) || !("Labels" in resource))
|
|
1892
|
-
return
|
|
2508
|
+
return fail4("inspection", `${kind} inspect`);
|
|
1893
2509
|
const labels = resource.Labels;
|
|
1894
|
-
if (labels === null) return
|
|
2510
|
+
if (labels === null) return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1895
2511
|
if (typeof labels !== "object" || Array.isArray(labels) || !Object.values(labels).every((value) => typeof value === "string"))
|
|
1896
|
-
return
|
|
2512
|
+
return fail4("inspection", `${kind} labels`);
|
|
1897
2513
|
if (!("com.docker.compose.project" in labels))
|
|
1898
|
-
return
|
|
2514
|
+
return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1899
2515
|
const foreignProject = labels["com.docker.compose.project"];
|
|
1900
2516
|
if (typeof foreignProject !== "string" || !/^[a-z0-9][a-z0-9_-]*$/.test(foreignProject))
|
|
1901
|
-
return
|
|
2517
|
+
return fail4("inspection", `${kind} project label`);
|
|
1902
2518
|
if (foreignProject === project2 || !inProjectNamespace(name, foreignProject))
|
|
1903
|
-
return
|
|
2519
|
+
return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1904
2520
|
return { _tag: "ok", value: void 0 };
|
|
1905
2521
|
} catch {
|
|
1906
|
-
return
|
|
2522
|
+
return fail4("inspection", `${kind} inspect`);
|
|
1907
2523
|
}
|
|
1908
2524
|
};
|
|
1909
2525
|
var refuseOldData = async (runtime, context, namespace, volumes) => {
|
|
1910
2526
|
if (volumes.some(
|
|
1911
2527
|
(name) => secondaryVolumesOf(namespace).includes(name) || name.startsWith(`${namespace.project}_`)
|
|
1912
2528
|
))
|
|
1913
|
-
return
|
|
2529
|
+
return fail4("unowned-data", "existing volumes without anchor");
|
|
1914
2530
|
for (const kind of ["volume", "container", "network"]) {
|
|
1915
2531
|
const labelled = await listNames(runtime, context, kind, namespace.project);
|
|
1916
2532
|
if (labelled._tag === "error") return labelled;
|
|
1917
|
-
if (labelled.value.length > 0) return
|
|
2533
|
+
if (labelled.value.length > 0) return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1918
2534
|
if (kind !== "volume") {
|
|
1919
2535
|
const named = await listNames(runtime, context, kind);
|
|
1920
2536
|
if (named._tag === "error") return named;
|
|
1921
2537
|
for (const name of named.value) {
|
|
1922
2538
|
if (reservedResourceName(name, kind, namespace))
|
|
1923
|
-
return
|
|
2539
|
+
return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1924
2540
|
if (name.startsWith(`${namespace.project}_`) || name.startsWith(`${namespace.project}-`)) {
|
|
1925
2541
|
const foreign = await inspectForeignProject(
|
|
1926
2542
|
runtime,
|
|
@@ -1941,8 +2557,8 @@ var parseOwnershipInput = (input) => {
|
|
|
1941
2557
|
if (!input.dockerContext.trim() || input.identityBytes.byteLength === 0 || !/^[a-z0-9][a-z0-9_-]*$/.test(namespace.project) || ![namespace.store, ...secondaryVolumesOf(namespace)].every(
|
|
1942
2558
|
(name) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/.test(name)
|
|
1943
2559
|
) || (/* @__PURE__ */ new Set([namespace.store, ...secondaryVolumesOf(namespace)])).size !== 1 + secondaryVolumesOf(namespace).length)
|
|
1944
|
-
return
|
|
1945
|
-
const owner =
|
|
2560
|
+
return fail4("invalid-input", "ownership inputs");
|
|
2561
|
+
const owner = createHash2("sha256").update(input.identityBytes).digest("hex");
|
|
1946
2562
|
return { _tag: "ok", value: { owner, namespace } };
|
|
1947
2563
|
};
|
|
1948
2564
|
var ownership = async (runtime, input, mode) => {
|
|
@@ -1955,7 +2571,7 @@ var ownership = async (runtime, input, mode) => {
|
|
|
1955
2571
|
const anchor = await inspectOwner(runtime, input.dockerContext, namespace.store, owner);
|
|
1956
2572
|
if (anchor._tag === "error") return anchor;
|
|
1957
2573
|
} else {
|
|
1958
|
-
if (mode === "verify") return
|
|
2574
|
+
if (mode === "verify") return fail4("missing", "anchor");
|
|
1959
2575
|
const empty = await refuseOldData(runtime, input.dockerContext, namespace, volumes.value);
|
|
1960
2576
|
if (empty._tag === "error") return empty;
|
|
1961
2577
|
const anchor = await ensureVolume(runtime, input.dockerContext, namespace.store, owner);
|
|
@@ -1969,7 +2585,7 @@ var ownership = async (runtime, input, mode) => {
|
|
|
1969
2585
|
const owned = await inspectOwner(runtime, input.dockerContext, name, owner);
|
|
1970
2586
|
if (owned._tag === "error") return owned;
|
|
1971
2587
|
} else {
|
|
1972
|
-
if (mode === "verify") return
|
|
2588
|
+
if (mode === "verify") return fail4("missing", role);
|
|
1973
2589
|
const created = await ensureVolume(runtime, input.dockerContext, name, owner);
|
|
1974
2590
|
if (created._tag === "error") return created;
|
|
1975
2591
|
}
|
|
@@ -1981,8 +2597,8 @@ var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "v
|
|
|
1981
2597
|
|
|
1982
2598
|
// src/server-runtime.ts
|
|
1983
2599
|
import { spawn as spawn2 } from "node:child_process";
|
|
1984
|
-
import * as
|
|
1985
|
-
import * as
|
|
2600
|
+
import * as fs5 from "node:fs";
|
|
2601
|
+
import * as path5 from "node:path";
|
|
1986
2602
|
var serverProcessDeadlines = {
|
|
1987
2603
|
ordinary: 3e4,
|
|
1988
2604
|
pull: 10 * 6e4,
|
|
@@ -2028,7 +2644,7 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
|
|
|
2028
2644
|
return new Promise((resolve4) => {
|
|
2029
2645
|
let fd;
|
|
2030
2646
|
try {
|
|
2031
|
-
if (options.stdoutFile !== void 0) fd =
|
|
2647
|
+
if (options.stdoutFile !== void 0) fd = fs5.openSync(options.stdoutFile, "wx", 384);
|
|
2032
2648
|
const child = spawn2(command, [...args], {
|
|
2033
2649
|
env,
|
|
2034
2650
|
detached: process.platform !== "win32",
|
|
@@ -2079,12 +2695,12 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
|
|
|
2079
2695
|
child.once("close", (status) => {
|
|
2080
2696
|
globalThis.clearTimeout(timer);
|
|
2081
2697
|
try {
|
|
2082
|
-
if (fd !== void 0)
|
|
2698
|
+
if (fd !== void 0) fs5.fsyncSync(fd);
|
|
2083
2699
|
} catch {
|
|
2084
2700
|
failure2 ??= "Could not durably write process output.";
|
|
2085
2701
|
} finally {
|
|
2086
2702
|
try {
|
|
2087
|
-
if (fd !== void 0)
|
|
2703
|
+
if (fd !== void 0) fs5.closeSync(fd);
|
|
2088
2704
|
} catch {
|
|
2089
2705
|
failure2 ??= "Could not close process output file.";
|
|
2090
2706
|
}
|
|
@@ -2098,7 +2714,7 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
|
|
|
2098
2714
|
});
|
|
2099
2715
|
} catch (cause) {
|
|
2100
2716
|
try {
|
|
2101
|
-
if (fd !== void 0)
|
|
2717
|
+
if (fd !== void 0) fs5.closeSync(fd);
|
|
2102
2718
|
} catch {
|
|
2103
2719
|
}
|
|
2104
2720
|
resolve4({
|
|
@@ -2119,17 +2735,17 @@ var serverComposeArgs = (installation, command) => [
|
|
|
2119
2735
|
"--project-directory",
|
|
2120
2736
|
installation.directory,
|
|
2121
2737
|
"--env-file",
|
|
2122
|
-
|
|
2738
|
+
path5.join(installation.directory, "server.env"),
|
|
2123
2739
|
"-f",
|
|
2124
|
-
|
|
2740
|
+
path5.join(installation.directory, "compose.yaml"),
|
|
2125
2741
|
...command
|
|
2126
2742
|
];
|
|
2127
2743
|
|
|
2128
2744
|
// src/server-store.ts
|
|
2129
2745
|
import { randomUUID } from "node:crypto";
|
|
2130
|
-
import * as
|
|
2131
|
-
import * as
|
|
2132
|
-
import * as
|
|
2746
|
+
import * as fs6 from "node:fs";
|
|
2747
|
+
import * as os3 from "node:os";
|
|
2748
|
+
import * as path6 from "node:path";
|
|
2133
2749
|
var ServerStoreError = class extends Error {
|
|
2134
2750
|
_tag = "ServerStoreError";
|
|
2135
2751
|
};
|
|
@@ -2150,28 +2766,28 @@ var attempt = (operation) => {
|
|
|
2150
2766
|
var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
|
|
2151
2767
|
var readOptional = (file) => {
|
|
2152
2768
|
try {
|
|
2153
|
-
return
|
|
2769
|
+
return fs6.readFileSync(file, "utf8");
|
|
2154
2770
|
} catch (cause) {
|
|
2155
2771
|
if (hasCode(cause, "ENOENT")) return null;
|
|
2156
2772
|
throw cause;
|
|
2157
2773
|
}
|
|
2158
2774
|
};
|
|
2159
2775
|
var syncDirectory = (directory) => {
|
|
2160
|
-
const fd =
|
|
2776
|
+
const fd = fs6.openSync(directory, "r");
|
|
2161
2777
|
try {
|
|
2162
|
-
|
|
2778
|
+
fs6.fsyncSync(fd);
|
|
2163
2779
|
} finally {
|
|
2164
|
-
|
|
2780
|
+
fs6.closeSync(fd);
|
|
2165
2781
|
}
|
|
2166
2782
|
};
|
|
2167
2783
|
var writeDurable = (file, content, mode = 384) => {
|
|
2168
|
-
const fd =
|
|
2784
|
+
const fd = fs6.openSync(file, "wx", mode);
|
|
2169
2785
|
try {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2786
|
+
fs6.writeFileSync(fd, content, "utf8");
|
|
2787
|
+
fs6.fchmodSync(fd, mode);
|
|
2788
|
+
fs6.fsyncSync(fd);
|
|
2173
2789
|
} finally {
|
|
2174
|
-
|
|
2790
|
+
fs6.closeSync(fd);
|
|
2175
2791
|
}
|
|
2176
2792
|
};
|
|
2177
2793
|
var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
|
|
@@ -2191,11 +2807,11 @@ var parseLockOwner = (raw) => {
|
|
|
2191
2807
|
};
|
|
2192
2808
|
var describeOwner = (lockDir) => {
|
|
2193
2809
|
try {
|
|
2194
|
-
const raw = JSON.parse(
|
|
2810
|
+
const raw = JSON.parse(fs6.readFileSync(path6.join(lockDir, "owner.json"), "utf8"));
|
|
2195
2811
|
const owner = parseLockOwner(raw);
|
|
2196
2812
|
if (owner === null) return "owner metadata is invalid";
|
|
2197
2813
|
const label = `PID ${owner.pid} on ${owner.hostname}`;
|
|
2198
|
-
if (owner.hostname !==
|
|
2814
|
+
if (owner.hostname !== os3.hostname()) return label;
|
|
2199
2815
|
try {
|
|
2200
2816
|
process.kill(owner.pid, 0);
|
|
2201
2817
|
return `${label} is still live`;
|
|
@@ -2211,28 +2827,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
|
|
|
2211
2827
|
);
|
|
2212
2828
|
var acquireLock = (configDir, create) => {
|
|
2213
2829
|
if (create) {
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
} else if (!
|
|
2830
|
+
fs6.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
2831
|
+
fs6.chmodSync(configDir, 448);
|
|
2832
|
+
} else if (!fs6.existsSync(configDir)) {
|
|
2217
2833
|
throw new ServerStoreError(
|
|
2218
2834
|
"No Mend server is configured. Run mend server setup explicitly to install one."
|
|
2219
2835
|
);
|
|
2220
2836
|
}
|
|
2221
|
-
const lockDir =
|
|
2837
|
+
const lockDir = path6.join(configDir, "server.lock");
|
|
2222
2838
|
try {
|
|
2223
|
-
|
|
2839
|
+
fs6.mkdirSync(lockDir, { mode: 448 });
|
|
2224
2840
|
} catch (cause) {
|
|
2225
2841
|
if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
|
|
2226
2842
|
throw cause;
|
|
2227
2843
|
}
|
|
2228
|
-
const owned =
|
|
2229
|
-
const ownerFile =
|
|
2230
|
-
const owner = `${JSON.stringify({ pid: process.pid, hostname:
|
|
2844
|
+
const owned = fs6.statSync(lockDir);
|
|
2845
|
+
const ownerFile = path6.join(lockDir, "owner.json");
|
|
2846
|
+
const owner = `${JSON.stringify({ pid: process.pid, hostname: os3.hostname(), token: randomUUID() })}
|
|
2231
2847
|
`;
|
|
2232
2848
|
let open = true;
|
|
2233
2849
|
const assertOwned = () => {
|
|
2234
|
-
const current =
|
|
2235
|
-
if (!open || current.dev !== owned.dev || current.ino !== owned.ino ||
|
|
2850
|
+
const current = fs6.statSync(lockDir);
|
|
2851
|
+
if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs6.readFileSync(ownerFile, "utf8") !== owner) {
|
|
2236
2852
|
throw new ServerStoreError(
|
|
2237
2853
|
"Server lock ownership was lost. Stop and check for another server command before retrying."
|
|
2238
2854
|
);
|
|
@@ -2248,8 +2864,8 @@ var acquireLock = (configDir, create) => {
|
|
|
2248
2864
|
release: () => {
|
|
2249
2865
|
const result = attempt(() => {
|
|
2250
2866
|
assertOwned();
|
|
2251
|
-
|
|
2252
|
-
|
|
2867
|
+
fs6.unlinkSync(ownerFile);
|
|
2868
|
+
fs6.rmdirSync(lockDir);
|
|
2253
2869
|
});
|
|
2254
2870
|
open = false;
|
|
2255
2871
|
return result;
|
|
@@ -2258,7 +2874,7 @@ var acquireLock = (configDir, create) => {
|
|
|
2258
2874
|
};
|
|
2259
2875
|
var readIdentity = (paths) => {
|
|
2260
2876
|
const identity = readOptional(paths.identity);
|
|
2261
|
-
if (identity === null && (
|
|
2877
|
+
if (identity === null && (fs6.existsSync(paths.active) || fs6.existsSync(paths.generations) && fs6.readdirSync(paths.generations).length > 0)) {
|
|
2262
2878
|
throw new ServerStoreError(
|
|
2263
2879
|
"Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
|
|
2264
2880
|
);
|
|
@@ -2268,7 +2884,7 @@ var readIdentity = (paths) => {
|
|
|
2268
2884
|
var readActive = (paths) => {
|
|
2269
2885
|
let target;
|
|
2270
2886
|
try {
|
|
2271
|
-
target =
|
|
2887
|
+
target = fs6.readlinkSync(paths.active);
|
|
2272
2888
|
} catch (cause) {
|
|
2273
2889
|
if (hasCode(cause, "ENOENT")) return null;
|
|
2274
2890
|
throw cause;
|
|
@@ -2278,13 +2894,13 @@ var readActive = (paths) => {
|
|
|
2278
2894
|
"Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
|
|
2279
2895
|
);
|
|
2280
2896
|
}
|
|
2281
|
-
const directory =
|
|
2897
|
+
const directory = path6.join(paths.configDir, target);
|
|
2282
2898
|
const files = {
|
|
2283
|
-
identity:
|
|
2284
|
-
config:
|
|
2285
|
-
env:
|
|
2286
|
-
compose:
|
|
2287
|
-
postgresInit:
|
|
2899
|
+
identity: fs6.readFileSync(path6.join(directory, fileNames.identity), "utf8"),
|
|
2900
|
+
config: fs6.readFileSync(path6.join(directory, fileNames.config), "utf8"),
|
|
2901
|
+
env: fs6.readFileSync(path6.join(directory, fileNames.env), "utf8"),
|
|
2902
|
+
compose: fs6.readFileSync(path6.join(directory, fileNames.compose), "utf8"),
|
|
2903
|
+
postgresInit: fs6.readFileSync(path6.join(directory, fileNames.postgresInit), "utf8")
|
|
2288
2904
|
};
|
|
2289
2905
|
if (readIdentity(paths) !== files.identity) {
|
|
2290
2906
|
throw new ServerStoreError(
|
|
@@ -2294,20 +2910,20 @@ var readActive = (paths) => {
|
|
|
2294
2910
|
return { directory, files };
|
|
2295
2911
|
};
|
|
2296
2912
|
var publishIdentity = (paths, identity) => {
|
|
2297
|
-
const temporary =
|
|
2913
|
+
const temporary = path6.join(paths.configDir, `.identity-${randomUUID()}`);
|
|
2298
2914
|
writeDurable(temporary, identity);
|
|
2299
|
-
|
|
2915
|
+
fs6.linkSync(temporary, paths.identity);
|
|
2300
2916
|
syncDirectory(paths.configDir);
|
|
2301
|
-
|
|
2917
|
+
fs6.unlinkSync(temporary);
|
|
2302
2918
|
};
|
|
2303
2919
|
var prepareGeneration = (paths, files) => {
|
|
2304
|
-
|
|
2920
|
+
fs6.mkdirSync(paths.generations, { recursive: true, mode: 448 });
|
|
2305
2921
|
const generationName = `gen-${randomUUID()}`;
|
|
2306
|
-
const directory =
|
|
2307
|
-
|
|
2922
|
+
const directory = path6.join(paths.generations, generationName);
|
|
2923
|
+
fs6.mkdirSync(directory, { mode: 448 });
|
|
2308
2924
|
for (const key of fileKeys) {
|
|
2309
2925
|
writeDurable(
|
|
2310
|
-
|
|
2926
|
+
path6.join(directory, fileNames[key]),
|
|
2311
2927
|
files[key],
|
|
2312
2928
|
key === "postgresInit" ? 493 : 384
|
|
2313
2929
|
);
|
|
@@ -2318,18 +2934,18 @@ var prepareGeneration = (paths, files) => {
|
|
|
2318
2934
|
return { directory, files };
|
|
2319
2935
|
};
|
|
2320
2936
|
var activateGeneration = (paths, generation) => {
|
|
2321
|
-
const relative3 =
|
|
2322
|
-
if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative3) || readIdentity(paths) !==
|
|
2937
|
+
const relative3 = path6.relative(paths.configDir, generation.directory);
|
|
2938
|
+
if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative3) || readIdentity(paths) !== fs6.readFileSync(path6.join(generation.directory, "identity.env"), "utf8")) {
|
|
2323
2939
|
throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
|
|
2324
2940
|
}
|
|
2325
2941
|
for (const key of fileKeys) {
|
|
2326
|
-
if (
|
|
2942
|
+
if (fs6.readFileSync(path6.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
|
|
2327
2943
|
throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
|
|
2328
2944
|
}
|
|
2329
2945
|
}
|
|
2330
|
-
const pointer =
|
|
2331
|
-
|
|
2332
|
-
|
|
2946
|
+
const pointer = path6.join(paths.configDir, `.active-${randomUUID()}`);
|
|
2947
|
+
fs6.symlinkSync(relative3, pointer);
|
|
2948
|
+
fs6.renameSync(pointer, paths.active);
|
|
2333
2949
|
syncDirectory(paths.configDir);
|
|
2334
2950
|
};
|
|
2335
2951
|
var prepareFiles = (paths, files) => {
|
|
@@ -2338,7 +2954,7 @@ var prepareFiles = (paths, files) => {
|
|
|
2338
2954
|
throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
|
|
2339
2955
|
}
|
|
2340
2956
|
const active = readActive(paths);
|
|
2341
|
-
if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (
|
|
2957
|
+
if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs6.statSync(path6.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
|
|
2342
2958
|
return active;
|
|
2343
2959
|
if (identity === null) publishIdentity(paths, files.identity);
|
|
2344
2960
|
return prepareGeneration(paths, files);
|
|
@@ -2351,9 +2967,9 @@ var commitGeneration = (paths, files) => {
|
|
|
2351
2967
|
var createStore = (configDir, lock) => {
|
|
2352
2968
|
const paths = {
|
|
2353
2969
|
configDir,
|
|
2354
|
-
identity:
|
|
2355
|
-
generations:
|
|
2356
|
-
active:
|
|
2970
|
+
identity: path6.join(configDir, "identity.env"),
|
|
2971
|
+
generations: path6.join(configDir, "generations"),
|
|
2972
|
+
active: path6.join(configDir, "active")
|
|
2357
2973
|
};
|
|
2358
2974
|
const whileOwned = (operation) => attempt(() => {
|
|
2359
2975
|
lock.assertOwned();
|
|
@@ -2366,13 +2982,13 @@ var createStore = (configDir, lock) => {
|
|
|
2366
2982
|
prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
|
|
2367
2983
|
activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
|
|
2368
2984
|
createBackup: (previous, target) => whileOwned(() => {
|
|
2369
|
-
const backups =
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
const directory =
|
|
2373
|
-
|
|
2985
|
+
const backups = path6.join(configDir, "backups");
|
|
2986
|
+
fs6.mkdirSync(backups, { recursive: true, mode: 448 });
|
|
2987
|
+
fs6.chmodSync(backups, 448);
|
|
2988
|
+
const directory = path6.join(backups, `upgrade-${randomUUID()}`);
|
|
2989
|
+
fs6.mkdirSync(directory, { mode: 448 });
|
|
2374
2990
|
writeDurable(
|
|
2375
|
-
|
|
2991
|
+
path6.join(directory, "recovery.json"),
|
|
2376
2992
|
`${JSON.stringify(
|
|
2377
2993
|
{
|
|
2378
2994
|
previousGeneration: previous.directory,
|
|
@@ -2388,21 +3004,21 @@ var createStore = (configDir, lock) => {
|
|
|
2388
3004
|
syncDirectory(directory);
|
|
2389
3005
|
syncDirectory(backups);
|
|
2390
3006
|
syncDirectory(configDir);
|
|
2391
|
-
const partialFile =
|
|
3007
|
+
const partialFile = path6.join(directory, "database.sql.partial");
|
|
2392
3008
|
return {
|
|
2393
3009
|
directory,
|
|
2394
3010
|
partialFile,
|
|
2395
3011
|
complete: () => whileOwned(() => {
|
|
2396
|
-
if (
|
|
3012
|
+
if (fs6.statSync(partialFile).size === 0)
|
|
2397
3013
|
throw new ServerStoreError("Database backup is empty; refusing target startup.");
|
|
2398
|
-
const fd =
|
|
3014
|
+
const fd = fs6.openSync(partialFile, "r");
|
|
2399
3015
|
try {
|
|
2400
|
-
|
|
2401
|
-
|
|
3016
|
+
fs6.fchmodSync(fd, 384);
|
|
3017
|
+
fs6.fsyncSync(fd);
|
|
2402
3018
|
} finally {
|
|
2403
|
-
|
|
3019
|
+
fs6.closeSync(fd);
|
|
2404
3020
|
}
|
|
2405
|
-
|
|
3021
|
+
fs6.renameSync(partialFile, path6.join(directory, "database.sql"));
|
|
2406
3022
|
syncDirectory(directory);
|
|
2407
3023
|
})
|
|
2408
3024
|
};
|
|
@@ -2411,7 +3027,7 @@ var createStore = (configDir, lock) => {
|
|
|
2411
3027
|
};
|
|
2412
3028
|
var refuseFlatLayout = (configDir) => {
|
|
2413
3029
|
if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
|
|
2414
|
-
(name) =>
|
|
3030
|
+
(name) => fs6.existsSync(path6.join(configDir, name))
|
|
2415
3031
|
)) {
|
|
2416
3032
|
throw new ServerStoreError(
|
|
2417
3033
|
"Unreleased flat server configuration found. Preserve its credentials and volumes; migrate it to the generation layout before retrying. Setup will not replace this identity."
|
|
@@ -2419,7 +3035,7 @@ var refuseFlatLayout = (configDir) => {
|
|
|
2419
3035
|
}
|
|
2420
3036
|
};
|
|
2421
3037
|
var withServerStore = async (directory, operation, options = {}) => {
|
|
2422
|
-
const configDir =
|
|
3038
|
+
const configDir = path6.resolve(directory);
|
|
2423
3039
|
let lock;
|
|
2424
3040
|
let result;
|
|
2425
3041
|
try {
|
|
@@ -2436,9 +3052,9 @@ var withServerStore = async (directory, operation, options = {}) => {
|
|
|
2436
3052
|
};
|
|
2437
3053
|
|
|
2438
3054
|
// src/version.ts
|
|
2439
|
-
import * as
|
|
3055
|
+
import * as fs7 from "node:fs";
|
|
2440
3056
|
var cliVersion = () => {
|
|
2441
|
-
const raw =
|
|
3057
|
+
const raw = fs7.readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
2442
3058
|
const parsed = JSON.parse(raw);
|
|
2443
3059
|
if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
|
|
2444
3060
|
const version = parsed.version;
|
|
@@ -3117,7 +3733,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
|
|
|
3117
3733
|
return version;
|
|
3118
3734
|
};
|
|
3119
3735
|
var parseDockerSocketPath = (socket) => {
|
|
3120
|
-
if (!
|
|
3736
|
+
if (!path7.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
|
|
3121
3737
|
throw setupError(
|
|
3122
3738
|
"--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
|
|
3123
3739
|
);
|
|
@@ -3143,8 +3759,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
|
|
|
3143
3759
|
if (options.assetsDir !== void 0) {
|
|
3144
3760
|
try {
|
|
3145
3761
|
const [compose2, postgresInit2] = await Promise.all([
|
|
3146
|
-
readFile(
|
|
3147
|
-
readFile(
|
|
3762
|
+
readFile(path7.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
|
|
3763
|
+
readFile(path7.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
|
|
3148
3764
|
]);
|
|
3149
3765
|
validateComposeAsset(compose2);
|
|
3150
3766
|
validatePostgresAsset(postgresInit2);
|
|
@@ -3657,10 +4273,10 @@ var manageServer = async (command, args, runtime, store) => {
|
|
|
3657
4273
|
};
|
|
3658
4274
|
var nodeServerRuntime = () => {
|
|
3659
4275
|
const environment = { ...process.env };
|
|
3660
|
-
const home =
|
|
3661
|
-
const configHome = environment["XDG_CONFIG_HOME"] ??
|
|
4276
|
+
const home = os4.homedir();
|
|
4277
|
+
const configHome = environment["XDG_CONFIG_HOME"] ?? path7.join(home, ".config");
|
|
3662
4278
|
return {
|
|
3663
|
-
configDir:
|
|
4279
|
+
configDir: path7.join(configHome, "mend"),
|
|
3664
4280
|
platform: process.platform,
|
|
3665
4281
|
cliVersion: cliVersion(),
|
|
3666
4282
|
run: (command, args, options) => runServerProcess(command, args, environment, options),
|
|
@@ -3886,8 +4502,8 @@ var renderMendToml = (proposals) => {
|
|
|
3886
4502
|
};
|
|
3887
4503
|
|
|
3888
4504
|
// src/skills.ts
|
|
3889
|
-
import * as
|
|
3890
|
-
import * as
|
|
4505
|
+
import * as fs8 from "node:fs";
|
|
4506
|
+
import * as path8 from "node:path";
|
|
3891
4507
|
var DEFAULT_SKILLS_DIR = ".agents/skills";
|
|
3892
4508
|
var MAX_SKILL_FILES = 64;
|
|
3893
4509
|
var MAX_SKILL_FILE_BYTES = 512 * 1024;
|
|
@@ -3910,12 +4526,12 @@ var readFrontmatterDescription = (contents) => {
|
|
|
3910
4526
|
var walkFiles = (root) => {
|
|
3911
4527
|
const found = [];
|
|
3912
4528
|
const visit = (dir) => {
|
|
3913
|
-
for (const entry of
|
|
4529
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
3914
4530
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3915
|
-
const absolute =
|
|
4531
|
+
const absolute = path8.join(dir, entry.name);
|
|
3916
4532
|
let stat;
|
|
3917
4533
|
try {
|
|
3918
|
-
stat =
|
|
4534
|
+
stat = fs8.statSync(absolute);
|
|
3919
4535
|
} catch {
|
|
3920
4536
|
continue;
|
|
3921
4537
|
}
|
|
@@ -3929,7 +4545,7 @@ var walkFiles = (root) => {
|
|
|
3929
4545
|
var scanSkillLibrary = (root) => {
|
|
3930
4546
|
let entries;
|
|
3931
4547
|
try {
|
|
3932
|
-
entries =
|
|
4548
|
+
entries = fs8.readdirSync(root, { withFileTypes: true });
|
|
3933
4549
|
} catch {
|
|
3934
4550
|
return { error: `${root} is not a readable directory` };
|
|
3935
4551
|
}
|
|
@@ -3937,23 +4553,23 @@ var scanSkillLibrary = (root) => {
|
|
|
3937
4553
|
const notes = [];
|
|
3938
4554
|
for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
|
|
3939
4555
|
if (entry.name.startsWith(".")) continue;
|
|
3940
|
-
const skillDir =
|
|
4556
|
+
const skillDir = path8.join(root, entry.name);
|
|
3941
4557
|
let stat;
|
|
3942
4558
|
try {
|
|
3943
|
-
stat =
|
|
4559
|
+
stat = fs8.statSync(skillDir);
|
|
3944
4560
|
} catch {
|
|
3945
4561
|
continue;
|
|
3946
4562
|
}
|
|
3947
4563
|
if (!stat.isDirectory()) continue;
|
|
3948
|
-
if (!
|
|
4564
|
+
if (!fs8.existsSync(path8.join(skillDir, "SKILL.md"))) {
|
|
3949
4565
|
notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
|
|
3950
4566
|
continue;
|
|
3951
4567
|
}
|
|
3952
4568
|
const files = [];
|
|
3953
4569
|
let bytes = 0;
|
|
3954
4570
|
for (const absolute of walkFiles(skillDir)) {
|
|
3955
|
-
const relative3 =
|
|
3956
|
-
const buffer =
|
|
4571
|
+
const relative3 = path8.relative(skillDir, absolute).split(path8.sep).join("/");
|
|
4572
|
+
const buffer = fs8.readFileSync(absolute);
|
|
3957
4573
|
if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
|
|
3958
4574
|
notes.push({ skill: entry.name, message: `${relative3} is over 512KB \u2014 skipped` });
|
|
3959
4575
|
continue;
|
|
@@ -3988,16 +4604,16 @@ var scanSkillLibrary = (root) => {
|
|
|
3988
4604
|
};
|
|
3989
4605
|
|
|
3990
4606
|
// src/ssh-setup.ts
|
|
3991
|
-
import * as
|
|
3992
|
-
import * as
|
|
4607
|
+
import * as os6 from "node:os";
|
|
4608
|
+
import * as path10 from "node:path";
|
|
3993
4609
|
|
|
3994
4610
|
// ../../packages/workspace-ssh/src/index.ts
|
|
3995
|
-
import { spawnSync } from "node:child_process";
|
|
3996
|
-
import { createHash as
|
|
3997
|
-
import * as
|
|
4611
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
4612
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4613
|
+
import * as fs9 from "node:fs";
|
|
3998
4614
|
import { isIP as isIP2 } from "node:net";
|
|
3999
|
-
import * as
|
|
4000
|
-
import * as
|
|
4615
|
+
import * as os5 from "node:os";
|
|
4616
|
+
import * as path9 from "node:path";
|
|
4001
4617
|
var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
|
|
4002
4618
|
var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
|
|
4003
4619
|
var WorkspaceSshError = class extends Error {
|
|
@@ -4015,7 +4631,7 @@ var WorkspaceSshError = class extends Error {
|
|
|
4015
4631
|
var success = (value) => ({ ok: true, value });
|
|
4016
4632
|
var failure = (error) => ({ ok: false, error });
|
|
4017
4633
|
var stripIpv6Brackets = (hostname6) => hostname6.startsWith("[") && hostname6.endsWith("]") ? hostname6.slice(1, -1) : hostname6;
|
|
4018
|
-
var hashIdentity = (identity) =>
|
|
4634
|
+
var hashIdentity = (identity) => createHash3("sha256").update(identity).digest("hex").slice(0, 24);
|
|
4019
4635
|
var aliasLabel = (hostname6) => {
|
|
4020
4636
|
const label = hostname6.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
4021
4637
|
return label === "" ? "server" : label;
|
|
@@ -4084,8 +4700,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
|
|
|
4084
4700
|
var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
|
|
4085
4701
|
var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
|
|
4086
4702
|
var parseIdentityPath = (value) => {
|
|
4087
|
-
const resolved =
|
|
4088
|
-
value.startsWith("~/") ?
|
|
4703
|
+
const resolved = path9.resolve(
|
|
4704
|
+
value.startsWith("~/") ? path9.join(os5.homedir(), value.slice(2)) : value
|
|
4089
4705
|
);
|
|
4090
4706
|
if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
|
|
4091
4707
|
return failure(
|
|
@@ -4220,7 +4836,7 @@ var workspaceSshPublicKeyFingerprint = (publicKey) => {
|
|
|
4220
4836
|
return failure(new WorkspaceSshError("key", "SSH public key has no encoded key data."));
|
|
4221
4837
|
}
|
|
4222
4838
|
try {
|
|
4223
|
-
const digest =
|
|
4839
|
+
const digest = createHash3("sha256").update(Buffer.from(encoded, "base64")).digest("base64");
|
|
4224
4840
|
return success(`SHA256:${digest.replace(/=+$/, "")}`);
|
|
4225
4841
|
} catch (cause) {
|
|
4226
4842
|
return failure(
|
|
@@ -4231,7 +4847,7 @@ var workspaceSshPublicKeyFingerprint = (publicKey) => {
|
|
|
4231
4847
|
var publicKeyIdentity = (publicKey) => publicKey.trim().split(/\s+/).slice(0, 2).join(" ");
|
|
4232
4848
|
var agentKeys = () => {
|
|
4233
4849
|
if (!process.env["SSH_AUTH_SOCK"]) return [];
|
|
4234
|
-
const listed =
|
|
4850
|
+
const listed = spawnSync2("ssh-add", ["-L"], {
|
|
4235
4851
|
encoding: "utf8",
|
|
4236
4852
|
timeout: 3e3,
|
|
4237
4853
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4241,7 +4857,7 @@ var agentKeys = () => {
|
|
|
4241
4857
|
};
|
|
4242
4858
|
var readText = (file) => {
|
|
4243
4859
|
try {
|
|
4244
|
-
return success(
|
|
4860
|
+
return success(fs9.readFileSync(file, "utf8"));
|
|
4245
4861
|
} catch (cause) {
|
|
4246
4862
|
const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
|
|
4247
4863
|
return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
|
|
@@ -4268,7 +4884,7 @@ var readRequiredKey = (keyPath, source) => {
|
|
|
4268
4884
|
const privatePath = publicPath.slice(0, -".pub".length);
|
|
4269
4885
|
const read = readText(publicPath);
|
|
4270
4886
|
if (!read.ok) return read;
|
|
4271
|
-
const derived =
|
|
4887
|
+
const derived = spawnSync2("ssh-keygen", ["-y", "-P", "", "-f", privatePath], {
|
|
4272
4888
|
encoding: "utf8",
|
|
4273
4889
|
timeout: 3e3,
|
|
4274
4890
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4299,19 +4915,19 @@ var readRequiredKey = (keyPath, source) => {
|
|
|
4299
4915
|
var readOptionalKey = (keyPath) => {
|
|
4300
4916
|
const parsed = parseIdentityPath(keyPath);
|
|
4301
4917
|
if (!parsed.ok) return parsed;
|
|
4302
|
-
if (!
|
|
4918
|
+
if (!fs9.existsSync(parsed.value) && !fs9.existsSync(publicKeyPath(parsed.value)))
|
|
4303
4919
|
return success(null);
|
|
4304
4920
|
return readRequiredKey(parsed.value, "existing");
|
|
4305
4921
|
};
|
|
4306
4922
|
var pinAgentKey = (configHome, publicKey) => {
|
|
4307
|
-
const identityFile =
|
|
4923
|
+
const identityFile = path9.resolve(
|
|
4308
4924
|
configHome,
|
|
4309
4925
|
"ssh",
|
|
4310
4926
|
`agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
|
|
4311
4927
|
);
|
|
4312
4928
|
try {
|
|
4313
|
-
|
|
4314
|
-
|
|
4929
|
+
fs9.mkdirSync(path9.dirname(identityFile), { recursive: true, mode: 448 });
|
|
4930
|
+
fs9.writeFileSync(identityFile, `${publicKey}
|
|
4315
4931
|
`, { mode: 384 });
|
|
4316
4932
|
return success({ publicKey, identityFile, source: "agent" });
|
|
4317
4933
|
} catch (cause) {
|
|
@@ -4322,15 +4938,15 @@ var pinAgentKey = (configHome, publicKey) => {
|
|
|
4322
4938
|
};
|
|
4323
4939
|
var generateDedicatedKey = (privatePath) => {
|
|
4324
4940
|
try {
|
|
4325
|
-
|
|
4941
|
+
fs9.mkdirSync(path9.dirname(privatePath), { recursive: true, mode: 448 });
|
|
4326
4942
|
} catch (cause) {
|
|
4327
4943
|
return failure(
|
|
4328
|
-
new WorkspaceSshError("key", `Could not create ${
|
|
4944
|
+
new WorkspaceSshError("key", `Could not create ${path9.dirname(privatePath)}.`, cause)
|
|
4329
4945
|
);
|
|
4330
4946
|
}
|
|
4331
|
-
const generated =
|
|
4947
|
+
const generated = spawnSync2(
|
|
4332
4948
|
"ssh-keygen",
|
|
4333
|
-
["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${
|
|
4949
|
+
["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os5.hostname()}`],
|
|
4334
4950
|
{ encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
|
|
4335
4951
|
);
|
|
4336
4952
|
if (generated.status !== 0) {
|
|
@@ -4354,7 +4970,7 @@ var generateDedicatedKey = (privatePath) => {
|
|
|
4354
4970
|
return fresh;
|
|
4355
4971
|
};
|
|
4356
4972
|
var dedicatedKey = (configHome, create) => {
|
|
4357
|
-
const privatePath =
|
|
4973
|
+
const privatePath = path9.join(configHome, "ssh", "id_ed25519");
|
|
4358
4974
|
const existing = readOptionalKey(privatePath);
|
|
4359
4975
|
if (existing.ok === false) return failure(existing.error);
|
|
4360
4976
|
if (existing.value !== null || !create) return existing;
|
|
@@ -4402,10 +5018,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
|
|
|
4402
5018
|
const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
|
|
4403
5019
|
if (reconciled.ok === false) return failure(reconciled.error);
|
|
4404
5020
|
try {
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
5021
|
+
fs9.mkdirSync(path9.dirname(configFile), { recursive: true, mode: 448 });
|
|
5022
|
+
fs9.chmodSync(path9.dirname(configFile), 448);
|
|
5023
|
+
fs9.writeFileSync(configFile, reconciled.value, { mode: 384 });
|
|
5024
|
+
fs9.chmodSync(configFile, 384);
|
|
4409
5025
|
return success(void 0);
|
|
4410
5026
|
} catch (cause) {
|
|
4411
5027
|
return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
|
|
@@ -4414,23 +5030,23 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
|
|
|
4414
5030
|
|
|
4415
5031
|
// src/ssh-setup.ts
|
|
4416
5032
|
var ansi = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
4417
|
-
var
|
|
4418
|
-
var
|
|
5033
|
+
var dim4 = ansi("2");
|
|
5034
|
+
var green4 = ansi("32");
|
|
4419
5035
|
var warn = ansi("33");
|
|
4420
|
-
var
|
|
4421
|
-
var sshConfigPath = () =>
|
|
5036
|
+
var say4 = (line) => console.log(line);
|
|
5037
|
+
var sshConfigPath = () => path10.join(os6.homedir(), ".ssh", "config");
|
|
4422
5038
|
var flagValue = (args, flag) => {
|
|
4423
5039
|
const index = args.indexOf(flag);
|
|
4424
5040
|
return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
|
|
4425
5041
|
};
|
|
4426
5042
|
var showFailure = (message) => {
|
|
4427
|
-
|
|
5043
|
+
say4(`mend: ${message}`);
|
|
4428
5044
|
process.exitCode = 1;
|
|
4429
5045
|
};
|
|
4430
5046
|
var showStatus = async (api2, cliHome, serverUrl, args) => {
|
|
4431
5047
|
const view = await api2("GET", "/workspace-ssh");
|
|
4432
5048
|
if (view.gateway === null) {
|
|
4433
|
-
|
|
5049
|
+
say4(`workspace ssh ${warn("no gateway")} ${dim4("\xB7 this deployment exposes none")}`);
|
|
4434
5050
|
return;
|
|
4435
5051
|
}
|
|
4436
5052
|
const parsedTarget = parseWorkspaceSshTarget({
|
|
@@ -4453,16 +5069,16 @@ var showStatus = async (api2, cliHome, serverUrl, args) => {
|
|
|
4453
5069
|
key: picked.value,
|
|
4454
5070
|
registeredFingerprints: view.keys.map((key) => key.fingerprint)
|
|
4455
5071
|
});
|
|
4456
|
-
|
|
4457
|
-
`gateway ${parsedTarget.value.hostname}:${parsedTarget.value.port} ${
|
|
5072
|
+
say4(
|
|
5073
|
+
`gateway ${parsedTarget.value.hostname}:${parsedTarget.value.port} ${dim4(`\xB7 published for ${serverUrl}`)}`
|
|
4458
5074
|
);
|
|
4459
|
-
|
|
4460
|
-
picked.value === null ? `client key ${warn("none available")} ${
|
|
5075
|
+
say4(
|
|
5076
|
+
picked.value === null ? `client key ${warn("none available")} ${dim4("\xB7 run: mend ssh setup")}` : readiness.keyRegistered ? `client key ${green4("\u25CF")} ${picked.value.fingerprint} ${dim4("\xB7 registered")}` : `client key ${warn("not registered")} ${dim4(`\xB7 ${picked.value.fingerprint}`)}`
|
|
4461
5077
|
);
|
|
4462
|
-
|
|
4463
|
-
readiness.configReady ? `ssh config ${
|
|
5078
|
+
say4(
|
|
5079
|
+
readiness.configReady ? `ssh config ${green4("\u25CF")} Host ${parsedTarget.value.alias} ${dim4(`\xB7 ${sshConfigPath()}`)}` : `ssh config ${warn("missing or stale")} ${dim4("\xB7 run: mend ssh setup")}`
|
|
4464
5080
|
);
|
|
4465
|
-
|
|
5081
|
+
say4(
|
|
4466
5082
|
"host trust not checked \xB7 status checks config and client-key registration, not an SSH connection"
|
|
4467
5083
|
);
|
|
4468
5084
|
};
|
|
@@ -4488,7 +5104,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
|
|
|
4488
5104
|
if (picked.value === null) return showFailure("No workspace SSH key is available.");
|
|
4489
5105
|
const registered = await api2("POST", "/workspace-ssh/keys", {
|
|
4490
5106
|
publicKey: picked.value.publicKey,
|
|
4491
|
-
name:
|
|
5107
|
+
name: os6.hostname()
|
|
4492
5108
|
});
|
|
4493
5109
|
const sourceLabel = {
|
|
4494
5110
|
explicit: "from --key",
|
|
@@ -4496,22 +5112,22 @@ var setup = async (api2, cliHome, serverUrl, args) => {
|
|
|
4496
5112
|
existing: "existing selected key",
|
|
4497
5113
|
generated: "generated dedicated key"
|
|
4498
5114
|
}[picked.value.source];
|
|
4499
|
-
|
|
5115
|
+
say4(`key ${green4("\u25CF")} ${registered.fingerprint} ${dim4(`\xB7 ${sourceLabel}`)}`);
|
|
4500
5116
|
const written = writeWorkspaceSshConfig(
|
|
4501
5117
|
sshConfigPath(),
|
|
4502
5118
|
parsedTarget.value,
|
|
4503
5119
|
picked.value.identityFile
|
|
4504
5120
|
);
|
|
4505
5121
|
if (written.ok === false) return showFailure(written.error.message);
|
|
4506
|
-
|
|
4507
|
-
`ssh config ${
|
|
5122
|
+
say4(
|
|
5123
|
+
`ssh config ${green4("\u25CF")} Host ${parsedTarget.value.alias} ${dim4(`\xB7 ${sshConfigPath()}`)}`
|
|
4508
5124
|
);
|
|
4509
|
-
|
|
5125
|
+
say4(
|
|
4510
5126
|
"host trust not checked \xB7 SSH verifies the gateway when you connect; setup does not replace known_hosts entries"
|
|
4511
5127
|
);
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
`connect with ssh ${view.gateway.usernamePrefix}-<workspace-id>@${parsedTarget.value.alias} ${
|
|
5128
|
+
say4("");
|
|
5129
|
+
say4(
|
|
5130
|
+
`connect with ssh ${view.gateway.usernamePrefix}-<workspace-id>@${parsedTarget.value.alias} ${dim4("\xB7 the VS Code extension uses this automatically")}`
|
|
4515
5131
|
);
|
|
4516
5132
|
};
|
|
4517
5133
|
var sshCommand = async (args, api2, cliHome, serverUrl) => {
|
|
@@ -4530,8 +5146,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
|
|
|
4530
5146
|
};
|
|
4531
5147
|
|
|
4532
5148
|
// src/uninstall.ts
|
|
4533
|
-
import * as
|
|
4534
|
-
import * as
|
|
5149
|
+
import * as fs10 from "node:fs";
|
|
5150
|
+
import * as path11 from "node:path";
|
|
4535
5151
|
var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
|
|
4536
5152
|
var parseUninstallArgs = (args) => {
|
|
4537
5153
|
let scope = null;
|
|
@@ -4553,14 +5169,14 @@ var parseUninstallArgs = (args) => {
|
|
|
4553
5169
|
var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
|
|
4554
5170
|
var countEntries = (directory) => {
|
|
4555
5171
|
try {
|
|
4556
|
-
return
|
|
5172
|
+
return fs10.readdirSync(directory).length;
|
|
4557
5173
|
} catch {
|
|
4558
5174
|
return 0;
|
|
4559
5175
|
}
|
|
4560
5176
|
};
|
|
4561
5177
|
var exists = (file) => {
|
|
4562
5178
|
try {
|
|
4563
|
-
|
|
5179
|
+
fs10.lstatSync(file);
|
|
4564
5180
|
return true;
|
|
4565
5181
|
} catch {
|
|
4566
5182
|
return false;
|
|
@@ -4569,7 +5185,7 @@ var exists = (file) => {
|
|
|
4569
5185
|
var managedBlockCount = (sshConfigFile) => {
|
|
4570
5186
|
let config;
|
|
4571
5187
|
try {
|
|
4572
|
-
config =
|
|
5188
|
+
config = fs10.readFileSync(sshConfigFile, "utf8");
|
|
4573
5189
|
} catch {
|
|
4574
5190
|
return 0;
|
|
4575
5191
|
}
|
|
@@ -4593,27 +5209,27 @@ var describeUninstall = async (runtime, scope) => {
|
|
|
4593
5209
|
if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
|
|
4594
5210
|
server = "none";
|
|
4595
5211
|
} else if (read.value === null) {
|
|
4596
|
-
server = exists(
|
|
5212
|
+
server = exists(path11.join(configDir, "identity.env")) ? {
|
|
4597
5213
|
version: "(no active generation)",
|
|
4598
5214
|
appUrl: "",
|
|
4599
5215
|
dockerContext: "",
|
|
4600
|
-
generations: countEntries(
|
|
4601
|
-
backups: countEntries(
|
|
5216
|
+
generations: countEntries(path11.join(configDir, "generations")),
|
|
5217
|
+
backups: countEntries(path11.join(configDir, "backups"))
|
|
4602
5218
|
} : "none";
|
|
4603
5219
|
} else {
|
|
4604
5220
|
server = {
|
|
4605
5221
|
version: read.value.config.serverVersion,
|
|
4606
5222
|
appUrl: read.value.config.appUrl,
|
|
4607
5223
|
dockerContext: read.value.config.dockerContext,
|
|
4608
|
-
generations: countEntries(
|
|
4609
|
-
backups: countEntries(
|
|
5224
|
+
generations: countEntries(path11.join(configDir, "generations")),
|
|
5225
|
+
backups: countEntries(path11.join(configDir, "backups"))
|
|
4610
5226
|
};
|
|
4611
5227
|
}
|
|
4612
5228
|
}
|
|
4613
5229
|
let home = null;
|
|
4614
5230
|
if (scope !== "server") {
|
|
4615
|
-
const cliConfig =
|
|
4616
|
-
const sshDirectory =
|
|
5231
|
+
const cliConfig = path11.join(runtime.cliHome, "cli.json");
|
|
5232
|
+
const sshDirectory = path11.join(runtime.cliHome, "ssh");
|
|
4617
5233
|
home = {
|
|
4618
5234
|
cliConfig: exists(cliConfig) ? cliConfig : null,
|
|
4619
5235
|
sshDirectory: exists(sshDirectory) ? sshDirectory : null,
|
|
@@ -4745,7 +5361,7 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
4745
5361
|
}
|
|
4746
5362
|
}
|
|
4747
5363
|
for (const name of SERVER_FILES) {
|
|
4748
|
-
|
|
5364
|
+
fs10.rmSync(path11.join(configDir, name), { recursive: true, force: true });
|
|
4749
5365
|
}
|
|
4750
5366
|
server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
|
|
4751
5367
|
},
|
|
@@ -4756,20 +5372,20 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
4756
5372
|
var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
4757
5373
|
const { server } = runtime;
|
|
4758
5374
|
if (plan.cliConfig !== null) {
|
|
4759
|
-
|
|
5375
|
+
fs10.rmSync(plan.cliConfig, { force: true });
|
|
4760
5376
|
server.writeLine(`removed ${plan.cliConfig}`);
|
|
4761
5377
|
}
|
|
4762
5378
|
if (plan.sshDirectory !== null) {
|
|
4763
|
-
|
|
5379
|
+
fs10.rmSync(plan.sshDirectory, { recursive: true, force: true });
|
|
4764
5380
|
server.writeLine(`removed ${plan.sshDirectory}`);
|
|
4765
5381
|
}
|
|
4766
5382
|
if (plan.managedSshBlocks > 0) {
|
|
4767
5383
|
try {
|
|
4768
|
-
const before =
|
|
5384
|
+
const before = fs10.readFileSync(runtime.sshConfigFile, "utf8");
|
|
4769
5385
|
const stripped = stripManagedWorkspaceSshBlocks(before);
|
|
4770
5386
|
if (!stripped.ok) throw stripped.error;
|
|
4771
5387
|
if (stripped.value.removed > 0) {
|
|
4772
|
-
|
|
5388
|
+
fs10.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
|
|
4773
5389
|
server.writeLine(
|
|
4774
5390
|
`removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
|
|
4775
5391
|
);
|
|
@@ -4782,12 +5398,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
|
4782
5398
|
}
|
|
4783
5399
|
let remaining = [];
|
|
4784
5400
|
try {
|
|
4785
|
-
remaining =
|
|
5401
|
+
remaining = fs10.readdirSync(runtime.cliHome);
|
|
4786
5402
|
} catch {
|
|
4787
5403
|
return;
|
|
4788
5404
|
}
|
|
4789
5405
|
if (remaining.length === 0) {
|
|
4790
|
-
|
|
5406
|
+
fs10.rmdirSync(runtime.cliHome);
|
|
4791
5407
|
server.writeLine(`removed ${runtime.cliHome}`);
|
|
4792
5408
|
} else {
|
|
4793
5409
|
leftovers.push(
|
|
@@ -4815,24 +5431,57 @@ var executeUninstall = async (runtime, plan) => {
|
|
|
4815
5431
|
return { failures, leftovers };
|
|
4816
5432
|
};
|
|
4817
5433
|
|
|
5434
|
+
// src/upgrade-url.ts
|
|
5435
|
+
var PATHS = {
|
|
5436
|
+
tty: "/api/tty",
|
|
5437
|
+
"service-tunnel": "/api/service-tunnel",
|
|
5438
|
+
"keys-bridge": "/api/keys/bridge/ws"
|
|
5439
|
+
};
|
|
5440
|
+
var upgradeBaseUrl = (serverUrl, target, params, extra = {}) => {
|
|
5441
|
+
const url = new URL(`${serverUrl.replace(/\/$/, "")}${PATHS[target]}`);
|
|
5442
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
5443
|
+
for (const [key, value] of Object.entries({ ...params, ...extra })) {
|
|
5444
|
+
if (typeof value === "string" && value !== "") url.searchParams.set(key, value);
|
|
5445
|
+
}
|
|
5446
|
+
return url;
|
|
5447
|
+
};
|
|
5448
|
+
var mintRefusedInTransit = async (serverUrl, send = fetch) => {
|
|
5449
|
+
try {
|
|
5450
|
+
const response = await send(`${serverUrl.replace(/\/$/, "")}/api/health`);
|
|
5451
|
+
if (!response.ok) return false;
|
|
5452
|
+
const health = await response.json();
|
|
5453
|
+
return typeof health === "object" && health !== null && "upgradeTickets" in health && health.upgradeTickets === true;
|
|
5454
|
+
} catch {
|
|
5455
|
+
return false;
|
|
5456
|
+
}
|
|
5457
|
+
};
|
|
5458
|
+
var MINT_REFUSED_IN_TRANSIT = "POST /api/upgrade-tickets answered 404, but this server mints upgrade tickets: something between this client and Mend is refusing it. The saved token was not put in a URL.";
|
|
5459
|
+
var upgradeUrl = async (input) => {
|
|
5460
|
+
const url = upgradeBaseUrl(input.serverUrl, input.target, input.params, input.extra);
|
|
5461
|
+
const minted = await input.mint(input.target, input.params);
|
|
5462
|
+
if (minted.kind === "ticket") url.searchParams.set("ticket", minted.ticket);
|
|
5463
|
+
else if (input.legacyToken !== null) url.searchParams.set("token", input.legacyToken);
|
|
5464
|
+
return url;
|
|
5465
|
+
};
|
|
5466
|
+
|
|
4818
5467
|
// src/main.ts
|
|
4819
5468
|
var mendCliHome = () => {
|
|
4820
5469
|
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
4821
|
-
const preferred =
|
|
4822
|
-
xdg === void 0 || xdg === "" ?
|
|
5470
|
+
const preferred = path12.join(
|
|
5471
|
+
xdg === void 0 || xdg === "" ? path12.join(os7.homedir(), ".config") : xdg,
|
|
4823
5472
|
"mend"
|
|
4824
5473
|
);
|
|
4825
|
-
const legacy =
|
|
4826
|
-
return !
|
|
5474
|
+
const legacy = path12.join(os7.homedir(), ".mend");
|
|
5475
|
+
return !fs11.existsSync(preferred) && fs11.existsSync(legacy) ? legacy : preferred;
|
|
4827
5476
|
};
|
|
4828
|
-
var CONFIG_PATH =
|
|
5477
|
+
var CONFIG_PATH = path12.join(mendCliHome(), "cli.json");
|
|
4829
5478
|
var loadConfig = () => {
|
|
4830
5479
|
let fileConfig = {};
|
|
4831
|
-
if (
|
|
5480
|
+
if (fs11.existsSync(CONFIG_PATH)) {
|
|
4832
5481
|
try {
|
|
4833
|
-
fileConfig = JSON.parse(
|
|
5482
|
+
fileConfig = JSON.parse(fs11.readFileSync(CONFIG_PATH, "utf8"));
|
|
4834
5483
|
} catch {
|
|
4835
|
-
|
|
5484
|
+
fail5(`could not parse ${CONFIG_PATH}`);
|
|
4836
5485
|
}
|
|
4837
5486
|
}
|
|
4838
5487
|
const configuredUrl = process.env["MEND_URL"] ?? fileConfig.url ?? null;
|
|
@@ -4843,7 +5492,7 @@ var loadConfig = () => {
|
|
|
4843
5492
|
deviceId: fileConfig.deviceId ?? null
|
|
4844
5493
|
};
|
|
4845
5494
|
};
|
|
4846
|
-
var
|
|
5495
|
+
var fail5 = (message) => {
|
|
4847
5496
|
process.stderr.write(`mend: ${message}
|
|
4848
5497
|
`);
|
|
4849
5498
|
process.exit(1);
|
|
@@ -4852,18 +5501,18 @@ var parseMendUrl = (value) => {
|
|
|
4852
5501
|
try {
|
|
4853
5502
|
return new URL(value);
|
|
4854
5503
|
} catch {
|
|
4855
|
-
return
|
|
5504
|
+
return fail5(`"${value}" is not a valid Mend URL`);
|
|
4856
5505
|
}
|
|
4857
5506
|
};
|
|
4858
|
-
var
|
|
4859
|
-
var
|
|
4860
|
-
var
|
|
4861
|
-
var amber =
|
|
4862
|
-
var cobalt2 =
|
|
4863
|
-
var
|
|
5507
|
+
var paint4 = (code) => (text) => process.stdout.isTTY ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
5508
|
+
var dim5 = paint4("2");
|
|
5509
|
+
var green5 = paint4("32");
|
|
5510
|
+
var amber = paint4("33");
|
|
5511
|
+
var cobalt2 = paint4("34");
|
|
5512
|
+
var say5 = (line) => process.stdout.write(`${line}
|
|
4864
5513
|
`);
|
|
4865
5514
|
var detachKeyEnabled = process.env["MEND_DETACH_KEY"] !== "none";
|
|
4866
|
-
var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${
|
|
5515
|
+
var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${dim5("Ctrl+]")}` : "";
|
|
4867
5516
|
var herdrAgentOf = (harness) => {
|
|
4868
5517
|
if (harness === "codex" || harness === "claude" || harness === "opencode") return harness;
|
|
4869
5518
|
return null;
|
|
@@ -4931,9 +5580,42 @@ var api = async (config, method, route, body) => {
|
|
|
4931
5580
|
try {
|
|
4932
5581
|
return await request(config, method, route, body);
|
|
4933
5582
|
} catch (error) {
|
|
4934
|
-
return
|
|
5583
|
+
return fail5(error instanceof Error ? error.message : String(error));
|
|
4935
5584
|
}
|
|
4936
5585
|
};
|
|
5586
|
+
var warnedLegacyBearer = false;
|
|
5587
|
+
var mintTicket = (config) => async (target, params) => {
|
|
5588
|
+
const headers = { "content-type": "application/json" };
|
|
5589
|
+
if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
|
|
5590
|
+
const response = await fetch(`${config.url}/api/upgrade-tickets`, {
|
|
5591
|
+
method: "POST",
|
|
5592
|
+
headers,
|
|
5593
|
+
body: JSON.stringify({ target, ...params })
|
|
5594
|
+
});
|
|
5595
|
+
if (response.status === 404) {
|
|
5596
|
+
if (await mintRefusedInTransit(config.url)) throw new Error(MINT_REFUSED_IN_TRANSIT);
|
|
5597
|
+
if (config.token !== null && !warnedLegacyBearer) {
|
|
5598
|
+
warnedLegacyBearer = true;
|
|
5599
|
+
console.error(
|
|
5600
|
+
"mend: this server predates upgrade tickets, so the saved token rides the socket's URL. Upgrade the server to stop that."
|
|
5601
|
+
);
|
|
5602
|
+
}
|
|
5603
|
+
return { kind: "unsupported" };
|
|
5604
|
+
}
|
|
5605
|
+
if (!response.ok) throw new Error(`upgrade ticket refused (${response.status})`);
|
|
5606
|
+
const minted = await response.json();
|
|
5607
|
+
if (typeof minted.ticket !== "string")
|
|
5608
|
+
throw new Error("upgrade ticket missing from the answer");
|
|
5609
|
+
return { kind: "ticket", ticket: minted.ticket };
|
|
5610
|
+
};
|
|
5611
|
+
var socketUrl = (config, target, params, extra) => upgradeUrl({
|
|
5612
|
+
serverUrl: parseMendUrl(config.url).toString(),
|
|
5613
|
+
target,
|
|
5614
|
+
params,
|
|
5615
|
+
...extra === void 0 ? {} : { extra },
|
|
5616
|
+
mint: mintTicket(config),
|
|
5617
|
+
legacyToken: config.token
|
|
5618
|
+
});
|
|
4937
5619
|
var boundApi = (config) => (method, route, body) => api(config, method, route, body);
|
|
4938
5620
|
var withSpinner = async (label, work) => {
|
|
4939
5621
|
if (process.stdout.isTTY !== true) return work;
|
|
@@ -4942,7 +5624,7 @@ var withSpinner = async (label, work) => {
|
|
|
4942
5624
|
let frame = 0;
|
|
4943
5625
|
const timer = setInterval(() => {
|
|
4944
5626
|
const seconds = Math.round((Date.now() - started) / 1e3);
|
|
4945
|
-
process.stdout.write(`\r ${frames[frame % frames.length]} ${label} ${
|
|
5627
|
+
process.stdout.write(`\r ${frames[frame % frames.length]} ${label} ${dim5(`${seconds}s`)} `);
|
|
4946
5628
|
frame += 1;
|
|
4947
5629
|
}, 120);
|
|
4948
5630
|
try {
|
|
@@ -4956,66 +5638,74 @@ var adopt = async (config, args) => {
|
|
|
4956
5638
|
const nameFlag = args.indexOf("--name");
|
|
4957
5639
|
const name = nameFlag !== -1 && args[nameFlag + 1] !== void 0 ? String(args[nameFlag + 1]) : null;
|
|
4958
5640
|
const authFlagIndex = args.indexOf("--auth");
|
|
5641
|
+
if (args.includes("--private") && args.includes("--shared")) {
|
|
5642
|
+
return fail5("--private and --shared pick one visibility; pass one of them");
|
|
5643
|
+
}
|
|
5644
|
+
const visibility = args.includes("--shared") ? "shared" : "private";
|
|
4959
5645
|
const positional = args.filter(
|
|
4960
5646
|
(a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1) && (authFlagIndex === -1 || i !== authFlagIndex + 1)
|
|
4961
5647
|
);
|
|
4962
5648
|
const source = positional[0] ?? gitOriginUrl(process.cwd());
|
|
4963
5649
|
if (source === null) {
|
|
4964
|
-
return
|
|
5650
|
+
return fail5(
|
|
4965
5651
|
"No Git URL was given and this repository has no origin. Run mend adopt <git-url>."
|
|
4966
5652
|
);
|
|
4967
5653
|
}
|
|
4968
5654
|
const sourceIssue = repositoryCloneUrlIssue(source);
|
|
4969
|
-
if (sourceIssue !== null) return
|
|
4970
|
-
const projectName = name ?? normalizeProjectName(
|
|
5655
|
+
if (sourceIssue !== null) return fail5(sourceIssue);
|
|
5656
|
+
const projectName = name ?? normalizeProjectName(path12.basename(source, ".git"));
|
|
4971
5657
|
const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
|
|
4972
5658
|
if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
|
|
4973
|
-
return
|
|
5659
|
+
return fail5(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
|
|
4974
5660
|
}
|
|
4975
5661
|
const project2 = await api(config, "POST", "/projects", {
|
|
4976
5662
|
name: projectName,
|
|
4977
5663
|
source,
|
|
5664
|
+
visibility,
|
|
4978
5665
|
...auth === null ? {} : { gitAuthMode: auth }
|
|
4979
5666
|
});
|
|
4980
|
-
|
|
4981
|
-
|
|
5667
|
+
say5(`${green5("\u2713")} adopted \xB7 ${project2.name} \xB7 ${dim5(project2.storePath)}`);
|
|
5668
|
+
say5(`${dim5(" default branch")} ${project2.defaultBranch}`);
|
|
5669
|
+
say5(
|
|
5670
|
+
`${dim5(" visible to")} ${visibility === "private" ? "only you" : "everyone in the organization"}`
|
|
5671
|
+
);
|
|
4982
5672
|
if (project2.gitAuthMode === "mend-key") {
|
|
4983
|
-
|
|
5673
|
+
say5(`${dim5(" git auth")} mend key ${dim5("(your Mend key signed this clone)")}`);
|
|
4984
5674
|
} else if (project2.gitAuthMode === "bridge") {
|
|
4985
|
-
|
|
5675
|
+
say5(`${dim5(" git auth")} bridge ${dim5("(signed through the connected `mend keys share`)")}`);
|
|
4986
5676
|
}
|
|
4987
|
-
|
|
4988
|
-
`${
|
|
5677
|
+
say5(
|
|
5678
|
+
`${dim5(" sessions start with:")} mend codex ${dim5("(from anywhere \u2014")} --project ${project2.name}${dim5(")")}`
|
|
4989
5679
|
);
|
|
4990
5680
|
};
|
|
4991
5681
|
var findProject = async (config, explicit, adoptCwd = false) => {
|
|
4992
5682
|
const projects = await api(config, "GET", "/projects");
|
|
4993
5683
|
if (explicit !== null) {
|
|
4994
5684
|
const named = projects.find((p) => p.name === explicit);
|
|
4995
|
-
if (named === void 0) return
|
|
5685
|
+
if (named === void 0) return fail5(`no adopted project named "${explicit}"`);
|
|
4996
5686
|
return named;
|
|
4997
5687
|
}
|
|
4998
5688
|
const cwd = process.cwd();
|
|
4999
5689
|
const project2 = matchProjectByCwd(projects, cwdFacts(cwd));
|
|
5000
5690
|
if (project2 !== void 0) return project2;
|
|
5001
5691
|
if (!adoptCwd) {
|
|
5002
|
-
return
|
|
5692
|
+
return fail5(
|
|
5003
5693
|
`no adopted project matches ${cwd} \u2014 run "mend adopt" here first, or name one with --project`
|
|
5004
5694
|
);
|
|
5005
5695
|
}
|
|
5006
5696
|
const facts = cwdFacts(cwd);
|
|
5007
5697
|
if (facts.repoRoot === null || facts.originUrl === null) {
|
|
5008
|
-
return
|
|
5698
|
+
return fail5(
|
|
5009
5699
|
`${cwd} does not match an adopted project with a Git origin \u2014 run "mend adopt <git-url>" first`
|
|
5010
5700
|
);
|
|
5011
5701
|
}
|
|
5012
5702
|
const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
|
|
5013
|
-
if (sourceIssue !== null) return
|
|
5703
|
+
if (sourceIssue !== null) return fail5(sourceIssue);
|
|
5014
5704
|
const adopted = await api(config, "POST", "/projects", {
|
|
5015
|
-
name: normalizeProjectName(
|
|
5705
|
+
name: normalizeProjectName(path12.basename(facts.repoRoot)),
|
|
5016
5706
|
source: facts.originUrl
|
|
5017
5707
|
});
|
|
5018
|
-
|
|
5708
|
+
say5(`${green5("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim5(adopted.storePath)}`);
|
|
5019
5709
|
return adopted;
|
|
5020
5710
|
};
|
|
5021
5711
|
var resolvedBackgroundSessions = async (config, project2) => {
|
|
@@ -5037,7 +5727,7 @@ var askWorktreeName = async () => {
|
|
|
5037
5727
|
const readline = await import("node:readline/promises");
|
|
5038
5728
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
5039
5729
|
try {
|
|
5040
|
-
const answer = (await rl.question(` worktree name ${
|
|
5730
|
+
const answer = (await rl.question(` worktree name ${dim5("(enter for auto)")}: `)).trim();
|
|
5041
5731
|
return answer === "" ? null : normalizeProjectName(answer);
|
|
5042
5732
|
} finally {
|
|
5043
5733
|
rl.close();
|
|
@@ -5045,35 +5735,35 @@ var askWorktreeName = async () => {
|
|
|
5045
5735
|
};
|
|
5046
5736
|
var launch = async (config, harness, args) => {
|
|
5047
5737
|
const parsed = parseLaunchArgs(args);
|
|
5048
|
-
if (parsed.error !== null) return
|
|
5738
|
+
if (parsed.error !== null) return fail5(parsed.error);
|
|
5049
5739
|
const structured = parsed.prompt !== null || parsed.model !== null || parsed.effort !== null || parsed.ask || parsed.fast;
|
|
5050
5740
|
if (harness === "run" && structured) {
|
|
5051
|
-
return
|
|
5741
|
+
return fail5(`mend run takes no prompt or harness flags \xB7 ${usageOf("run")}`);
|
|
5052
5742
|
}
|
|
5053
5743
|
if (harness === "run" && (parsed.detach || parsed.foreground)) {
|
|
5054
|
-
return
|
|
5744
|
+
return fail5(
|
|
5055
5745
|
"mend run takes no lifecycle flags \u2014 it tails the record; Ctrl+C stops watching, not the command"
|
|
5056
5746
|
);
|
|
5057
5747
|
}
|
|
5058
5748
|
const argv = harness === "run" ? parsed.custom : HARNESS_COMMANDS[harness] ?? [];
|
|
5059
5749
|
if (argv.length === 0) {
|
|
5060
|
-
return
|
|
5750
|
+
return fail5(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
|
|
5061
5751
|
}
|
|
5062
5752
|
const project2 = await findProject(config, parsed.project, true);
|
|
5063
|
-
|
|
5064
|
-
`${
|
|
5753
|
+
say5(
|
|
5754
|
+
`${green5("\u2713")} project ${project2.name} ${dim5(`\xB7 ${project2.defaultBranch}${parsed.project === null ? " \xB7 from cwd" : ""}`)}`
|
|
5065
5755
|
);
|
|
5066
5756
|
let worktreeName;
|
|
5067
5757
|
if (parsed.worktree !== null) {
|
|
5068
5758
|
const detail = await api(config, "GET", `/projects/${project2.id}`);
|
|
5069
5759
|
const existing = detail.worktrees ?? [];
|
|
5070
5760
|
if (detail.worktrees === void 0) {
|
|
5071
|
-
return
|
|
5761
|
+
return fail5("this server predates shared worktrees \u2014 use --name instead");
|
|
5072
5762
|
}
|
|
5073
5763
|
const match = existing.find((worktree) => worktree.name === parsed.worktree) ?? (existing.filter((worktree) => worktree.name.startsWith(parsed.worktree ?? "")).length === 1 ? existing.find((worktree) => worktree.name.startsWith(parsed.worktree ?? "")) : void 0);
|
|
5074
5764
|
if (match === void 0) {
|
|
5075
5765
|
const names = existing.map((worktree) => worktree.name).join(", ");
|
|
5076
|
-
return
|
|
5766
|
+
return fail5(
|
|
5077
5767
|
`no worktree matches "${parsed.worktree}" in ${project2.name}${names === "" ? "" : ` \u2014 have: ${names}`}`
|
|
5078
5768
|
);
|
|
5079
5769
|
}
|
|
@@ -5090,8 +5780,8 @@ var launch = async (config, harness, args) => {
|
|
|
5090
5780
|
const members = (detail?.sessions ?? []).filter(
|
|
5091
5781
|
(candidate) => candidate.worktreeId === joined.id
|
|
5092
5782
|
).length;
|
|
5093
|
-
|
|
5094
|
-
`${
|
|
5783
|
+
say5(
|
|
5784
|
+
`${green5("\u2713")} joins worktree ${joined.name} ${dim5(`\xB7 ${members} session${members === 1 ? "" : "s"} \xB7 branch ${joined.branch}`)}`
|
|
5095
5785
|
);
|
|
5096
5786
|
}
|
|
5097
5787
|
}
|
|
@@ -5116,12 +5806,12 @@ var launch = async (config, harness, args) => {
|
|
|
5116
5806
|
base: parsed.base
|
|
5117
5807
|
});
|
|
5118
5808
|
createdSessionId = session.id;
|
|
5119
|
-
|
|
5809
|
+
say5(`${green5("\u2713")} worktree ${session.worktree} ${dim5(`\xB7 branch ${session.branch}`)}`);
|
|
5120
5810
|
const baseWord = session.baseRef === null ? "" : `${session.baseRef} `;
|
|
5121
|
-
|
|
5122
|
-
`${
|
|
5811
|
+
say5(
|
|
5812
|
+
`${green5("\u2713")} base ${baseWord}${dim5(session.baseSha.slice(0, 12))} \xB7 session ${dim5(session.id.slice(0, 8))}`
|
|
5123
5813
|
);
|
|
5124
|
-
|
|
5814
|
+
say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
5125
5815
|
if (harness === "run") {
|
|
5126
5816
|
return supervisedRun(config, session, argv);
|
|
5127
5817
|
}
|
|
@@ -5137,23 +5827,28 @@ var launch = async (config, harness, args) => {
|
|
|
5137
5827
|
api(config, "POST", `/sessions/${session.id}/launch`, launchBody)
|
|
5138
5828
|
);
|
|
5139
5829
|
if (lifecycle === "detach") {
|
|
5140
|
-
|
|
5141
|
-
|
|
5830
|
+
say5(`${green5("\u2713 recording")} \xB7 running detached`);
|
|
5831
|
+
say5(`${cobalt2(" attach")} \xB7 mend attach ${session.id.slice(0, 8)}`);
|
|
5142
5832
|
return;
|
|
5143
5833
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5834
|
+
say5(`${green5("\u2713 recording")} \xB7 workspace mounts the worktree${detachHint()}`);
|
|
5835
|
+
say5("");
|
|
5146
5836
|
attachOwnsSignals = true;
|
|
5147
5837
|
await attachOrExit(config, session.id, session.harness, lifecycle);
|
|
5148
5838
|
exitAfterSessionEnd(config, session.id);
|
|
5149
5839
|
};
|
|
5150
5840
|
var attachTty = async (config, sessionId, harness, from, processId, options) => {
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5841
|
+
let url;
|
|
5842
|
+
try {
|
|
5843
|
+
url = await socketUrl(
|
|
5844
|
+
config,
|
|
5845
|
+
"tty",
|
|
5846
|
+
processId === void 0 ? { session: sessionId } : { process: processId },
|
|
5847
|
+
{ from: from.toString() }
|
|
5848
|
+
);
|
|
5849
|
+
} catch {
|
|
5850
|
+
return "unavailable";
|
|
5851
|
+
}
|
|
5157
5852
|
const ws = new WebSocket(url);
|
|
5158
5853
|
ws.binaryType = "arraybuffer";
|
|
5159
5854
|
try {
|
|
@@ -5297,44 +5992,44 @@ var stopSessionQuickly = async (config, sessionId) => {
|
|
|
5297
5992
|
var stopAndExit = async (config, sessionId) => {
|
|
5298
5993
|
const id8 = sessionId.slice(0, 8);
|
|
5299
5994
|
if (await stopSessionQuickly(config, sessionId)) {
|
|
5300
|
-
|
|
5301
|
-
|
|
5995
|
+
say5(`${green5("\u2713")} stopped \xB7 ${id8}`);
|
|
5996
|
+
say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
|
|
5302
5997
|
process.exit(0);
|
|
5303
5998
|
}
|
|
5304
|
-
|
|
5999
|
+
say5(`${amber("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
|
|
5305
6000
|
process.exit(1);
|
|
5306
6001
|
};
|
|
5307
6002
|
var finishAttach = async (config, sessionId, outcome, mode = "background") => {
|
|
5308
6003
|
const id8 = sessionId.slice(0, 8);
|
|
5309
6004
|
if (outcome === "unavailable") {
|
|
5310
|
-
return
|
|
6005
|
+
return fail5(`tty attach unavailable: could not connect to ${config.url}`);
|
|
5311
6006
|
}
|
|
5312
6007
|
if (outcome === "detached") {
|
|
5313
|
-
|
|
5314
|
-
|
|
6008
|
+
say5("");
|
|
6009
|
+
say5(`${amber("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
5315
6010
|
process.exit(0);
|
|
5316
6011
|
}
|
|
5317
6012
|
if (outcome === "interrupted") {
|
|
5318
|
-
|
|
6013
|
+
say5("");
|
|
5319
6014
|
if (mode === "foreground") return stopAndExit(config, sessionId);
|
|
5320
|
-
|
|
6015
|
+
say5(`${amber("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
5321
6016
|
process.exit(0);
|
|
5322
6017
|
}
|
|
5323
6018
|
if (outcome === "dropped") {
|
|
5324
|
-
|
|
6019
|
+
say5("");
|
|
5325
6020
|
if (mode === "foreground") {
|
|
5326
6021
|
let live;
|
|
5327
6022
|
try {
|
|
5328
6023
|
const detail = await request(config, "GET", `/sessions/${sessionId}`);
|
|
5329
6024
|
live = agentIsLive(detail.session, detail.currentAgent);
|
|
5330
6025
|
} catch {
|
|
5331
|
-
|
|
6026
|
+
say5(`${amber("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
|
|
5332
6027
|
process.exit(1);
|
|
5333
6028
|
}
|
|
5334
6029
|
if (!live) return;
|
|
5335
6030
|
return stopAndExit(config, sessionId);
|
|
5336
6031
|
}
|
|
5337
|
-
|
|
6032
|
+
say5(`${amber("disconnected")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
5338
6033
|
process.exit(0);
|
|
5339
6034
|
}
|
|
5340
6035
|
};
|
|
@@ -5345,9 +6040,9 @@ var attachOrExit = async (config, sessionId, harness, mode = "background") => fi
|
|
|
5345
6040
|
mode
|
|
5346
6041
|
);
|
|
5347
6042
|
var exitAfterSessionEnd = (config, sessionId) => {
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
6043
|
+
say5("");
|
|
6044
|
+
say5(`${green5("\u2713")} session ended`);
|
|
6045
|
+
say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
|
|
5351
6046
|
process.exit(0);
|
|
5352
6047
|
};
|
|
5353
6048
|
var attach = async (config, args) => {
|
|
@@ -5358,26 +6053,26 @@ var attach = async (config, args) => {
|
|
|
5358
6053
|
}
|
|
5359
6054
|
const sessions = await api(config, "GET", "/sessions");
|
|
5360
6055
|
const match = sessions.find((s) => s.id.startsWith(prefix));
|
|
5361
|
-
if (match === void 0) return
|
|
6056
|
+
if (match === void 0) return fail5(`no active session matches "${prefix}"`);
|
|
5362
6057
|
return attachPicked(config, match);
|
|
5363
6058
|
};
|
|
5364
6059
|
var attachPicked = async (config, session) => {
|
|
5365
|
-
|
|
5366
|
-
`${
|
|
6060
|
+
say5(
|
|
6061
|
+
`${green5("\u2713")} attaching to ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim5(session.id.slice(0, 8))}${detachHint()}`
|
|
5367
6062
|
);
|
|
5368
|
-
|
|
6063
|
+
say5("");
|
|
5369
6064
|
const outcome = await attachTty(config, session.id, session.harness, 0n, void 0, {
|
|
5370
6065
|
handleSignals: true
|
|
5371
6066
|
});
|
|
5372
6067
|
if (outcome === "unavailable") {
|
|
5373
6068
|
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
5374
6069
|
if (detail.currentAgent?.kind === "agent-protocol" && agentIsLive(detail.session, detail.currentAgent)) {
|
|
5375
|
-
|
|
6070
|
+
say5(`${amber("taking over")} from the protocol session`);
|
|
5376
6071
|
await withSpinner(
|
|
5377
6072
|
"reopening as a terminal \u2014 same conversation\u2026",
|
|
5378
6073
|
api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
|
|
5379
6074
|
);
|
|
5380
|
-
|
|
6075
|
+
say5("");
|
|
5381
6076
|
await attachOrExit(config, session.id, session.harness);
|
|
5382
6077
|
return exitAfterSessionEnd(config, session.id);
|
|
5383
6078
|
}
|
|
@@ -5396,7 +6091,7 @@ var stopCommand = async (config, args) => {
|
|
|
5396
6091
|
return;
|
|
5397
6092
|
}
|
|
5398
6093
|
if (!all && prefix === void 0) {
|
|
5399
|
-
return
|
|
6094
|
+
return fail5(usageOf("stop"));
|
|
5400
6095
|
}
|
|
5401
6096
|
const [sessions, projects] = await Promise.all([
|
|
5402
6097
|
api(config, "GET", "/sessions"),
|
|
@@ -5405,21 +6100,21 @@ var stopCommand = async (config, args) => {
|
|
|
5405
6100
|
let scoped = sessions;
|
|
5406
6101
|
if (projectName !== null) {
|
|
5407
6102
|
const project2 = projects.find((p) => p.name === projectName);
|
|
5408
|
-
if (project2 === void 0) return
|
|
6103
|
+
if (project2 === void 0) return fail5(`no project named "${projectName}"`);
|
|
5409
6104
|
scoped = sessions.filter((s) => s.projectId === project2.id);
|
|
5410
6105
|
}
|
|
5411
6106
|
let targets;
|
|
5412
6107
|
if (all) {
|
|
5413
6108
|
targets = scoped;
|
|
5414
6109
|
if (targets.length === 0) {
|
|
5415
|
-
|
|
6110
|
+
say5("no active sessions");
|
|
5416
6111
|
return;
|
|
5417
6112
|
}
|
|
5418
6113
|
} else {
|
|
5419
6114
|
const matches = scoped.filter((s) => s.id.startsWith(prefix ?? ""));
|
|
5420
|
-
if (matches.length === 0) return
|
|
6115
|
+
if (matches.length === 0) return fail5(`no active session matches "${prefix}"`);
|
|
5421
6116
|
if (matches.length > 1) {
|
|
5422
|
-
return
|
|
6117
|
+
return fail5(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
|
|
5423
6118
|
}
|
|
5424
6119
|
targets = matches;
|
|
5425
6120
|
}
|
|
@@ -5428,20 +6123,20 @@ var stopCommand = async (config, args) => {
|
|
|
5428
6123
|
var stopSessions = async (config, targets, summarise) => {
|
|
5429
6124
|
for (const session of targets) {
|
|
5430
6125
|
await api(config, "POST", `/sessions/${session.id}/stop`);
|
|
5431
|
-
|
|
5432
|
-
`${
|
|
6126
|
+
say5(
|
|
6127
|
+
`${green5("\u2713")} stopped \xB7 ${session.harness} \xB7 ${dim5(session.id.slice(0, 8))} \xB7 ${session.branch}`
|
|
5433
6128
|
);
|
|
5434
|
-
|
|
6129
|
+
say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
5435
6130
|
}
|
|
5436
6131
|
if (summarise) {
|
|
5437
|
-
|
|
6132
|
+
say5(`${green5("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
|
|
5438
6133
|
}
|
|
5439
6134
|
};
|
|
5440
6135
|
var pickSessionInteractively = async (rows) => {
|
|
5441
|
-
|
|
6136
|
+
say5(dim5("more than one live session \u2014 pick one:"));
|
|
5442
6137
|
rows.forEach((row, index) => {
|
|
5443
|
-
|
|
5444
|
-
` ${index + 1}. ${row.session.harness.padEnd(8)} ${
|
|
6138
|
+
say5(
|
|
6139
|
+
` ${index + 1}. ${row.session.harness.padEnd(8)} ${dim5(row.session.id.slice(0, 8))} ${row.projectName} ${dim5(row.session.branch)} ${dim5(row.session.status)}`
|
|
5445
6140
|
);
|
|
5446
6141
|
});
|
|
5447
6142
|
const readline = await import("node:readline/promises");
|
|
@@ -5449,17 +6144,17 @@ var pickSessionInteractively = async (rows) => {
|
|
|
5449
6144
|
const answer = await rl.question(" session #: ");
|
|
5450
6145
|
rl.close();
|
|
5451
6146
|
const chosen = rows[Number(answer.trim()) - 1];
|
|
5452
|
-
if (chosen === void 0) return
|
|
6147
|
+
if (chosen === void 0) return fail5(`"${answer.trim()}" is not one of the choices`);
|
|
5453
6148
|
return chosen.session;
|
|
5454
6149
|
};
|
|
5455
6150
|
var resolveLiveSession = async (config, prefix, command) => {
|
|
5456
6151
|
const sessions = await api(config, "GET", "/sessions?retained=1");
|
|
5457
6152
|
if (prefix !== void 0) {
|
|
5458
6153
|
const matches = sessions.filter((s) => s.id.startsWith(prefix));
|
|
5459
|
-
if (matches.length === 0) return
|
|
6154
|
+
if (matches.length === 0) return fail5(`no live or retained session matches "${prefix}"`);
|
|
5460
6155
|
const exact = matches[0];
|
|
5461
6156
|
if (matches.length > 1 || exact === void 0) {
|
|
5462
|
-
return
|
|
6157
|
+
return fail5(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
|
|
5463
6158
|
}
|
|
5464
6159
|
return exact;
|
|
5465
6160
|
}
|
|
@@ -5468,11 +6163,11 @@ var resolveLiveSession = async (config, prefix, command) => {
|
|
|
5468
6163
|
const candidates = project2 === void 0 ? sessions : sessions.filter((s) => s.projectId === project2.id);
|
|
5469
6164
|
const only = candidates[0];
|
|
5470
6165
|
if (only === void 0) {
|
|
5471
|
-
return
|
|
6166
|
+
return fail5("no live or retained session \u2014 start one with mend codex|claude|opencode");
|
|
5472
6167
|
}
|
|
5473
6168
|
if (candidates.length === 1) return only;
|
|
5474
6169
|
if (process.stdin.isTTY !== true) {
|
|
5475
|
-
return
|
|
6170
|
+
return fail5(`several live sessions \u2014 name one: mend ${command} <session-id-prefix>`);
|
|
5476
6171
|
}
|
|
5477
6172
|
const nameById = new Map(projects.map((p) => [p.id, p.name]));
|
|
5478
6173
|
return pickSessionInteractively(
|
|
@@ -5485,26 +6180,26 @@ var resolveLiveSession = async (config, prefix, command) => {
|
|
|
5485
6180
|
var shellCommand = async (config, args) => {
|
|
5486
6181
|
const prefix = args.find((a) => !a.startsWith("--"));
|
|
5487
6182
|
const session = await resolveLiveSession(config, prefix, "shell");
|
|
5488
|
-
|
|
5489
|
-
`${
|
|
6183
|
+
say5(
|
|
6184
|
+
`${green5("\u2713")} shell in ${session.harness} session ${dim5(session.id.slice(0, 8))} \xB7 ${dim5(session.branch)}${detachHint()}`
|
|
5490
6185
|
);
|
|
5491
6186
|
const shellProcess = await withSpinner(
|
|
5492
6187
|
"opening a shell in the session workspace\u2026",
|
|
5493
6188
|
api(config, "POST", `/sessions/${session.id}/shell`)
|
|
5494
6189
|
);
|
|
5495
|
-
|
|
6190
|
+
say5("");
|
|
5496
6191
|
const outcome = await attachTty(config, session.id, "shell", 0n, shellProcess.id, {
|
|
5497
6192
|
handleSignals: true
|
|
5498
6193
|
});
|
|
5499
6194
|
if (outcome === "unavailable") {
|
|
5500
|
-
return
|
|
6195
|
+
return fail5(`tty attach unavailable: could not connect to ${config.url}`);
|
|
5501
6196
|
}
|
|
5502
|
-
|
|
6197
|
+
say5("");
|
|
5503
6198
|
if (outcome === "detached" || outcome === "interrupted" || outcome === "dropped") {
|
|
5504
|
-
|
|
6199
|
+
say5(`${amber("detached")} \u2014 the shell keeps running and holds the workspace open`);
|
|
5505
6200
|
process.exit(0);
|
|
5506
6201
|
}
|
|
5507
|
-
|
|
6202
|
+
say5(`${green5("\u2713")} shell ended`);
|
|
5508
6203
|
process.exit(0);
|
|
5509
6204
|
};
|
|
5510
6205
|
var flattenService = (view) => {
|
|
@@ -5545,30 +6240,30 @@ var serviceUrl = (service) => service.browserUrl ?? (service.authority === null
|
|
|
5545
6240
|
var printServiceAccess = (config, service, tunneling = false) => {
|
|
5546
6241
|
const gate = "no Mend sign-in on this port \u2014 network reach is the only gate";
|
|
5547
6242
|
if (serverIsLocal(config)) {
|
|
5548
|
-
if (service.authority !== null)
|
|
6243
|
+
if (service.authority !== null) say5(` ${cobalt2(serviceUrl(service))} ${dim5(gate)}`);
|
|
5549
6244
|
return;
|
|
5550
6245
|
}
|
|
5551
6246
|
if (service.protocol === "udp") {
|
|
5552
6247
|
if (service.authority !== null) {
|
|
5553
|
-
|
|
6248
|
+
say5(` ${cobalt2(serviceUrl(service))} ${dim5(`on the server's network \xB7 ${gate}`)}`);
|
|
5554
6249
|
}
|
|
5555
6250
|
return;
|
|
5556
6251
|
}
|
|
5557
6252
|
if (!tunneling) {
|
|
5558
6253
|
const local = service.hostPort ?? service.workspacePort;
|
|
5559
|
-
|
|
5560
|
-
` ${cobalt2(`mend service connect ${service.label}`)} ${
|
|
6254
|
+
say5(
|
|
6255
|
+
` ${cobalt2(`mend service connect ${service.label}`)} ${dim5(`\u2192 127.0.0.1:${local} on this machine, authenticated as you`)}`
|
|
5561
6256
|
);
|
|
5562
6257
|
}
|
|
5563
6258
|
if (service.authority !== null) {
|
|
5564
|
-
|
|
6259
|
+
say5(` ${dim5(`server-side listener ${service.authority} \xB7 ${gate}`)}`);
|
|
5565
6260
|
}
|
|
5566
6261
|
};
|
|
5567
6262
|
var printWorkspaceTtlFailure = (service) => {
|
|
5568
6263
|
if (service.workspaceTtlRenewalError === null) return;
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
6264
|
+
say5(amber(` workspace TTL renewal failed \xB7 ${service.workspaceTtlRenewalError}`));
|
|
6265
|
+
say5(
|
|
6266
|
+
dim5(
|
|
5572
6267
|
` last renewed ${service.workspaceTtlRenewedAt ?? "unknown"} \xB7 known expiry ${service.workspaceExpiresAt ?? "unknown"} \xB7 failed ${service.workspaceTtlRenewalFailedAt ?? "unknown"}`
|
|
5573
6268
|
)
|
|
5574
6269
|
);
|
|
@@ -5578,11 +6273,11 @@ var printServiceEndpoint = (config, service, tunneling = false) => {
|
|
|
5578
6273
|
printWorkspaceTtlFailure(service);
|
|
5579
6274
|
};
|
|
5580
6275
|
var printService = (config, service) => {
|
|
5581
|
-
const status = service.status === "reachable" ?
|
|
6276
|
+
const status = service.status === "reachable" ? green5(service.status) : amber(service.status);
|
|
5582
6277
|
const port = `:${service.workspacePort ?? "?"}${service.protocol === "udp" ? "/udp" : ""}`;
|
|
5583
6278
|
const statusPad = " ".repeat(Math.max(1, 12 - service.status.length));
|
|
5584
|
-
|
|
5585
|
-
`${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${
|
|
6279
|
+
say5(
|
|
6280
|
+
`${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${dim5(port.padEnd(7))} ${dim5(service.id.slice(0, 8))}`
|
|
5586
6281
|
);
|
|
5587
6282
|
printServiceAccess(config, service);
|
|
5588
6283
|
printWorkspaceTtlFailure(service);
|
|
@@ -5593,21 +6288,21 @@ var serviceAdd = async (config, args) => {
|
|
|
5593
6288
|
const protocol = args.includes("--udp") ? "udp" : "tcp";
|
|
5594
6289
|
const http = args.includes("--http");
|
|
5595
6290
|
const https = args.includes("--https");
|
|
5596
|
-
if (http && https) return
|
|
6291
|
+
if (http && https) return fail5("Choose either --http or --https, not both.");
|
|
5597
6292
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
5598
6293
|
if (protocol === "udp" && browserScheme !== null) {
|
|
5599
|
-
return
|
|
6294
|
+
return fail5("UDP Services cannot use --http or --https");
|
|
5600
6295
|
}
|
|
5601
6296
|
const positional = args.filter(
|
|
5602
6297
|
(a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1)
|
|
5603
6298
|
);
|
|
5604
6299
|
const portRaw = positional.find((a) => /^\d+$/.test(a));
|
|
5605
6300
|
if (portRaw === void 0) {
|
|
5606
|
-
return
|
|
6301
|
+
return fail5(usageOf("service add"));
|
|
5607
6302
|
}
|
|
5608
6303
|
const port = Number(portRaw);
|
|
5609
6304
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5610
|
-
return
|
|
6305
|
+
return fail5(`"${portRaw}" is not a port`);
|
|
5611
6306
|
}
|
|
5612
6307
|
const prefix = positional.find((a) => a !== portRaw);
|
|
5613
6308
|
const session = await resolveLiveSession(config, prefix, "service add");
|
|
@@ -5617,52 +6312,52 @@ var serviceAdd = async (config, args) => {
|
|
|
5617
6312
|
protocol,
|
|
5618
6313
|
browserScheme
|
|
5619
6314
|
});
|
|
5620
|
-
|
|
6315
|
+
say5(`${green5("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
|
|
5621
6316
|
printServiceEndpoint(config, service);
|
|
5622
6317
|
if (protocol === "udp") {
|
|
5623
|
-
|
|
6318
|
+
say5(dim5(` udp \u2014 a reply is the only reachability signal; silence just relays`));
|
|
5624
6319
|
} else if (service.status !== "reachable") {
|
|
5625
|
-
|
|
6320
|
+
say5(dim5(` nothing answered on :${port} yet \u2014 the URL goes live when something listens`));
|
|
5626
6321
|
}
|
|
5627
6322
|
};
|
|
5628
6323
|
var serviceList = async (config) => {
|
|
5629
6324
|
const services = await fetchServices(config);
|
|
5630
6325
|
if (services.length === 0) {
|
|
5631
|
-
|
|
6326
|
+
say5(dim5("no live services \u2014 mend service add <port> adopts a listening one"));
|
|
5632
6327
|
return;
|
|
5633
6328
|
}
|
|
5634
6329
|
for (const service of services) printService(config, service);
|
|
5635
6330
|
};
|
|
5636
6331
|
var serviceStop = async (config, args) => {
|
|
5637
6332
|
const needle = args.find((a) => !a.startsWith("--"));
|
|
5638
|
-
if (needle === void 0) return
|
|
6333
|
+
if (needle === void 0) return fail5(usageOf("service stop"));
|
|
5639
6334
|
const services = await fetchServices(config);
|
|
5640
6335
|
const matches = services.filter(
|
|
5641
6336
|
(service) => service.label === needle || service.id.startsWith(needle)
|
|
5642
6337
|
);
|
|
5643
|
-
if (matches.length === 0) return
|
|
6338
|
+
if (matches.length === 0) return fail5(`no live service matches "${needle}"`);
|
|
5644
6339
|
const match = matches[0];
|
|
5645
6340
|
if (matches.length > 1 || match === void 0) {
|
|
5646
|
-
return
|
|
6341
|
+
return fail5(`"${needle}" is ambiguous \u2014 use more of the id`);
|
|
5647
6342
|
}
|
|
5648
6343
|
const stoppedService = await mutateService(config, "POST", `/services/${match.id}/stop`);
|
|
5649
|
-
|
|
6344
|
+
say5(`${green5("\u2713")} stopped \xB7 ${stoppedService.label ?? stoppedService.id.slice(0, 8)}`);
|
|
5650
6345
|
};
|
|
5651
6346
|
var findLiveService = async (config, needle) => {
|
|
5652
6347
|
const services = await fetchServices(config);
|
|
5653
6348
|
const matches = services.filter(
|
|
5654
6349
|
(service) => service.label === needle || service.id.startsWith(needle)
|
|
5655
6350
|
);
|
|
5656
|
-
if (matches.length === 0) return
|
|
6351
|
+
if (matches.length === 0) return fail5(`no live service matches "${needle}"`);
|
|
5657
6352
|
const match = matches[0];
|
|
5658
6353
|
if (matches.length > 1 || match === void 0) {
|
|
5659
|
-
return
|
|
6354
|
+
return fail5(`"${needle}" is ambiguous \u2014 use more of the id`);
|
|
5660
6355
|
}
|
|
5661
6356
|
return match;
|
|
5662
6357
|
};
|
|
5663
6358
|
var willAutoConnect = (config, service, optOut) => !optOut && !serverIsLocal(config) && service.protocol !== "udp";
|
|
5664
6359
|
var autoConnect = async (config, service) => {
|
|
5665
|
-
|
|
6360
|
+
say5(dim5(" remote server \u2014 tunneling the port here \xB7 Ctrl-C stops the tunnel, not the Service"));
|
|
5666
6361
|
await tunnelServices(config, [service], null);
|
|
5667
6362
|
};
|
|
5668
6363
|
var serviceRun = async (config, args) => {
|
|
@@ -5671,7 +6366,7 @@ var serviceRun = async (config, args) => {
|
|
|
5671
6366
|
if (dashdash === -1) {
|
|
5672
6367
|
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
5673
6368
|
const name2 = positionals.at(-1);
|
|
5674
|
-
if (name2 === void 0) return
|
|
6369
|
+
if (name2 === void 0) return fail5(usage);
|
|
5675
6370
|
const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
|
|
5676
6371
|
const session2 = await resolveLiveSession(config, prefix2, "service run");
|
|
5677
6372
|
const recipes = await api(
|
|
@@ -5682,7 +6377,7 @@ var serviceRun = async (config, args) => {
|
|
|
5682
6377
|
const recipe = recipes.find((entry) => entry.name === name2);
|
|
5683
6378
|
if (recipe === void 0) {
|
|
5684
6379
|
const known = recipes.map((entry) => entry.name).join(", ");
|
|
5685
|
-
return
|
|
6380
|
+
return fail5(
|
|
5686
6381
|
recipes.length === 0 ? `no mend.toml recipes in this worktree \u2014 declare [service.${name2}] first` : `no recipe named "${name2}" \u2014 declared: ${known}`
|
|
5687
6382
|
);
|
|
5688
6383
|
}
|
|
@@ -5693,26 +6388,26 @@ var serviceRun = async (config, args) => {
|
|
|
5693
6388
|
})
|
|
5694
6389
|
);
|
|
5695
6390
|
const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect"));
|
|
5696
|
-
|
|
6391
|
+
say5(`${green5("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
|
|
5697
6392
|
printServiceEndpoint(config, service2, tunneling2);
|
|
5698
|
-
|
|
6393
|
+
say5(dim5(` logs: mend service logs ${service2.label ?? service2.id.slice(0, 8)}`));
|
|
5699
6394
|
if (tunneling2) await autoConnect(config, service2);
|
|
5700
6395
|
return;
|
|
5701
6396
|
}
|
|
5702
6397
|
const argv = args.slice(dashdash + 1);
|
|
5703
|
-
if (argv.length === 0) return
|
|
6398
|
+
if (argv.length === 0) return fail5(usage);
|
|
5704
6399
|
const head = args.slice(0, dashdash);
|
|
5705
6400
|
const portFlag = head.indexOf("--port");
|
|
5706
6401
|
const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
|
|
5707
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) return
|
|
6402
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return fail5(usage);
|
|
5708
6403
|
const nameFlag = head.indexOf("--name");
|
|
5709
6404
|
const name = nameFlag !== -1 && head[nameFlag + 1] !== void 0 ? String(head[nameFlag + 1]) : null;
|
|
5710
6405
|
const protocol = head.includes("--udp") ? "udp" : "tcp";
|
|
5711
6406
|
const http = head.includes("--http");
|
|
5712
6407
|
const https = head.includes("--https");
|
|
5713
|
-
if (http && https) return
|
|
6408
|
+
if (http && https) return fail5(usage);
|
|
5714
6409
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
5715
|
-
if (protocol === "udp" && browserScheme !== null) return
|
|
6410
|
+
if (protocol === "udp" && browserScheme !== null) return fail5(usage);
|
|
5716
6411
|
const prefix = head.find(
|
|
5717
6412
|
(a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
|
|
5718
6413
|
);
|
|
@@ -5728,9 +6423,9 @@ var serviceRun = async (config, args) => {
|
|
|
5728
6423
|
})
|
|
5729
6424
|
);
|
|
5730
6425
|
const tunneling = willAutoConnect(config, service, head.includes("--no-connect"));
|
|
5731
|
-
|
|
6426
|
+
say5(`${green5("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
|
|
5732
6427
|
printServiceEndpoint(config, service, tunneling);
|
|
5733
|
-
|
|
6428
|
+
say5(dim5(` logs: mend service logs ${service.label ?? service.id.slice(0, 8)}`));
|
|
5734
6429
|
if (tunneling) await autoConnect(config, service);
|
|
5735
6430
|
};
|
|
5736
6431
|
var serviceLogs = async (config, args) => {
|
|
@@ -5740,32 +6435,32 @@ var serviceLogs = async (config, args) => {
|
|
|
5740
6435
|
(argument, index) => !argument.startsWith("--") && index !== fromFlag + 1
|
|
5741
6436
|
);
|
|
5742
6437
|
if (needle === void 0 || from === void 0 || !/^(0|[1-9]\d*)$/.test(from)) {
|
|
5743
|
-
return
|
|
6438
|
+
return fail5(usageOf("service logs"));
|
|
5744
6439
|
}
|
|
5745
6440
|
const everything = await fetchServiceViews(config, true);
|
|
5746
6441
|
const matches = everything.filter(
|
|
5747
6442
|
(view2) => view2.service.name === needle || view2.service.id.startsWith(needle) || view2.attempts.some((attempt3) => attempt3.id.startsWith(needle))
|
|
5748
6443
|
);
|
|
5749
|
-
if (matches.length === 0) return
|
|
6444
|
+
if (matches.length === 0) return fail5(`no service matches "${needle}"`);
|
|
5750
6445
|
const view = matches.find((candidate) => {
|
|
5751
6446
|
const current = candidate.service.currentAttemptId === null ? null : candidate.attempts.find((attempt3) => attempt3.id === candidate.service.currentAttemptId);
|
|
5752
6447
|
return current !== null && current !== void 0 && current.exitedAt === null;
|
|
5753
6448
|
}) ?? matches[0];
|
|
5754
|
-
if (view === void 0) return
|
|
6449
|
+
if (view === void 0) return fail5(`no service matches "${needle}"`);
|
|
5755
6450
|
const currentAttempt = view.service.currentAttemptId === null ? null : view.attempts.find((candidate) => candidate.id === view.service.currentAttemptId) ?? null;
|
|
5756
6451
|
const attempt2 = view.attempts.find((candidate) => candidate.id.startsWith(needle)) ?? currentAttempt ?? view.attempts.findLast((candidate) => candidate.sealantSessionId !== null) ?? null;
|
|
5757
6452
|
if (attempt2?.sealantSessionId === null || attempt2 === null) {
|
|
5758
|
-
return
|
|
6453
|
+
return fail5(
|
|
5759
6454
|
`"${needle}" is an adopted port \u2014 no process of Mend's, no logs. mend service run supervises.`
|
|
5760
6455
|
);
|
|
5761
6456
|
}
|
|
5762
6457
|
const label = view.service.name;
|
|
5763
|
-
|
|
5764
|
-
|
|
6458
|
+
say5(
|
|
6459
|
+
dim5(
|
|
5765
6460
|
attempt2.exitedAt === null ? `following ${label} from sequence ${from} \u2014 Ctrl+C detaches, the Service keeps running` : `${label} \xB7 ${attempt2.status} \u2014 recorded output from sequence ${from}`
|
|
5766
6461
|
)
|
|
5767
6462
|
);
|
|
5768
|
-
|
|
6463
|
+
say5("");
|
|
5769
6464
|
let cursor = from;
|
|
5770
6465
|
let telemetryReported = false;
|
|
5771
6466
|
for (; ; ) {
|
|
@@ -5775,7 +6470,7 @@ var serviceLogs = async (config, args) => {
|
|
|
5775
6470
|
`/processes/${attempt2.id}/logs?from=${encodeURIComponent(cursor)}&limit=256`
|
|
5776
6471
|
);
|
|
5777
6472
|
if (!telemetryReported) {
|
|
5778
|
-
|
|
6473
|
+
say5(dim5(`telemetry loss: ${page.telemetryLoss} \xB7 ${page.telemetryNote}`));
|
|
5779
6474
|
telemetryReported = true;
|
|
5780
6475
|
}
|
|
5781
6476
|
for (const chunk of page.chunks) {
|
|
@@ -5787,29 +6482,29 @@ var serviceLogs = async (config, args) => {
|
|
|
5787
6482
|
if (ended && !advanced) break;
|
|
5788
6483
|
if (!advanced) await new Promise((resolve4) => setTimeout(resolve4, 250));
|
|
5789
6484
|
}
|
|
5790
|
-
|
|
5791
|
-
|
|
6485
|
+
say5("");
|
|
6486
|
+
say5(dim5(`stream ended \xB7 next sequence ${cursor}`));
|
|
5792
6487
|
};
|
|
5793
6488
|
var serviceRestart = async (config, args) => {
|
|
5794
6489
|
const needle = args.find((a) => !a.startsWith("--"));
|
|
5795
|
-
if (needle === void 0) return
|
|
6490
|
+
if (needle === void 0) return fail5(usageOf("service restart"));
|
|
5796
6491
|
const service = await findLiveService(config, needle);
|
|
5797
6492
|
const restarted = await withSpinner(
|
|
5798
6493
|
`restarting ${service.label ?? service.id.slice(0, 8)}\u2026`,
|
|
5799
6494
|
mutateService(config, "POST", `/services/${service.id}/restart`)
|
|
5800
6495
|
);
|
|
5801
|
-
|
|
6496
|
+
say5(`${green5("\u2713")} restarted \xB7 ${restarted.status}`);
|
|
5802
6497
|
printServiceEndpoint(config, restarted);
|
|
5803
6498
|
};
|
|
5804
6499
|
var serviceInit = async (args) => {
|
|
5805
6500
|
const root = gitTopLevel(process.cwd()) ?? process.cwd();
|
|
5806
|
-
const target =
|
|
5807
|
-
if (
|
|
5808
|
-
return
|
|
6501
|
+
const target = path12.join(root, "mend.toml");
|
|
6502
|
+
if (fs11.existsSync(target)) {
|
|
6503
|
+
return fail5(`${target} already exists \u2014 edit it directly (init never merges)`);
|
|
5809
6504
|
}
|
|
5810
|
-
const rootFiles =
|
|
6505
|
+
const rootFiles = fs11.readdirSync(root);
|
|
5811
6506
|
const proposals = [];
|
|
5812
|
-
const readRoot = (name) =>
|
|
6507
|
+
const readRoot = (name) => fs11.readFileSync(path12.join(root, name), "utf8");
|
|
5813
6508
|
if (rootFiles.includes("package.json")) {
|
|
5814
6509
|
proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
|
|
5815
6510
|
}
|
|
@@ -5819,13 +6514,13 @@ var serviceInit = async (args) => {
|
|
|
5819
6514
|
);
|
|
5820
6515
|
for (const glob of globs) {
|
|
5821
6516
|
const parent = glob.replace(/\/?\*+$/, "");
|
|
5822
|
-
const dirs = glob.endsWith("*") ?
|
|
6517
|
+
const dirs = glob.endsWith("*") ? fs11.existsSync(path12.join(root, parent)) ? fs11.readdirSync(path12.join(root, parent)).map((dir) => path12.join(parent, dir)) : [] : [glob];
|
|
5823
6518
|
for (const dir of dirs) {
|
|
5824
|
-
const manifest =
|
|
5825
|
-
if (!
|
|
6519
|
+
const manifest = path12.join(root, dir, "package.json");
|
|
6520
|
+
if (!fs11.existsSync(manifest)) continue;
|
|
5826
6521
|
for (const proposal of proposeFromWorkspacePackage(
|
|
5827
|
-
|
|
5828
|
-
|
|
6522
|
+
path12.basename(dir),
|
|
6523
|
+
fs11.readFileSync(manifest, "utf8"),
|
|
5829
6524
|
rootFiles
|
|
5830
6525
|
)) {
|
|
5831
6526
|
if (!proposals.some((existing) => existing.name === proposal.name)) {
|
|
@@ -5847,58 +6542,60 @@ var serviceInit = async (args) => {
|
|
|
5847
6542
|
}
|
|
5848
6543
|
}
|
|
5849
6544
|
if (proposals.length === 0) {
|
|
5850
|
-
return
|
|
6545
|
+
return fail5(
|
|
5851
6546
|
"nothing to propose \u2014 no server-ish package.json script with a nameable port, no compose ports"
|
|
5852
6547
|
);
|
|
5853
6548
|
}
|
|
5854
6549
|
const toml = renderMendToml(proposals);
|
|
5855
|
-
|
|
5856
|
-
|
|
6550
|
+
say5(dim5(`proposed ${target}:`));
|
|
6551
|
+
say5("");
|
|
5857
6552
|
process.stdout.write(toml);
|
|
5858
|
-
|
|
6553
|
+
say5("");
|
|
5859
6554
|
if (!args.includes("--yes")) {
|
|
5860
6555
|
if (process.stdin.isTTY !== true) {
|
|
5861
|
-
return
|
|
6556
|
+
return fail5("non-interactive \u2014 pass --yes to write the file");
|
|
5862
6557
|
}
|
|
5863
6558
|
const readline = await import("node:readline/promises");
|
|
5864
6559
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
5865
6560
|
const answer = await rl.question("write it? [y/N] ");
|
|
5866
6561
|
rl.close();
|
|
5867
6562
|
if (answer.trim().toLowerCase() !== "y") {
|
|
5868
|
-
|
|
6563
|
+
say5(dim5("nothing written"));
|
|
5869
6564
|
return;
|
|
5870
6565
|
}
|
|
5871
6566
|
}
|
|
5872
|
-
|
|
5873
|
-
|
|
6567
|
+
fs11.writeFileSync(target, toml);
|
|
6568
|
+
say5(`${green5("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
|
|
5874
6569
|
};
|
|
5875
6570
|
var tunnelServices = async (config, services, portOverride) => {
|
|
5876
|
-
const tunnelUrl = (serviceId) => {
|
|
5877
|
-
const url = parseMendUrl(`${config.url}/api/service-tunnel`);
|
|
5878
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
5879
|
-
url.searchParams.set("service", serviceId);
|
|
5880
|
-
if (config.token !== null) url.searchParams.set("token", config.token);
|
|
5881
|
-
return url;
|
|
5882
|
-
};
|
|
6571
|
+
const tunnelUrl = (serviceId) => socketUrl(config, "service-tunnel", { service: serviceId });
|
|
5883
6572
|
for (const service of services) {
|
|
5884
6573
|
const port = portOverride ?? service.hostPort ?? service.workspacePort;
|
|
5885
6574
|
const server = net3.createServer((socket) => {
|
|
5886
6575
|
socket.pause();
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
6576
|
+
void tunnelUrl(service.id).then(
|
|
6577
|
+
(url) => {
|
|
6578
|
+
if (socket.destroyed) return null;
|
|
6579
|
+
const ws = new WebSocket(url);
|
|
6580
|
+
ws.binaryType = "arraybuffer";
|
|
6581
|
+
ws.addEventListener("open", () => socket.resume(), { once: true });
|
|
6582
|
+
ws.addEventListener("message", (event) => {
|
|
6583
|
+
if (typeof event.data === "string") return;
|
|
6584
|
+
socket.write(Buffer.from(event.data));
|
|
6585
|
+
});
|
|
6586
|
+
ws.addEventListener("close", () => socket.end(), { once: true });
|
|
6587
|
+
ws.addEventListener("error", () => socket.destroy(), { once: true });
|
|
6588
|
+
socket.on("data", (chunk) => ws.send(new Uint8Array(chunk)));
|
|
6589
|
+
socket.on("end", () => {
|
|
6590
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ t: "eof" }));
|
|
6591
|
+
});
|
|
6592
|
+
socket.on("close", () => ws.close());
|
|
6593
|
+
socket.on("error", () => ws.close());
|
|
6594
|
+
return null;
|
|
6595
|
+
},
|
|
6596
|
+
// No ticket, no tunnel: the local connection ends the way a refused one would.
|
|
6597
|
+
() => socket.destroy()
|
|
6598
|
+
);
|
|
5902
6599
|
});
|
|
5903
6600
|
await new Promise((resolve4, reject) => {
|
|
5904
6601
|
server.once("error", (error) => {
|
|
@@ -5909,12 +6606,12 @@ var tunnelServices = async (config, services, portOverride) => {
|
|
|
5909
6606
|
);
|
|
5910
6607
|
});
|
|
5911
6608
|
server.listen(port, "127.0.0.1", () => resolve4());
|
|
5912
|
-
}).catch((error) =>
|
|
5913
|
-
|
|
5914
|
-
`${
|
|
6609
|
+
}).catch((error) => fail5(error.message));
|
|
6610
|
+
say5(
|
|
6611
|
+
`${green5("\u25CF")} ${service.label ?? service.id.slice(0, 8)} \u2192 127.0.0.1:${port} ${dim5(`(tunnel to ${config.url})`)}`
|
|
5915
6612
|
);
|
|
5916
6613
|
}
|
|
5917
|
-
|
|
6614
|
+
say5(dim5(" connections are authenticated as you \xB7 Ctrl-C stops"));
|
|
5918
6615
|
await new Promise(() => {
|
|
5919
6616
|
});
|
|
5920
6617
|
};
|
|
@@ -5922,18 +6619,18 @@ var serviceConnect = async (config, args) => {
|
|
|
5922
6619
|
const portFlag = args.indexOf("--port");
|
|
5923
6620
|
const portOverride = portFlag === -1 ? null : Number(args[portFlag + 1]);
|
|
5924
6621
|
if (portOverride !== null && !Number.isInteger(portOverride)) {
|
|
5925
|
-
return
|
|
6622
|
+
return fail5("--port takes a port number");
|
|
5926
6623
|
}
|
|
5927
6624
|
const names = args.filter((a, i) => !a.startsWith("--") && i !== portFlag + 1);
|
|
5928
6625
|
const live = (await fetchServices(config)).filter((s) => s.protocol === "tcp");
|
|
5929
6626
|
const picked = names.length === 0 ? live : live.filter((s) => names.some((n) => s.label === n || s.id.startsWith(n)));
|
|
5930
6627
|
if (picked.length === 0) {
|
|
5931
|
-
return
|
|
6628
|
+
return fail5(
|
|
5932
6629
|
names.length === 0 ? "no live TCP services \u2014 mend service run starts one" : `no live TCP service matches "${names.join('", "')}"`
|
|
5933
6630
|
);
|
|
5934
6631
|
}
|
|
5935
6632
|
if (portOverride !== null && picked.length !== 1) {
|
|
5936
|
-
return
|
|
6633
|
+
return fail5("--port applies to exactly one service \u2014 name it");
|
|
5937
6634
|
}
|
|
5938
6635
|
await tunnelServices(config, picked, portOverride);
|
|
5939
6636
|
};
|
|
@@ -5962,23 +6659,23 @@ var serviceCommand = async (config, args) => {
|
|
|
5962
6659
|
}
|
|
5963
6660
|
};
|
|
5964
6661
|
var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
|
|
5965
|
-
var readIfExists = (file) =>
|
|
6662
|
+
var readIfExists = (file) => fs11.existsSync(file) ? fs11.readFileSync(file, "utf8") : null;
|
|
5966
6663
|
var localCredential = (provider) => {
|
|
5967
|
-
const home =
|
|
6664
|
+
const home = os7.homedir();
|
|
5968
6665
|
switch (provider) {
|
|
5969
6666
|
case "codex":
|
|
5970
6667
|
return readIfExists(
|
|
5971
|
-
|
|
6668
|
+
path12.join(process.env["CODEX_HOME"] ?? path12.join(home, ".codex"), "auth.json")
|
|
5972
6669
|
);
|
|
5973
6670
|
case "claude":
|
|
5974
6671
|
return readIfExists(
|
|
5975
|
-
|
|
5976
|
-
process.env["CLAUDE_CONFIG_DIR"] ??
|
|
6672
|
+
path12.join(
|
|
6673
|
+
process.env["CLAUDE_CONFIG_DIR"] ?? path12.join(home, ".claude"),
|
|
5977
6674
|
".credentials.json"
|
|
5978
6675
|
)
|
|
5979
6676
|
);
|
|
5980
6677
|
case "github": {
|
|
5981
|
-
const result =
|
|
6678
|
+
const result = spawnSync3("gh", ["auth", "token"], { encoding: "utf8" });
|
|
5982
6679
|
const token = result.status === 0 ? result.stdout.trim() : "";
|
|
5983
6680
|
return token === "" ? null : token;
|
|
5984
6681
|
}
|
|
@@ -6010,16 +6707,94 @@ var accountsCommand = async (config) => {
|
|
|
6010
6707
|
);
|
|
6011
6708
|
}
|
|
6012
6709
|
};
|
|
6710
|
+
var minuteOrUnknown = (at) => at === null ? "unknown" : at.toISOString().slice(0, 16);
|
|
6711
|
+
var staleGrantReason = (secret) => {
|
|
6712
|
+
const facts = claudeGrantFacts(secret);
|
|
6713
|
+
if (facts === null) return null;
|
|
6714
|
+
if (!facts.hasRefreshToken) return "signed out";
|
|
6715
|
+
return facts.refreshExpiresAt !== null && facts.refreshExpiresAt.getTime() <= Date.now() ? `expired ${facts.refreshExpiresAt.toISOString().slice(0, 10)}` : null;
|
|
6716
|
+
};
|
|
6717
|
+
var claudeGrant = async () => {
|
|
6718
|
+
const cli = claudeCli();
|
|
6719
|
+
const dir = claudeGrantDir(mendCliHome());
|
|
6720
|
+
const personal = personalClaudeDir();
|
|
6721
|
+
const personalBefore = grantStatus(cli, personal);
|
|
6722
|
+
let status = grantStatus(cli, dir);
|
|
6723
|
+
if (status === null) {
|
|
6724
|
+
fail5(
|
|
6725
|
+
"claude: could not run `claude auth status` \u2014 install Claude Code, set MEND_CLAUDE_BIN, or paste a credential: mend connect claude --from-stdin"
|
|
6726
|
+
);
|
|
6727
|
+
return null;
|
|
6728
|
+
}
|
|
6729
|
+
if (!status.loggedIn) {
|
|
6730
|
+
say5(` Mend needs its own Claude login, kept in ${dir}`);
|
|
6731
|
+
say5(dim5(" your own Claude login stays as it is"));
|
|
6732
|
+
if (!runClaudeLogin(cli, dir)) {
|
|
6733
|
+
fail5("claude: the login did not complete");
|
|
6734
|
+
return null;
|
|
6735
|
+
}
|
|
6736
|
+
status = grantStatus(cli, dir) ?? status;
|
|
6737
|
+
if (!status.loggedIn) {
|
|
6738
|
+
fail5("claude: the login completed but Claude still reports no grant in Mend's directory");
|
|
6739
|
+
return null;
|
|
6740
|
+
}
|
|
6741
|
+
}
|
|
6742
|
+
if (status.configDirectory !== null && path12.resolve(status.configDirectory) !== path12.resolve(dir)) {
|
|
6743
|
+
fail5(
|
|
6744
|
+
`claude: this Claude read ${status.configDirectory} instead of ${dir}, so a separate grant is not possible \u2014 connect the shared one deliberately with --use-my-login`
|
|
6745
|
+
);
|
|
6746
|
+
return null;
|
|
6747
|
+
}
|
|
6748
|
+
let read = readGrant(dir);
|
|
6749
|
+
if (read.kind !== "missing") {
|
|
6750
|
+
const why = staleGrantReason(read.secret);
|
|
6751
|
+
if (why !== null) {
|
|
6752
|
+
say5(` Mend's Claude grant is ${why}; logging in again`);
|
|
6753
|
+
if (!runClaudeLogin(cli, dir)) {
|
|
6754
|
+
fail5("claude: the login did not complete");
|
|
6755
|
+
return null;
|
|
6756
|
+
}
|
|
6757
|
+
read = readGrant(dir);
|
|
6758
|
+
const still = read.kind === "missing" ? "missing" : staleGrantReason(read.secret);
|
|
6759
|
+
if (still !== null) {
|
|
6760
|
+
fail5(`claude: the grant is still ${still} after logging in`);
|
|
6761
|
+
return null;
|
|
6762
|
+
}
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
if (read.kind === "missing") {
|
|
6766
|
+
const where = read.triedService === null ? read.triedPath : `${read.triedPath} or the Keychain item ${read.triedService}`;
|
|
6767
|
+
fail5(`claude: logged in, but no credential to read \u2014 looked in ${where}`);
|
|
6768
|
+
return null;
|
|
6769
|
+
}
|
|
6770
|
+
const mine = localCredential("claude");
|
|
6771
|
+
if (mine !== null && sameGrant(read.secret, mine)) {
|
|
6772
|
+
fail5(
|
|
6773
|
+
"claude: that is the same grant this machine's Claude holds, so both sides would race on refresh \u2014 remove Mend's directory and log in again, or accept the race with --use-my-login"
|
|
6774
|
+
);
|
|
6775
|
+
return null;
|
|
6776
|
+
}
|
|
6777
|
+
const personalAfter = grantStatus(cli, personal);
|
|
6778
|
+
if (personalBefore?.loggedIn === true && personalAfter?.loggedIn === false) {
|
|
6779
|
+
say5(
|
|
6780
|
+
` your own Claude login was signed out by this one \u2014 this account allows one grant at a time. Run \`claude auth login\` to get it back, and connect the shared grant with --use-my-login instead.`
|
|
6781
|
+
);
|
|
6782
|
+
} else if (personalAfter?.loggedIn === true) {
|
|
6783
|
+
say5(dim5(" your own Claude login still works \xB7 verified"));
|
|
6784
|
+
}
|
|
6785
|
+
say5(dim5(` grant ${read.kind === "file" ? read.path : `keychain ${read.service}`}`));
|
|
6786
|
+
return read.secret;
|
|
6787
|
+
};
|
|
6013
6788
|
var connectCommand = async (config, args) => {
|
|
6014
6789
|
const [providerArg, ...flags] = args;
|
|
6015
6790
|
if (!isProvider(providerArg)) {
|
|
6016
|
-
return
|
|
6791
|
+
return fail5(usageOf("connect"));
|
|
6017
6792
|
}
|
|
6018
6793
|
const provider = providerArg;
|
|
6019
6794
|
if (flags.includes("--remove")) {
|
|
6020
6795
|
const identity = await api(config, "GET", "/me/sealant");
|
|
6021
6796
|
const account2 = identity.accounts.find((row) => row.provider === provider);
|
|
6022
|
-
if (account2 === void 0) return
|
|
6797
|
+
if (account2 === void 0) return fail5(`${provider}: nothing connected`);
|
|
6023
6798
|
await api(config, "DELETE", `/me/sealant/accounts/${account2.id}`);
|
|
6024
6799
|
process.stdout.write(`${provider}: disconnected
|
|
6025
6800
|
`);
|
|
@@ -6027,35 +6802,60 @@ var connectCommand = async (config, args) => {
|
|
|
6027
6802
|
}
|
|
6028
6803
|
let secret;
|
|
6029
6804
|
if (flags.includes("--from-stdin")) {
|
|
6030
|
-
secret =
|
|
6031
|
-
if (secret === "") return
|
|
6805
|
+
secret = fs11.readFileSync(0, "utf8").trim();
|
|
6806
|
+
if (secret === "") return fail5("nothing on stdin");
|
|
6807
|
+
} else if (provider === "claude" && !flags.includes("--use-my-login")) {
|
|
6808
|
+
secret = await claudeGrant();
|
|
6809
|
+
if (secret === null) return;
|
|
6032
6810
|
} else {
|
|
6033
6811
|
secret = localCredential(provider);
|
|
6034
6812
|
if (secret === null) {
|
|
6035
|
-
const where = provider === "github" ? "`gh auth login` first, or pipe a token: gh auth token | mend connect github --from-stdin" : provider === "codex" ? "`codex login` first, or: mend connect codex --from-stdin < auth.json" : "`claude
|
|
6036
|
-
return
|
|
6813
|
+
const where = provider === "github" ? "`gh auth login` first, or pipe a token: gh auth token | mend connect github --from-stdin" : provider === "codex" ? "`codex login` first, or: mend connect codex --from-stdin < auth.json" : "`claude auth login` first, or: mend connect claude --from-stdin";
|
|
6814
|
+
return fail5(`${provider}: no credential on this machine \u2014 ${where}`);
|
|
6815
|
+
}
|
|
6816
|
+
if (provider === "claude") {
|
|
6817
|
+
say5(
|
|
6818
|
+
dim5(
|
|
6819
|
+
" --use-my-login: Mend and this machine will share one grant, and whichever refreshes second is signed out"
|
|
6820
|
+
)
|
|
6821
|
+
);
|
|
6037
6822
|
}
|
|
6038
6823
|
}
|
|
6824
|
+
const narrowed = narrowCredential(provider, secret);
|
|
6825
|
+
if (narrowed.kind === "narrowed" && narrowed.dropped.length > 0) {
|
|
6826
|
+
say5(dim5(` keeping ${narrowed.dropped.join(", ")} on this machine`));
|
|
6827
|
+
}
|
|
6039
6828
|
const account = await withSpinner(
|
|
6040
6829
|
`connecting ${provider}`,
|
|
6041
|
-
api(config, "POST", "/me/sealant/accounts", {
|
|
6830
|
+
api(config, "POST", "/me/sealant/accounts", {
|
|
6831
|
+
provider,
|
|
6832
|
+
secret: narrowed.secret
|
|
6833
|
+
})
|
|
6042
6834
|
);
|
|
6043
6835
|
process.stdout.write(`${accountLine(account)}
|
|
6044
6836
|
`);
|
|
6837
|
+
const facts = claudeGrantFacts(narrowed.secret);
|
|
6838
|
+
if (facts !== null) {
|
|
6839
|
+
say5(
|
|
6840
|
+
dim5(
|
|
6841
|
+
` access expires ${minuteOrUnknown(facts.accessExpiresAt)} \xB7 grant expires ${minuteOrUnknown(facts.refreshExpiresAt)}`
|
|
6842
|
+
)
|
|
6843
|
+
);
|
|
6844
|
+
}
|
|
6045
6845
|
};
|
|
6046
|
-
var
|
|
6846
|
+
var takeFlagValue4 = (args, flag) => {
|
|
6047
6847
|
const at = args.indexOf(flag);
|
|
6048
6848
|
return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
|
|
6049
6849
|
};
|
|
6050
6850
|
var saveCliConfig = (next) => {
|
|
6051
|
-
|
|
6052
|
-
|
|
6851
|
+
fs11.mkdirSync(path12.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
|
|
6852
|
+
fs11.writeFileSync(
|
|
6053
6853
|
CONFIG_PATH,
|
|
6054
6854
|
`${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
|
|
6055
6855
|
`,
|
|
6056
6856
|
{ mode: 384 }
|
|
6057
6857
|
);
|
|
6058
|
-
|
|
6858
|
+
fs11.chmodSync(CONFIG_PATH, 384);
|
|
6059
6859
|
};
|
|
6060
6860
|
var login = async (config, args) => {
|
|
6061
6861
|
await loginCommand(args, {
|
|
@@ -6063,25 +6863,25 @@ var login = async (config, args) => {
|
|
|
6063
6863
|
defaultUrl: config.url,
|
|
6064
6864
|
save: (next) => {
|
|
6065
6865
|
saveCliConfig({ url: next.url, token: next.token, deviceId: next.deviceId });
|
|
6066
|
-
|
|
6866
|
+
say5(dim5(` token saved to ${CONFIG_PATH} (0600)`));
|
|
6067
6867
|
}
|
|
6068
6868
|
});
|
|
6069
6869
|
};
|
|
6070
6870
|
var logout = async (config) => {
|
|
6071
|
-
if (!
|
|
6072
|
-
|
|
6871
|
+
if (!fs11.existsSync(CONFIG_PATH) && config.token === null) {
|
|
6872
|
+
say5(dim5("nothing saved \u2014 already signed out"));
|
|
6073
6873
|
return;
|
|
6074
6874
|
}
|
|
6075
6875
|
if (config.token !== null && config.deviceId !== null) {
|
|
6076
6876
|
try {
|
|
6077
6877
|
await request(config, "DELETE", `/me/devices/${config.deviceId}`);
|
|
6078
|
-
|
|
6878
|
+
say5(`${green5("\u2713")} device revoked on ${config.url}`);
|
|
6079
6879
|
} catch {
|
|
6080
|
-
|
|
6880
|
+
say5(dim5(" could not revoke on the server \u2014 end it under Settings \u2192 Devices"));
|
|
6081
6881
|
}
|
|
6082
6882
|
}
|
|
6083
6883
|
saveCliConfig({ ...config, token: null, deviceId: null });
|
|
6084
|
-
|
|
6884
|
+
say5(`${green5("\u2713")} signed out \xB7 ${dim5(`token removed from ${CONFIG_PATH}`)}`);
|
|
6085
6885
|
};
|
|
6086
6886
|
var UNINSTALL_CHOICES = [
|
|
6087
6887
|
{ scope: "all", text: "everything \xB7 the server on this machine and this machine's Mend files" },
|
|
@@ -6093,20 +6893,20 @@ var UNINSTALL_CHOICES = [
|
|
|
6093
6893
|
];
|
|
6094
6894
|
var askUninstallScope = async () => {
|
|
6095
6895
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
6096
|
-
return
|
|
6896
|
+
return fail5(`${UNINSTALL_USAGE} \xB7 no terminal to ask on`);
|
|
6097
6897
|
}
|
|
6098
|
-
|
|
6099
|
-
UNINSTALL_CHOICES.forEach((choice, index) =>
|
|
6898
|
+
say5("what should go?");
|
|
6899
|
+
UNINSTALL_CHOICES.forEach((choice, index) => say5(` ${index + 1}. ${choice.text}`));
|
|
6100
6900
|
const readline = await import("node:readline/promises");
|
|
6101
6901
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6102
6902
|
const answer = (await rl.question(" 1, 2 or 3: ")).trim();
|
|
6103
6903
|
rl.close();
|
|
6104
6904
|
const chosen = UNINSTALL_CHOICES[Number(answer) - 1];
|
|
6105
|
-
if (chosen === void 0) return
|
|
6905
|
+
if (chosen === void 0) return fail5(`"${answer}" is not one of the choices`);
|
|
6106
6906
|
return chosen.scope;
|
|
6107
6907
|
};
|
|
6108
6908
|
var confirmUninstall = async (word) => {
|
|
6109
|
-
if (process.stdin.isTTY !== true) return
|
|
6909
|
+
if (process.stdin.isTTY !== true) return fail5("non-interactive \u2014 pass --yes to remove");
|
|
6110
6910
|
const readline = await import("node:readline/promises");
|
|
6111
6911
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6112
6912
|
const answer = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
|
|
@@ -6115,14 +6915,14 @@ var confirmUninstall = async (word) => {
|
|
|
6115
6915
|
};
|
|
6116
6916
|
var uninstallCommand = async (config, args) => {
|
|
6117
6917
|
const parsed = parseUninstallArgs(args);
|
|
6118
|
-
if ("error" in parsed) return
|
|
6918
|
+
if ("error" in parsed) return fail5(parsed.error);
|
|
6119
6919
|
const scope = parsed.scope ?? await askUninstallScope();
|
|
6120
6920
|
const server = nodeServerRuntime();
|
|
6121
6921
|
const runtime = {
|
|
6122
6922
|
server,
|
|
6123
6923
|
cliHome: mendCliHome(),
|
|
6124
|
-
sshConfigFile:
|
|
6125
|
-
signedIn: config.token === null || !
|
|
6924
|
+
sshConfigFile: path12.join(os7.homedir(), ".ssh", "config"),
|
|
6925
|
+
signedIn: config.token === null || !fs11.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
|
|
6126
6926
|
revokeDevice: async () => {
|
|
6127
6927
|
if (config.deviceId === null) return "no device id saved";
|
|
6128
6928
|
try {
|
|
@@ -6134,42 +6934,42 @@ var uninstallCommand = async (config, args) => {
|
|
|
6134
6934
|
}
|
|
6135
6935
|
};
|
|
6136
6936
|
const plan = await describeUninstall(runtime, scope);
|
|
6137
|
-
|
|
6138
|
-
for (const line of planLines(plan, server.configDir))
|
|
6937
|
+
say5(dim5(`mend uninstall \xB7 ${scope === "all" ? "everything" : scope}`));
|
|
6938
|
+
for (const line of planLines(plan, server.configDir)) say5(` ${line}`);
|
|
6139
6939
|
const nothing = (plan.server === null || plan.server === "none") && (plan.home === null || plan.home.cliConfig === null && plan.home.sshDirectory === null && plan.home.managedSshBlocks === 0);
|
|
6140
6940
|
if (nothing) {
|
|
6141
|
-
|
|
6941
|
+
say5(dim5("nothing to remove"));
|
|
6142
6942
|
return;
|
|
6143
6943
|
}
|
|
6144
6944
|
if (planDeletesData(plan)) {
|
|
6145
|
-
|
|
6945
|
+
say5("repositories, worktrees, the database and its backups are deleted with the server");
|
|
6146
6946
|
}
|
|
6147
6947
|
if (!parsed.yes && !await confirmUninstall(planDeletesData(plan) ? "delete" : "y")) {
|
|
6148
|
-
|
|
6948
|
+
say5(dim5("nothing removed"));
|
|
6149
6949
|
return;
|
|
6150
6950
|
}
|
|
6151
6951
|
const outcome = await executeUninstall(runtime, plan);
|
|
6152
|
-
for (const line of outcome.leftovers)
|
|
6952
|
+
for (const line of outcome.leftovers) say5(dim5(` kept \xB7 ${line}`));
|
|
6153
6953
|
if (outcome.failures.length > 0) {
|
|
6154
|
-
return
|
|
6954
|
+
return fail5(outcome.failures.join("\n"));
|
|
6155
6955
|
}
|
|
6156
|
-
|
|
6157
|
-
`${
|
|
6956
|
+
say5(
|
|
6957
|
+
`${green5("\u2713")} ${scope === "all" ? "Mend is gone from this machine" : scope === "server" ? "the server is gone from this machine" : "this machine no longer holds Mend files"}`
|
|
6158
6958
|
);
|
|
6159
6959
|
};
|
|
6160
6960
|
var printGitKey = (key) => {
|
|
6161
6961
|
if (key.publicKey === null) return;
|
|
6162
|
-
|
|
6163
|
-
if (key.fingerprint !== null)
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6962
|
+
say5(key.publicKey);
|
|
6963
|
+
if (key.fingerprint !== null) say5(dim5(` ${key.fingerprint}`));
|
|
6964
|
+
say5(dim5(" add this to your git account's SSH keys (GitHub: settings \u2192 SSH keys) so"));
|
|
6965
|
+
say5(dim5(" every repository you can reach works; for one repository only, add it"));
|
|
6966
|
+
say5(dim5(" as that repository's deploy key instead (grant write if sessions push)"));
|
|
6167
6967
|
};
|
|
6168
6968
|
var keysShow = async (config) => {
|
|
6169
6969
|
const key = await api(config, "GET", "/keys/git");
|
|
6170
6970
|
if (!key.exists) {
|
|
6171
|
-
|
|
6172
|
-
`no Mend key yet ${
|
|
6971
|
+
say5(
|
|
6972
|
+
`no Mend key yet ${dim5("\u2014 mend keys init generates one (ed25519, stays on the server host)")}`
|
|
6173
6973
|
);
|
|
6174
6974
|
return;
|
|
6175
6975
|
}
|
|
@@ -6177,46 +6977,41 @@ var keysShow = async (config) => {
|
|
|
6177
6977
|
};
|
|
6178
6978
|
var keysInit = async (config) => {
|
|
6179
6979
|
const key = await api(config, "POST", "/keys/git");
|
|
6180
|
-
|
|
6980
|
+
say5(`${green5("\u2713")} Mend key ready ${dim5("(private half stays on the server host)")}`);
|
|
6181
6981
|
printGitKey(key);
|
|
6182
6982
|
};
|
|
6183
6983
|
var keysShare = async (config) => {
|
|
6184
6984
|
const agentSock = process.env["SSH_AUTH_SOCK"];
|
|
6185
6985
|
if (agentSock === void 0 || agentSock === "") {
|
|
6186
|
-
return
|
|
6986
|
+
return fail5("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
|
|
6187
6987
|
}
|
|
6188
|
-
const url = bridgeUrlOf(
|
|
6189
|
-
parseMendUrl(config.url).toString().replace(/\/$/, ""),
|
|
6190
|
-
config.token,
|
|
6191
|
-
os6.hostname()
|
|
6192
|
-
);
|
|
6193
6988
|
await shareAgent({
|
|
6194
|
-
url,
|
|
6989
|
+
url: () => socketUrl(config, "keys-bridge", { host: os7.hostname() }),
|
|
6195
6990
|
agentSock,
|
|
6196
6991
|
signal: new AbortController().signal,
|
|
6197
6992
|
onEvent: (event) => {
|
|
6198
6993
|
switch (event.kind) {
|
|
6199
6994
|
case "connected":
|
|
6200
|
-
|
|
6201
|
-
|
|
6995
|
+
say5(`${green5("\u25CF")} sharing this machine's ssh-agent with ${config.url}`);
|
|
6996
|
+
say5(dim5(` agent: ${agentSock} \xB7 signature requests print here \xB7 Ctrl-C stops sharing`));
|
|
6202
6997
|
return;
|
|
6203
6998
|
case "disconnected":
|
|
6204
|
-
|
|
6205
|
-
|
|
6999
|
+
say5(
|
|
7000
|
+
dim5(`not connected \u2014 retrying in ${Math.round(event.retryMs / 1e3)}s (Ctrl-C stops)`)
|
|
6206
7001
|
);
|
|
6207
7002
|
return;
|
|
6208
7003
|
case "sign-requested":
|
|
6209
|
-
|
|
6210
|
-
|
|
7004
|
+
say5(`${amber("\u270E")} signature requested by mend ${dim5(`(${event.context})`)}`);
|
|
7005
|
+
say5(dim5(" waiting \u2014 touch your key if it blinks (up to 60s)\u2026"));
|
|
6211
7006
|
return;
|
|
6212
7007
|
case "identities-requested":
|
|
6213
|
-
|
|
7008
|
+
say5(dim5(` identities requested (${event.context})`));
|
|
6214
7009
|
return;
|
|
6215
7010
|
case "signed":
|
|
6216
|
-
|
|
7011
|
+
say5(`${green5("\u2713")} signed ${dim5(`(${event.seconds.toFixed(1)}s)`)}`);
|
|
6217
7012
|
return;
|
|
6218
7013
|
case "not-signed":
|
|
6219
|
-
|
|
7014
|
+
say5(`${amber("\u2717")} not signed \u2014 ${event.message}`);
|
|
6220
7015
|
return;
|
|
6221
7016
|
}
|
|
6222
7017
|
}
|
|
@@ -6225,19 +7020,19 @@ var keysShare = async (config) => {
|
|
|
6225
7020
|
var keysMode = async (config, args) => {
|
|
6226
7021
|
const [value] = args;
|
|
6227
7022
|
if (value !== void 0 && value !== "mend-key" && value !== "bridge") {
|
|
6228
|
-
return
|
|
7023
|
+
return fail5(`keys mode takes mend-key or bridge, not "${value}"`);
|
|
6229
7024
|
}
|
|
6230
7025
|
const access = value === void 0 ? await api(config, "GET", "/me/git-access") : await api(config, "PUT", "/me/git-access", { mode: value });
|
|
6231
|
-
if (value !== void 0)
|
|
7026
|
+
if (value !== void 0) say5(`${green5("\u2713")} git access \xB7 ${value}`);
|
|
6232
7027
|
if (access.mode === "mend-key") {
|
|
6233
|
-
|
|
7028
|
+
say5(`mend-key ${dim5("\u2014 your Mend key on the server signs; works whenever the server is up")}`);
|
|
6234
7029
|
if (access.key.exists) printGitKey(access.key);
|
|
6235
|
-
else
|
|
7030
|
+
else say5(dim5(" no key yet \u2014 mend keys init creates it"));
|
|
6236
7031
|
return;
|
|
6237
7032
|
}
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
access.bridge.connected ? `${
|
|
7033
|
+
say5(`bridge ${dim5("\u2014 this machine's ssh-agent signs; only while a mend command runs here")}`);
|
|
7034
|
+
say5(
|
|
7035
|
+
access.bridge.connected ? `${green5("\u25CF")} signer connected \xB7 ${access.bridge.clientName ?? "unknown machine"}` : dim5(" no signer connected \u2014 mend shares the agent whenever it runs (or: mend keys share)")
|
|
6241
7036
|
);
|
|
6242
7037
|
};
|
|
6243
7038
|
var keysCommand = async (config, args) => {
|
|
@@ -6253,75 +7048,75 @@ var keysCommand = async (config, args) => {
|
|
|
6253
7048
|
case void 0:
|
|
6254
7049
|
return keysShow(config);
|
|
6255
7050
|
default:
|
|
6256
|
-
return
|
|
7051
|
+
return fail5(`unknown keys command "${verb}" \xB7 mend help keys lists them`);
|
|
6257
7052
|
}
|
|
6258
7053
|
};
|
|
6259
7054
|
var formatDotfileBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
6260
7055
|
var dotfilesShow = async (config) => {
|
|
6261
7056
|
const dotfiles = await api(config, "GET", "/dotfiles");
|
|
6262
7057
|
if (dotfiles.repository === null) {
|
|
6263
|
-
|
|
7058
|
+
say5(`repo ${dim5("none")}`);
|
|
6264
7059
|
} else {
|
|
6265
7060
|
const branch = dotfiles.repository.ref ?? "default branch";
|
|
6266
7061
|
const subdir = dotfiles.repository.subdirectory === null ? [] : [`${dotfiles.repository.subdirectory}/`];
|
|
6267
7062
|
const bootstrap = dotfiles.repository.bootstrap ? "install.sh on" : "install.sh off";
|
|
6268
|
-
|
|
6269
|
-
`repo ${dotfiles.repository.url} ${
|
|
7063
|
+
say5(
|
|
7064
|
+
`repo ${dotfiles.repository.url} ${dim5(`(${[branch, ...subdir, bootstrap].join(" \xB7 ")})`)}`
|
|
6270
7065
|
);
|
|
6271
7066
|
}
|
|
6272
7067
|
if (dotfiles.snapshot === null) {
|
|
6273
|
-
|
|
7068
|
+
say5(`snapshot ${dim5("none \u2014 sync from this machine: mend dotfiles sync --all")}`);
|
|
6274
7069
|
return;
|
|
6275
7070
|
}
|
|
6276
7071
|
const snapshot = dotfiles.snapshot;
|
|
6277
|
-
|
|
6278
|
-
`snapshot ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} \xB7 from ${snapshot.source} \xB7 ${
|
|
7072
|
+
say5(
|
|
7073
|
+
`snapshot ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} \xB7 from ${snapshot.source} \xB7 ${dim5(snapshot.sha.slice(0, 7))}`
|
|
6279
7074
|
);
|
|
6280
7075
|
for (const file of snapshot.files) {
|
|
6281
|
-
|
|
7076
|
+
say5(` ${file.path.padEnd(36)} ${dim5(formatDotfileBytes(file.bytes))}`);
|
|
6282
7077
|
}
|
|
6283
7078
|
};
|
|
6284
7079
|
var dotfilesSync = async (config, args) => {
|
|
6285
7080
|
const all = args.includes("--all");
|
|
6286
7081
|
const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
|
|
6287
|
-
const home =
|
|
7082
|
+
const home = os7.homedir();
|
|
6288
7083
|
if (!all && requestedPaths.length === 0) {
|
|
6289
7084
|
const found = scanDotfileCandidates(home);
|
|
6290
7085
|
if (found.length === 0) {
|
|
6291
|
-
|
|
7086
|
+
say5(dim5("no known config files found under ~"));
|
|
6292
7087
|
return;
|
|
6293
7088
|
}
|
|
6294
7089
|
let group = "";
|
|
6295
7090
|
for (const entry of found) {
|
|
6296
7091
|
if (entry.group !== group) {
|
|
6297
7092
|
group = entry.group;
|
|
6298
|
-
|
|
7093
|
+
say5(dim5(group));
|
|
6299
7094
|
}
|
|
6300
|
-
|
|
7095
|
+
say5(` ${entry.path.padEnd(36)} ${dim5(formatDotfileBytes(entry.bytes))}`);
|
|
6301
7096
|
}
|
|
6302
|
-
|
|
6303
|
-
|
|
7097
|
+
say5("");
|
|
7098
|
+
say5(
|
|
6304
7099
|
`sync everything with ${cobalt2("mend dotfiles sync --all")}, or pick: ${cobalt2("mend dotfiles sync .zshrc .gitconfig")}`
|
|
6305
7100
|
);
|
|
6306
7101
|
return;
|
|
6307
7102
|
}
|
|
6308
7103
|
const selected = all ? scanDotfileCandidates(home).map((entry) => entry.path) : requestedPaths;
|
|
6309
|
-
if (selected.length === 0) return
|
|
7104
|
+
if (selected.length === 0) return fail5("nothing to sync \u2014 no known config files found under ~");
|
|
6310
7105
|
const read = readSyncFiles(home, selected);
|
|
6311
|
-
if ("error" in read) return
|
|
7106
|
+
if ("error" in read) return fail5(read.error);
|
|
6312
7107
|
const result = await api(config, "POST", "/dotfiles/snapshot", {
|
|
6313
7108
|
files: read.files,
|
|
6314
|
-
source:
|
|
7109
|
+
source: os7.hostname(),
|
|
6315
7110
|
merge: false
|
|
6316
7111
|
});
|
|
6317
7112
|
const snapshot = result.snapshot;
|
|
6318
|
-
if (snapshot === null) return
|
|
6319
|
-
|
|
6320
|
-
`${
|
|
7113
|
+
if (snapshot === null) return fail5("the server accepted the sync but reports no snapshot");
|
|
7114
|
+
say5(
|
|
7115
|
+
`${green5("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os7.hostname()} \xB7 ${dim5(snapshot.sha.slice(0, 7))} ${dim5("\u2014 applies from the next session launch")}`
|
|
6321
7116
|
);
|
|
6322
7117
|
};
|
|
6323
7118
|
var envLoad = async (config, args) => {
|
|
6324
|
-
const explicitProject =
|
|
7119
|
+
const explicitProject = takeFlagValue4(args, "--project");
|
|
6325
7120
|
const secretFlag = args.indexOf("--secret");
|
|
6326
7121
|
const secretArg = secretFlag === -1 ? void 0 : args[secretFlag + 1];
|
|
6327
7122
|
const secretNames = secretArg !== void 0 && !secretArg.startsWith("--") && !secretArg.includes("/") && !secretArg.includes(".") ? secretArg.split(",").map((s) => s.trim()).filter((s) => s !== "") : [];
|
|
@@ -6331,15 +7126,15 @@ var envLoad = async (config, args) => {
|
|
|
6331
7126
|
const positional = args.filter(
|
|
6332
7127
|
(arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
|
|
6333
7128
|
);
|
|
6334
|
-
const file =
|
|
7129
|
+
const file = path12.resolve(positional[0] ?? ".env");
|
|
6335
7130
|
let contents;
|
|
6336
7131
|
try {
|
|
6337
|
-
contents =
|
|
7132
|
+
contents = fs11.readFileSync(file, "utf8");
|
|
6338
7133
|
} catch {
|
|
6339
|
-
return
|
|
7134
|
+
return fail5(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
|
|
6340
7135
|
}
|
|
6341
7136
|
if (contents.trim() === "") {
|
|
6342
|
-
|
|
7137
|
+
say5(dim5(`${file} is empty`));
|
|
6343
7138
|
return;
|
|
6344
7139
|
}
|
|
6345
7140
|
const project2 = await findProject(config, explicitProject);
|
|
@@ -6350,46 +7145,46 @@ var envLoad = async (config, args) => {
|
|
|
6350
7145
|
{ contents, allSecret, secretNames }
|
|
6351
7146
|
);
|
|
6352
7147
|
if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
|
|
6353
|
-
|
|
7148
|
+
say5(dim5(`${path12.basename(file)} has no variables`));
|
|
6354
7149
|
return;
|
|
6355
7150
|
}
|
|
6356
|
-
|
|
6357
|
-
for (const line of formatLoadReport(report, { dim:
|
|
7151
|
+
say5(`${green5("\u2713")} loaded ${path12.basename(file)} into ${project2.name}`);
|
|
7152
|
+
for (const line of formatLoadReport(report, { dim: dim5, warn: amber })) say5(line);
|
|
6358
7153
|
const plaintextUrls = report.loaded.filter(
|
|
6359
7154
|
(entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
|
|
6360
7155
|
);
|
|
6361
7156
|
if (plaintextUrls.length > 0) {
|
|
6362
|
-
|
|
7157
|
+
say5(
|
|
6363
7158
|
amber(
|
|
6364
7159
|
` ${plaintextUrls.map((entry) => entry.name).join(", ")} stored as plaintext configuration \u2014 if a value embeds a password, store it as a secret: mend env load --secret ${plaintextUrls.map((entry) => entry.name).join(",")}`
|
|
6365
7160
|
)
|
|
6366
7161
|
);
|
|
6367
7162
|
}
|
|
6368
7163
|
if (report.malformedLines.length > 0) {
|
|
6369
|
-
|
|
7164
|
+
say5(
|
|
6370
7165
|
amber(
|
|
6371
7166
|
` skipped ${report.malformedLines.length} malformed line${report.malformedLines.length === 1 ? "" : "s"}: ${report.malformedLines.join(", ")}`
|
|
6372
7167
|
)
|
|
6373
7168
|
);
|
|
6374
7169
|
}
|
|
6375
|
-
|
|
6376
|
-
|
|
7170
|
+
say5(
|
|
7171
|
+
dim5(
|
|
6377
7172
|
` configuration r${report.environmentRevision} \xB7 secrets r${report.secretRevision} \u2014 applies from the next workspace launch, including resume; running workspaces keep what they started with`
|
|
6378
7173
|
)
|
|
6379
7174
|
);
|
|
6380
7175
|
};
|
|
6381
7176
|
var envShow = async (config, args) => {
|
|
6382
|
-
const project2 = await findProject(config,
|
|
7177
|
+
const project2 = await findProject(config, takeFlagValue4(args, "--project"));
|
|
6383
7178
|
const [environment, secrets, cluster] = await Promise.all([
|
|
6384
7179
|
api(config, "GET", `/projects/${project2.id}/environment`),
|
|
6385
7180
|
api(config, "GET", `/projects/${project2.id}/secrets`),
|
|
6386
7181
|
api(config, "GET", `/projects/${project2.id}/cluster-bindings`)
|
|
6387
7182
|
]);
|
|
6388
|
-
|
|
6389
|
-
`${project2.name} ${
|
|
7183
|
+
say5(
|
|
7184
|
+
`${project2.name} ${dim5(`\xB7 configuration r${environment.revision} \xB7 secrets r${secrets.revision} \xB7 cluster r${cluster.revision}`)}`
|
|
6390
7185
|
);
|
|
6391
7186
|
if (environment.variables.length === 0 && secrets.secrets.length === 0 && cluster.bindings.length === 0 && cluster.serviceAccount === null) {
|
|
6392
|
-
|
|
7187
|
+
say5(dim5(` nothing stored \u2014 load a file: ${cobalt2("mend env load")}`));
|
|
6393
7188
|
return;
|
|
6394
7189
|
}
|
|
6395
7190
|
const bindingNames = cluster.bindings.map((b) => `${b.kind}/${b.objectName}`);
|
|
@@ -6400,23 +7195,23 @@ var envShow = async (config, args) => {
|
|
|
6400
7195
|
...bindingNames.map((name) => name.length)
|
|
6401
7196
|
);
|
|
6402
7197
|
for (const variable of environment.variables) {
|
|
6403
|
-
|
|
7198
|
+
say5(` ${variable.name.padEnd(width)} configuration ${dim5("\xB7 plaintext")}`);
|
|
6404
7199
|
}
|
|
6405
7200
|
for (const secret of secrets.secrets) {
|
|
6406
|
-
|
|
7201
|
+
say5(` ${secret.name.padEnd(width)} secret ${dim5("\xB7 value set, never shown")}`);
|
|
6407
7202
|
}
|
|
6408
7203
|
for (const name of bindingNames) {
|
|
6409
|
-
|
|
6410
|
-
` ${name.padEnd(width)} cluster binding ${
|
|
7204
|
+
say5(
|
|
7205
|
+
` ${name.padEnd(width)} cluster binding ${dim5("\xB7 resolved by the platform at launch \xB7 contents unknown to Mend")}`
|
|
6411
7206
|
);
|
|
6412
7207
|
}
|
|
6413
7208
|
if (cluster.serviceAccount !== null) {
|
|
6414
|
-
|
|
6415
|
-
` ${cluster.serviceAccount.padEnd(width)} service account ${
|
|
7209
|
+
say5(
|
|
7210
|
+
` ${cluster.serviceAccount.padEnd(width)} service account ${dim5("\xB7 workspace pod identity \xB7 allowlisted by the operator")}`
|
|
6416
7211
|
);
|
|
6417
7212
|
}
|
|
6418
7213
|
if (!cluster.clusterCapable && (cluster.bindings.length > 0 || cluster.serviceAccount !== null)) {
|
|
6419
|
-
|
|
7214
|
+
say5(
|
|
6420
7215
|
amber(
|
|
6421
7216
|
` ${cluster.bindings.length} cluster binding${cluster.bindings.length === 1 ? "" : "s"}${cluster.serviceAccount === null ? "" : " \xB7 service account set"} \xB7 local runner \u2014 cluster bindings do not resolve here`
|
|
6422
7217
|
)
|
|
@@ -6424,54 +7219,54 @@ var envShow = async (config, args) => {
|
|
|
6424
7219
|
}
|
|
6425
7220
|
};
|
|
6426
7221
|
var envCluster = async (config, args) => {
|
|
6427
|
-
const explicitProject =
|
|
7222
|
+
const explicitProject = takeFlagValue4(args, "--project");
|
|
6428
7223
|
const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
|
|
6429
7224
|
const [verb, ...rest] = positional;
|
|
6430
7225
|
const usage = usageOf("env cluster");
|
|
6431
7226
|
const project2 = await findProject(config, explicitProject);
|
|
6432
7227
|
const route = `/projects/${project2.id}/cluster-bindings`;
|
|
6433
|
-
const appliesLine = () =>
|
|
6434
|
-
|
|
7228
|
+
const appliesLine = () => say5(
|
|
7229
|
+
dim5(
|
|
6435
7230
|
" applies from the next workspace launch; running workspaces keep what they started with"
|
|
6436
7231
|
)
|
|
6437
7232
|
);
|
|
6438
7233
|
if (verb === "add") {
|
|
6439
7234
|
const [kind, objectName] = rest;
|
|
6440
7235
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
6441
|
-
return
|
|
7236
|
+
return fail5(`env cluster add takes a kind and an object name
|
|
6442
7237
|
${usage}`);
|
|
6443
7238
|
}
|
|
6444
7239
|
const result = await api(config, "POST", route, {
|
|
6445
7240
|
kind,
|
|
6446
7241
|
objectName
|
|
6447
7242
|
});
|
|
6448
|
-
|
|
6449
|
-
|
|
7243
|
+
say5(`${green5("\u2713")} bound ${kind}/${objectName} ${dim5(`\xB7 cluster r${result.revision}`)}`);
|
|
7244
|
+
say5(dim5(" resolved by the platform at launch \xB7 contents unknown to Mend"));
|
|
6450
7245
|
return appliesLine();
|
|
6451
7246
|
}
|
|
6452
7247
|
if (verb === "remove") {
|
|
6453
7248
|
const [ref] = rest;
|
|
6454
7249
|
const [kind, objectName] = ref?.split("/", 2) ?? [];
|
|
6455
7250
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
6456
|
-
return
|
|
7251
|
+
return fail5(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
|
|
6457
7252
|
${usage}`);
|
|
6458
7253
|
}
|
|
6459
7254
|
const snapshot = await api(config, "GET", route);
|
|
6460
7255
|
const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
|
|
6461
|
-
if (binding === void 0) return
|
|
7256
|
+
if (binding === void 0) return fail5(`${kind}/${objectName} is not bound on ${project2.name}`);
|
|
6462
7257
|
const result = await api(
|
|
6463
7258
|
config,
|
|
6464
7259
|
"DELETE",
|
|
6465
7260
|
`${route}/${binding.id}`
|
|
6466
7261
|
);
|
|
6467
|
-
|
|
7262
|
+
say5(`${green5("\u2713")} removed ${kind}/${objectName} ${dim5(`\xB7 cluster r${result.revision}`)}`);
|
|
6468
7263
|
return appliesLine();
|
|
6469
7264
|
}
|
|
6470
7265
|
if (verb === "sa") {
|
|
6471
7266
|
const clear = args.includes("--clear");
|
|
6472
7267
|
const [name] = rest;
|
|
6473
7268
|
if (!clear && name === void 0)
|
|
6474
|
-
return
|
|
7269
|
+
return fail5(`env cluster sa takes a name or --clear
|
|
6475
7270
|
${usage}`);
|
|
6476
7271
|
const result = await api(
|
|
6477
7272
|
config,
|
|
@@ -6479,11 +7274,11 @@ ${usage}`);
|
|
|
6479
7274
|
`${route}/service-account`,
|
|
6480
7275
|
{ serviceAccount: clear ? null : name }
|
|
6481
7276
|
);
|
|
6482
|
-
|
|
6483
|
-
result.serviceAccount === null ? `${
|
|
7277
|
+
say5(
|
|
7278
|
+
result.serviceAccount === null ? `${green5("\u2713")} cleared the workspace service account ${dim5(`\xB7 cluster r${result.revision}`)}` : `${green5("\u2713")} service account ${result.serviceAccount} ${dim5(`\xB7 cluster r${result.revision}`)}`
|
|
6484
7279
|
);
|
|
6485
7280
|
if (result.serviceAccount !== null) {
|
|
6486
|
-
|
|
7281
|
+
say5(
|
|
6487
7282
|
amber(
|
|
6488
7283
|
" the session agent holds this role's full permissions for the whole session \u2014 bind a least-privilege role intended for untrusted code; names outside the platform allowlist fail the launch"
|
|
6489
7284
|
)
|
|
@@ -6491,7 +7286,7 @@ ${usage}`);
|
|
|
6491
7286
|
}
|
|
6492
7287
|
return appliesLine();
|
|
6493
7288
|
}
|
|
6494
|
-
return
|
|
7289
|
+
return fail5(`unknown env cluster command "${verb ?? ""}"
|
|
6495
7290
|
${usage}`);
|
|
6496
7291
|
};
|
|
6497
7292
|
var envCommand = async (config, args) => {
|
|
@@ -6505,7 +7300,7 @@ var envCommand = async (config, args) => {
|
|
|
6505
7300
|
case void 0:
|
|
6506
7301
|
return envShow(config, rest);
|
|
6507
7302
|
default:
|
|
6508
|
-
return
|
|
7303
|
+
return fail5(`unknown env command "${verb}" \xB7 mend help env lists them`);
|
|
6509
7304
|
}
|
|
6510
7305
|
};
|
|
6511
7306
|
var dotfilesCommand = async (config, args) => {
|
|
@@ -6517,12 +7312,12 @@ var dotfilesCommand = async (config, args) => {
|
|
|
6517
7312
|
case void 0:
|
|
6518
7313
|
return dotfilesShow(config);
|
|
6519
7314
|
default:
|
|
6520
|
-
return
|
|
7315
|
+
return fail5(`unknown dotfiles command "${verb}" \xB7 mend help dotfiles lists them`);
|
|
6521
7316
|
}
|
|
6522
7317
|
};
|
|
6523
7318
|
var skillsList = async (config, args) => {
|
|
6524
7319
|
const forProject = args.includes("--project");
|
|
6525
|
-
const project2 = forProject ? await findProject(config,
|
|
7320
|
+
const project2 = forProject ? await findProject(config, takeFlagValue4(args, "--project")) : null;
|
|
6526
7321
|
const skills = await api(
|
|
6527
7322
|
config,
|
|
6528
7323
|
"GET",
|
|
@@ -6530,25 +7325,25 @@ var skillsList = async (config, args) => {
|
|
|
6530
7325
|
);
|
|
6531
7326
|
const library = project2 === null ? "your library" : project2.name;
|
|
6532
7327
|
if (skills.length === 0) {
|
|
6533
|
-
|
|
7328
|
+
say5(dim5(`no skills in ${library} \u2014 push a local one: mend skills push`));
|
|
6534
7329
|
return;
|
|
6535
7330
|
}
|
|
6536
7331
|
for (const skill of skills) {
|
|
6537
|
-
|
|
6538
|
-
` ${skill.name.padEnd(28)} ${
|
|
7332
|
+
say5(
|
|
7333
|
+
` ${skill.name.padEnd(28)} ${dim5(`${skill.fileCount} file${skill.fileCount === 1 ? "" : "s"} \xB7 ${formatDotfileBytes(skill.bytes)}`)}`
|
|
6539
7334
|
);
|
|
6540
|
-
if (skill.description !== "")
|
|
7335
|
+
if (skill.description !== "") say5(` ${dim5(skill.description)}`);
|
|
6541
7336
|
}
|
|
6542
7337
|
};
|
|
6543
7338
|
var skillsPush = async (config, args) => {
|
|
6544
|
-
const dirFlag =
|
|
6545
|
-
const root = dirFlag === null ?
|
|
7339
|
+
const dirFlag = takeFlagValue4(args, "--dir");
|
|
7340
|
+
const root = dirFlag === null ? path12.join(os7.homedir(), DEFAULT_SKILLS_DIR) : path12.resolve(dirFlag);
|
|
6546
7341
|
const scanned = scanSkillLibrary(root);
|
|
6547
|
-
if ("error" in scanned) return
|
|
6548
|
-
for (const note of scanned.notes)
|
|
6549
|
-
if (scanned.skills.length === 0) return
|
|
7342
|
+
if ("error" in scanned) return fail5(scanned.error);
|
|
7343
|
+
for (const note of scanned.notes) say5(dim5(` ${note.skill}: ${note.message}`));
|
|
7344
|
+
if (scanned.skills.length === 0) return fail5(`no skills found under ${root}`);
|
|
6550
7345
|
const forProject = args.includes("--project");
|
|
6551
|
-
const project2 = forProject ? await findProject(config,
|
|
7346
|
+
const project2 = forProject ? await findProject(config, takeFlagValue4(args, "--project")) : null;
|
|
6552
7347
|
const report = await api(config, "POST", "/skills/sync", {
|
|
6553
7348
|
scope: project2 === null ? "user" : "project",
|
|
6554
7349
|
projectId: project2?.id ?? null,
|
|
@@ -6566,10 +7361,10 @@ var skillsPush = async (config, args) => {
|
|
|
6566
7361
|
`${report.unchanged.length} unchanged`,
|
|
6567
7362
|
...report.removed.length > 0 ? [`${report.removed.length} removed`] : []
|
|
6568
7363
|
].join(" \xB7 ");
|
|
6569
|
-
|
|
6570
|
-
`${
|
|
7364
|
+
say5(
|
|
7365
|
+
`${green5("pushed")} ${scanned.skills.length} skill${scanned.skills.length === 1 ? "" : "s"} from ${root} to ${library} \xB7 ${dim5(counts)}`
|
|
6571
7366
|
);
|
|
6572
|
-
|
|
7367
|
+
say5(dim5("sessions receive them from the next launch"));
|
|
6573
7368
|
};
|
|
6574
7369
|
var skillsCommand = async (config, args) => {
|
|
6575
7370
|
const [verb, ...rest] = args;
|
|
@@ -6582,7 +7377,7 @@ var skillsCommand = async (config, args) => {
|
|
|
6582
7377
|
return skillsList(config, args);
|
|
6583
7378
|
default:
|
|
6584
7379
|
if (verb.startsWith("--")) return skillsList(config, args);
|
|
6585
|
-
return
|
|
7380
|
+
return fail5(`unknown skills command "${verb}" \xB7 mend help skills lists them`);
|
|
6586
7381
|
}
|
|
6587
7382
|
};
|
|
6588
7383
|
var completeCommand = async (config, args) => {
|
|
@@ -6686,7 +7481,7 @@ var completionsCommand = (args) => {
|
|
|
6686
7481
|
process.stdout.write(BASH_COMPLETIONS);
|
|
6687
7482
|
return;
|
|
6688
7483
|
default:
|
|
6689
|
-
return
|
|
7484
|
+
return fail5(`${usageOf("completions")} \xB7 e.g. mend completions zsh > "$fpath[1]/_mend"`);
|
|
6690
7485
|
}
|
|
6691
7486
|
};
|
|
6692
7487
|
var supervisedRun = async (config, session, argv) => {
|
|
@@ -6694,15 +7489,15 @@ var supervisedRun = async (config, session, argv) => {
|
|
|
6694
7489
|
"provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
|
|
6695
7490
|
api(config, "POST", `/sessions/${session.id}/launch`, { argv })
|
|
6696
7491
|
);
|
|
6697
|
-
|
|
6698
|
-
`${
|
|
7492
|
+
say5(
|
|
7493
|
+
`${green5("\u2713 recording")} \xB7 run ${dim5(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
|
|
6699
7494
|
);
|
|
6700
|
-
|
|
6701
|
-
|
|
7495
|
+
say5(dim5(` ${argv.join(" ")} \u2014 live record:`));
|
|
7496
|
+
say5("");
|
|
6702
7497
|
const headers = {};
|
|
6703
7498
|
if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
|
|
6704
7499
|
const response = await fetch(`${config.url}/api/events`, { headers });
|
|
6705
|
-
if (response.body === null) return
|
|
7500
|
+
if (response.body === null) return fail5("event stream unavailable");
|
|
6706
7501
|
const reader = response.body.getReader();
|
|
6707
7502
|
const decoder = new TextDecoder();
|
|
6708
7503
|
let buffer = "";
|
|
@@ -6724,7 +7519,7 @@ var supervisedRun = async (config, session, argv) => {
|
|
|
6724
7519
|
}
|
|
6725
7520
|
if (event.sessionId !== session.id) continue;
|
|
6726
7521
|
if (event.type === "session-progress" && event.line !== void 0) {
|
|
6727
|
-
|
|
7522
|
+
say5(dim5(` ${event.line}`));
|
|
6728
7523
|
}
|
|
6729
7524
|
if (event.type === "session") {
|
|
6730
7525
|
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
@@ -6734,9 +7529,9 @@ var supervisedRun = async (config, session, argv) => {
|
|
|
6734
7529
|
}
|
|
6735
7530
|
}
|
|
6736
7531
|
await reader.cancel().catch(() => void 0);
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
7532
|
+
say5("");
|
|
7533
|
+
say5(`${green5("\u2713")} session ${settled ?? "settled"} \xB7 recorded \xB7 checkpoint taken`);
|
|
7534
|
+
say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
6740
7535
|
process.exit(settled === "failed" ? 1 : 0);
|
|
6741
7536
|
};
|
|
6742
7537
|
var continueSession = async (config, args) => {
|
|
@@ -6759,20 +7554,20 @@ var continueSession = async (config, args) => {
|
|
|
6759
7554
|
}
|
|
6760
7555
|
}
|
|
6761
7556
|
if (sessionId === null || followUp === null) {
|
|
6762
|
-
return
|
|
7557
|
+
return fail5("no session with a pending follow-up \u2014 send one from the review first");
|
|
6763
7558
|
}
|
|
6764
7559
|
} else {
|
|
6765
7560
|
followUp = await api(config, "GET", `/sessions/${sessionId}/follow-up`);
|
|
6766
|
-
if (followUp === null) return
|
|
7561
|
+
if (followUp === null) return fail5(`session ${sessionId} has no pending follow-up`);
|
|
6767
7562
|
}
|
|
6768
7563
|
const detail = await api(config, "GET", `/sessions/${sessionId}`);
|
|
6769
7564
|
const session = detail.session;
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
for (const line of followUp.instruction.split("\n").slice(0, 6))
|
|
6773
|
-
if (followUp.instruction.split("\n").length > 6)
|
|
7565
|
+
say5(`${green5("\u2713")} follow-up for session ${dim5(session.id.slice(0, 8))} \xB7 ${session.branch}`);
|
|
7566
|
+
say5(dim5(" instruction:"));
|
|
7567
|
+
for (const line of followUp.instruction.split("\n").slice(0, 6)) say5(dim5(` \u2502 ${line}`));
|
|
7568
|
+
if (followUp.instruction.split("\n").length > 6) say5(dim5(" \u2502 \u2026"));
|
|
6774
7569
|
if (followUp.reviewSliceId === null || followUp.checkpointAId === null || followUp.checkpointBId === null || followUp.diffDigest === null || followUp.idempotencyKey === null) {
|
|
6775
|
-
return
|
|
7570
|
+
return fail5("legacy follow-up \u2014 recreate it from a pinned Review before delivery");
|
|
6776
7571
|
}
|
|
6777
7572
|
const delivered = await withSpinner(
|
|
6778
7573
|
"delivering persisted Review bundle \u2014 retries reconcile one process\u2026",
|
|
@@ -6787,16 +7582,16 @@ var continueSession = async (config, args) => {
|
|
|
6787
7582
|
})
|
|
6788
7583
|
);
|
|
6789
7584
|
if (delivered.status === "pending") {
|
|
6790
|
-
return
|
|
7585
|
+
return fail5("the session is active \u2014 the follow-up remains pending");
|
|
6791
7586
|
}
|
|
6792
7587
|
if (delivered.status === "delivery_failed") {
|
|
6793
|
-
return
|
|
7588
|
+
return fail5(delivered.deliveryError ?? "delivery failed before process membership finalized");
|
|
6794
7589
|
}
|
|
6795
|
-
|
|
6796
|
-
`${
|
|
7590
|
+
say5(
|
|
7591
|
+
`${green5("\u2713 recording")} \xB7 delivered to run ${delivered.deliverySealantRunId ?? "unknown"}${detachHint()}`
|
|
6797
7592
|
);
|
|
6798
|
-
|
|
6799
|
-
|
|
7593
|
+
say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
7594
|
+
say5("");
|
|
6800
7595
|
await attachOrExit(config, session.id, session.harness);
|
|
6801
7596
|
exitAfterSessionEnd(config, session.id);
|
|
6802
7597
|
};
|
|
@@ -6813,27 +7608,27 @@ var resumeCommand = async (config, args) => {
|
|
|
6813
7608
|
const detail = await api(config, "GET", `/projects/${project2.id}`);
|
|
6814
7609
|
const match = prefix === void 0 ? detail.sessions.find((s) => !agentLiveIn(detail, s)) : detail.sessions.find((s) => s.id.startsWith(prefix));
|
|
6815
7610
|
if (match === void 0) {
|
|
6816
|
-
return
|
|
7611
|
+
return fail5(
|
|
6817
7612
|
prefix === void 0 ? "no settled session to resume \u2014 mend status lists sessions" : `no session matches "${prefix}"`
|
|
6818
7613
|
);
|
|
6819
7614
|
}
|
|
6820
7615
|
if (agentLiveIn(detail, match)) {
|
|
6821
|
-
return
|
|
7616
|
+
return fail5(
|
|
6822
7617
|
`session ${match.id.slice(0, 8)} is live \u2014 attach: mend attach ${match.id.slice(0, 8)}`
|
|
6823
7618
|
);
|
|
6824
7619
|
}
|
|
6825
|
-
|
|
6826
|
-
`${
|
|
7620
|
+
say5(
|
|
7621
|
+
`${green5("\u2713")} resuming ${match.harness} \xB7 ${dim5(match.id.slice(0, 8))}${withHarness === null ? "" : ` ${dim5("as")} ${withHarness}`}`
|
|
6827
7622
|
);
|
|
6828
|
-
|
|
7623
|
+
say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
|
|
6829
7624
|
const priorAgent = detail.annotations.find((annotation) => annotation.sessionId === match.id)?.currentAgent ?? null;
|
|
6830
7625
|
const protocolPrior = priorAgent?.kind === "agent-protocol" && withHarness === null;
|
|
6831
7626
|
await withSpinner(
|
|
6832
7627
|
protocolPrior ? "reopening as a terminal \u2014 same conversation\u2026" : "resuming \u2014 a fresh workspace restores the saved session state\u2026",
|
|
6833
7628
|
protocolPrior ? api(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : api(config, "POST", `/sessions/${match.id}/resume`, { harness: withHarness })
|
|
6834
7629
|
);
|
|
6835
|
-
|
|
6836
|
-
|
|
7630
|
+
say5(`${green5("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
|
|
7631
|
+
say5("");
|
|
6837
7632
|
await attachOrExit(config, match.id, withHarness ?? match.harness);
|
|
6838
7633
|
exitAfterSessionEnd(config, match.id);
|
|
6839
7634
|
};
|
|
@@ -6847,7 +7642,7 @@ var resumeForRejoin = async (config, sessionId, label) => {
|
|
|
6847
7642
|
} catch (error) {
|
|
6848
7643
|
const refreshed = await api(config, "GET", `/sessions/${sessionId}`);
|
|
6849
7644
|
if (agentIsLive(refreshed.session, refreshed.currentAgent)) return false;
|
|
6850
|
-
return
|
|
7645
|
+
return fail5(error instanceof Error ? error.message : String(error));
|
|
6851
7646
|
}
|
|
6852
7647
|
};
|
|
6853
7648
|
var rejoinCommand = async (config, args) => {
|
|
@@ -6861,20 +7656,20 @@ var rejoinCommand = async (config, args) => {
|
|
|
6861
7656
|
const matches = prefix === void 0 ? newest === void 0 ? [] : [newest] : eligible.filter((session2) => session2.id.startsWith(prefix));
|
|
6862
7657
|
if (matches.length === 0) {
|
|
6863
7658
|
const harnessDescription = harness === null ? "" : ` ${harness}`;
|
|
6864
|
-
return
|
|
7659
|
+
return fail5(
|
|
6865
7660
|
prefix === void 0 ? `no${harnessDescription} session to rejoin in project ${project2.name}` : `no${harnessDescription} session matches "${prefix}" in project ${project2.name}`
|
|
6866
7661
|
);
|
|
6867
7662
|
}
|
|
6868
7663
|
if (matches.length > 1) {
|
|
6869
|
-
return
|
|
7664
|
+
return fail5(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
|
|
6870
7665
|
}
|
|
6871
7666
|
const session = matches[0];
|
|
6872
|
-
if (session === void 0) return
|
|
7667
|
+
if (session === void 0) return fail5("session selection failed");
|
|
6873
7668
|
const alreadyLive = agentLiveIn(detail, session);
|
|
6874
|
-
|
|
6875
|
-
`${
|
|
7669
|
+
say5(
|
|
7670
|
+
`${green5("\u2713")} rejoining ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim5(session.id.slice(0, 8))} \xB7 ${alreadyLive ? "already live" : "restoring"}`
|
|
6876
7671
|
);
|
|
6877
|
-
|
|
7672
|
+
say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
6878
7673
|
let restored = false;
|
|
6879
7674
|
const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ?? null;
|
|
6880
7675
|
if (currentAgent?.kind === "agent-protocol") {
|
|
@@ -6890,10 +7685,10 @@ var rejoinCommand = async (config, args) => {
|
|
|
6890
7685
|
"resuming \u2014 a fresh workspace restores the saved session state\u2026"
|
|
6891
7686
|
);
|
|
6892
7687
|
}
|
|
6893
|
-
|
|
6894
|
-
restored ? `${
|
|
7688
|
+
say5(
|
|
7689
|
+
restored ? `${green5("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}` : `${green5("\u2713 recording")} \xB7 attached to the live session${detachHint()}`
|
|
6895
7690
|
);
|
|
6896
|
-
|
|
7691
|
+
say5("");
|
|
6897
7692
|
let outcome = await attachTty(config, session.id, session.harness, 0n, void 0, {
|
|
6898
7693
|
handleSignals: true
|
|
6899
7694
|
});
|
|
@@ -6919,7 +7714,7 @@ var projectsCommand = async (config) => {
|
|
|
6919
7714
|
api(config, "GET", "/sessions")
|
|
6920
7715
|
]);
|
|
6921
7716
|
if (projects.length === 0) {
|
|
6922
|
-
|
|
7717
|
+
say5(dim5("no adopted projects \u2014 mend adopt brings one in"));
|
|
6923
7718
|
return;
|
|
6924
7719
|
}
|
|
6925
7720
|
const liveByProject = /* @__PURE__ */ new Map();
|
|
@@ -6931,30 +7726,30 @@ var projectsCommand = async (config) => {
|
|
|
6931
7726
|
const here = matchProjectByCwd(projects, cwdFacts(process.cwd()));
|
|
6932
7727
|
for (const project2 of projects) {
|
|
6933
7728
|
const live = liveByProject.get(project2.id) ?? 0;
|
|
6934
|
-
const liveLabel = live > 0 ?
|
|
7729
|
+
const liveLabel = live > 0 ? green5(`${live} live`) : dim5("\u2014");
|
|
6935
7730
|
const marker = project2.id === here?.id ? cobalt2("\u25B8 ") : " ";
|
|
6936
|
-
|
|
6937
|
-
`${marker}${project2.name.padEnd(nameWidth)} ${
|
|
7731
|
+
say5(
|
|
7732
|
+
`${marker}${project2.name.padEnd(nameWidth)} ${dim5(project2.defaultBranch.padEnd(branchWidth))} ${liveLabel} ${dim5(project2.storePath)}`
|
|
6938
7733
|
);
|
|
6939
7734
|
}
|
|
6940
|
-
if (here !== void 0)
|
|
7735
|
+
if (here !== void 0) say5(dim5(` \u25B8 ${here.name} is the cwd's project`));
|
|
6941
7736
|
};
|
|
6942
7737
|
var refreshCommand = async (config, args) => {
|
|
6943
|
-
const explicit = args.find((a) => !a.startsWith("--")) ??
|
|
7738
|
+
const explicit = args.find((a) => !a.startsWith("--")) ?? takeFlagValue4(args, "--project");
|
|
6944
7739
|
const project2 = await findProject(config, explicit ?? null);
|
|
6945
7740
|
const branches = await api(
|
|
6946
7741
|
config,
|
|
6947
7742
|
"POST",
|
|
6948
7743
|
`/projects/${project2.id}/refresh`
|
|
6949
7744
|
);
|
|
6950
|
-
|
|
7745
|
+
say5(`${green5("\u2713")} refreshed ${project2.name} ${dim5(`\xB7 ${branches.length} branches`)}`);
|
|
6951
7746
|
for (const branch of branches.slice(0, 12)) {
|
|
6952
7747
|
const marker = branch.isDefault ? cobalt2("\u25B8 ") : " ";
|
|
6953
|
-
|
|
6954
|
-
`${marker}${branch.name} ${
|
|
7748
|
+
say5(
|
|
7749
|
+
`${marker}${branch.name} ${dim5(branch.sha.slice(0, 12))} ${dim5(branch.committedAt.slice(0, 10))}`
|
|
6955
7750
|
);
|
|
6956
7751
|
}
|
|
6957
|
-
if (branches.length > 12)
|
|
7752
|
+
if (branches.length > 12) say5(dim5(` \u2026 ${branches.length - 12} more`));
|
|
6958
7753
|
};
|
|
6959
7754
|
var printSessionRow = (row) => {
|
|
6960
7755
|
const { session, annotation } = row;
|
|
@@ -6968,8 +7763,8 @@ var printSessionRow = (row) => {
|
|
|
6968
7763
|
if (annotation !== void 0 && annotation.pendingFollowUp)
|
|
6969
7764
|
facts.push(amber("follow-up pending"));
|
|
6970
7765
|
const base = session.baseRef === null ? session.baseSha.slice(0, 12) : session.baseRef;
|
|
6971
|
-
|
|
6972
|
-
`${session.harness.padEnd(8)} ${
|
|
7766
|
+
say5(
|
|
7767
|
+
`${session.harness.padEnd(8)} ${dim5(session.id.slice(0, 8))} ${live ? green5(status) : dim5(status)} ${row.projectName} ${dim5(`${session.branch} \xB7 base ${base}`)}${facts.length > 0 ? ` ${facts.join(dim5(" \xB7 "))}` : ""}`
|
|
6973
7768
|
);
|
|
6974
7769
|
};
|
|
6975
7770
|
var sessionsCommand = async (config, args) => {
|
|
@@ -6977,7 +7772,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
6977
7772
|
if (args.includes("--json=v2")) {
|
|
6978
7773
|
const projectFlag2 = args.indexOf("--project");
|
|
6979
7774
|
const projectName2 = projectFlag2 !== -1 && args[projectFlag2 + 1] !== void 0 ? String(args[projectFlag2 + 1]) : null;
|
|
6980
|
-
|
|
7775
|
+
say5(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
|
|
6981
7776
|
return;
|
|
6982
7777
|
}
|
|
6983
7778
|
const json = args.includes("--json");
|
|
@@ -6986,7 +7781,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
6986
7781
|
const projects = await api(config, "GET", "/projects");
|
|
6987
7782
|
const scope = projectName === null ? projects : projects.filter((p) => p.name === projectName);
|
|
6988
7783
|
if (projectName !== null && scope.length === 0) {
|
|
6989
|
-
return
|
|
7784
|
+
return fail5(`no adopted project named "${projectName}"`);
|
|
6990
7785
|
}
|
|
6991
7786
|
let rows;
|
|
6992
7787
|
if (all || projectName !== null) {
|
|
@@ -7013,11 +7808,11 @@ var sessionsCommand = async (config, args) => {
|
|
|
7013
7808
|
}
|
|
7014
7809
|
if (rows.length === 0) {
|
|
7015
7810
|
if (json) {
|
|
7016
|
-
|
|
7811
|
+
say5(JSON.stringify({ version: 1, sessions: [] }, null, 2));
|
|
7017
7812
|
return;
|
|
7018
7813
|
}
|
|
7019
|
-
|
|
7020
|
-
|
|
7814
|
+
say5(
|
|
7815
|
+
dim5(all ? "no sessions" : "no active sessions \u2014 mend sessions --all includes settled ones")
|
|
7021
7816
|
);
|
|
7022
7817
|
return;
|
|
7023
7818
|
}
|
|
@@ -7051,7 +7846,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
7051
7846
|
}
|
|
7052
7847
|
}))
|
|
7053
7848
|
};
|
|
7054
|
-
|
|
7849
|
+
say5(JSON.stringify(payload, null, 2));
|
|
7055
7850
|
return;
|
|
7056
7851
|
}
|
|
7057
7852
|
for (const row of rows) printSessionRow(row);
|
|
@@ -7131,11 +7926,11 @@ var worktreesCommand = async (config, args) => {
|
|
|
7131
7926
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
|
|
7132
7927
|
const payload = await buildWorktreesJson(config, projectName);
|
|
7133
7928
|
if (json) {
|
|
7134
|
-
|
|
7929
|
+
say5(JSON.stringify(payload, null, 2));
|
|
7135
7930
|
return;
|
|
7136
7931
|
}
|
|
7137
7932
|
if (payload.worktrees.length === 0) {
|
|
7138
|
-
|
|
7933
|
+
say5(dim5("no worktrees yet \u2014 mend claude --name <worktree> starts one"));
|
|
7139
7934
|
return;
|
|
7140
7935
|
}
|
|
7141
7936
|
for (const worktree of payload.worktrees) {
|
|
@@ -7143,19 +7938,19 @@ var worktreesCommand = async (config, args) => {
|
|
|
7143
7938
|
const facts = [
|
|
7144
7939
|
`${worktree.sessions.length} session${worktree.sessions.length === 1 ? "" : "s"}`
|
|
7145
7940
|
];
|
|
7146
|
-
if (live > 0) facts.push(
|
|
7941
|
+
if (live > 0) facts.push(green5(`${live} live`));
|
|
7147
7942
|
if (worktree.review !== null && worktree.review.openComments > 0) {
|
|
7148
7943
|
facts.push(amber(`${worktree.review.openComments} open`));
|
|
7149
7944
|
}
|
|
7150
7945
|
const base = worktree.baseRef ?? worktree.baseSha.slice(0, 12);
|
|
7151
|
-
|
|
7152
|
-
`${worktree.name.padEnd(24)} ${worktree.projectName} ${
|
|
7946
|
+
say5(
|
|
7947
|
+
`${worktree.name.padEnd(24)} ${worktree.projectName} ${dim5(`${worktree.branch} \xB7 base ${base}`)} ${facts.join(dim5(" \xB7 "))}`
|
|
7153
7948
|
);
|
|
7154
7949
|
for (const session of worktree.sessions) {
|
|
7155
7950
|
const status = session.status.padEnd(9);
|
|
7156
7951
|
const name = session.label ?? `session ${session.id.slice(0, 8)}`;
|
|
7157
|
-
|
|
7158
|
-
` \u2514 ${session.harness.padEnd(8)} ${
|
|
7952
|
+
say5(
|
|
7953
|
+
` \u2514 ${session.harness.padEnd(8)} ${dim5(session.id.slice(0, 8))} ${ACTIVE_STATUSES.has(session.status) ? green5(status) : dim5(status)} ${name}`
|
|
7159
7954
|
);
|
|
7160
7955
|
}
|
|
7161
7956
|
}
|
|
@@ -7170,24 +7965,24 @@ var hasNodeFfi = () => {
|
|
|
7170
7965
|
};
|
|
7171
7966
|
var dashboard = async (config) => {
|
|
7172
7967
|
if (process.stdout.isTTY !== true) {
|
|
7173
|
-
|
|
7968
|
+
say5(renderIndex());
|
|
7174
7969
|
return;
|
|
7175
7970
|
}
|
|
7176
7971
|
if (!hasNodeFfi()) {
|
|
7177
7972
|
const major = Number(process.versions.node.split(".")[0]);
|
|
7178
7973
|
if (Number.isNaN(major) || major < 26) {
|
|
7179
|
-
return
|
|
7974
|
+
return fail5(
|
|
7180
7975
|
`the dashboard needs Node >= 26 (node:ffi) \u2014 this is ${process.version}; every other command still works`
|
|
7181
7976
|
);
|
|
7182
7977
|
}
|
|
7183
|
-
const rerun =
|
|
7978
|
+
const rerun = spawnSync3(
|
|
7184
7979
|
process.execPath,
|
|
7185
7980
|
["--experimental-ffi", "--disable-warning=ExperimentalWarning", ...process.argv.slice(1)],
|
|
7186
7981
|
{ stdio: "inherit" }
|
|
7187
7982
|
);
|
|
7188
7983
|
process.exit(rerun.status ?? 0);
|
|
7189
7984
|
}
|
|
7190
|
-
const { runDashboard } = await import("./dashboard-
|
|
7985
|
+
const { runDashboard } = await import("./dashboard-ZKCPU2QJ.js");
|
|
7191
7986
|
const agentShare = await startShareIfBridge(config);
|
|
7192
7987
|
try {
|
|
7193
7988
|
await runDashboard({
|
|
@@ -7213,11 +8008,7 @@ var startShareIfBridge = async (config) => {
|
|
|
7213
8008
|
}
|
|
7214
8009
|
if (access.mode !== "bridge") return null;
|
|
7215
8010
|
return startAgentShare({
|
|
7216
|
-
url:
|
|
7217
|
-
parseMendUrl(config.url).toString().replace(/\/$/, ""),
|
|
7218
|
-
config.token,
|
|
7219
|
-
os6.hostname()
|
|
7220
|
-
),
|
|
8011
|
+
url: () => socketUrl(config, "keys-bridge", { host: os7.hostname() }),
|
|
7221
8012
|
agentSock
|
|
7222
8013
|
});
|
|
7223
8014
|
};
|
|
@@ -7231,38 +8022,38 @@ var withAgentShare = async (config, work) => {
|
|
|
7231
8022
|
};
|
|
7232
8023
|
var helpCommand = (words) => {
|
|
7233
8024
|
if (words.length === 0) {
|
|
7234
|
-
|
|
8025
|
+
say5(renderIndex());
|
|
7235
8026
|
return;
|
|
7236
8027
|
}
|
|
7237
8028
|
const doc = findCommand(words);
|
|
7238
8029
|
if (doc !== null && (doc.name.split(" ").length > 1 || words.length === 1)) {
|
|
7239
|
-
|
|
8030
|
+
say5(renderCommand(doc));
|
|
7240
8031
|
return;
|
|
7241
8032
|
}
|
|
7242
8033
|
const group = renderGroup(words[0] ?? "");
|
|
7243
8034
|
if (doc !== null) {
|
|
7244
|
-
|
|
8035
|
+
say5(renderCommand(doc));
|
|
7245
8036
|
return;
|
|
7246
8037
|
}
|
|
7247
8038
|
if (group !== null) {
|
|
7248
|
-
|
|
8039
|
+
say5(group);
|
|
7249
8040
|
return;
|
|
7250
8041
|
}
|
|
7251
|
-
return
|
|
8042
|
+
return fail5(`no command "${words.join(" ")}" \xB7 mend help lists them`);
|
|
7252
8043
|
};
|
|
7253
8044
|
var manCommand = (words) => {
|
|
7254
8045
|
const doc = words.length === 0 ? null : findCommand(words);
|
|
7255
8046
|
if (words.length > 0 && doc === null) {
|
|
7256
|
-
return
|
|
8047
|
+
return fail5(`no command "${words.join(" ")}" \xB7 mend help lists them`);
|
|
7257
8048
|
}
|
|
7258
8049
|
const version = cliVersion();
|
|
7259
8050
|
const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
|
|
7260
|
-
const file =
|
|
7261
|
-
|
|
7262
|
-
const result =
|
|
7263
|
-
|
|
8051
|
+
const file = path12.join(os7.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
|
|
8052
|
+
fs11.writeFileSync(file, page);
|
|
8053
|
+
const result = spawnSync3("man", ["-l", file], { stdio: "inherit" });
|
|
8054
|
+
fs11.rmSync(file, { force: true });
|
|
7264
8055
|
if (result.error !== void 0 || result.status !== 0) {
|
|
7265
|
-
|
|
8056
|
+
say5(doc === null ? renderIndex() : renderCommand(doc));
|
|
7266
8057
|
}
|
|
7267
8058
|
};
|
|
7268
8059
|
var main = async () => {
|
|
@@ -7273,7 +8064,7 @@ var main = async () => {
|
|
|
7273
8064
|
}
|
|
7274
8065
|
if (command === "server") {
|
|
7275
8066
|
const result = await serverCommand(rest);
|
|
7276
|
-
if (result._tag === "error") return
|
|
8067
|
+
if (result._tag === "error") return fail5(result.message);
|
|
7277
8068
|
return;
|
|
7278
8069
|
}
|
|
7279
8070
|
const config = loadConfig();
|
|
@@ -7311,11 +8102,30 @@ var main = async () => {
|
|
|
7311
8102
|
return connectCommand(config, rest);
|
|
7312
8103
|
case "pair":
|
|
7313
8104
|
return pairCommand(rest, boundApi(config));
|
|
8105
|
+
case "invite":
|
|
8106
|
+
return inviteCommand(boundApi(config), config.url, rest);
|
|
8107
|
+
case "members":
|
|
8108
|
+
return membersCommand(boundApi(config));
|
|
8109
|
+
case "folder":
|
|
8110
|
+
return folderCommand(boundApi(config), rest);
|
|
8111
|
+
case "operator":
|
|
8112
|
+
return operatorCommand(
|
|
8113
|
+
boundApi(config),
|
|
8114
|
+
(method, route, body) => request(config, method, route, body),
|
|
8115
|
+
config.url,
|
|
8116
|
+
rest
|
|
8117
|
+
);
|
|
8118
|
+
case "session":
|
|
8119
|
+
if (rest[0] === "share") return sessionShareCommand(boundApi(config), rest.slice(1));
|
|
8120
|
+
return fail5(`unknown session command "${rest[0] ?? ""}" \xB7 mend help session share`);
|
|
7314
8121
|
// Hidden in the catalog: the installer renders its own QR through this.
|
|
7315
8122
|
case "qr":
|
|
7316
8123
|
return qrCommand(rest);
|
|
7317
8124
|
case "doctor":
|
|
7318
|
-
return doctorCommand(config, localCredential)
|
|
8125
|
+
return doctorCommand(config, localCredential, () => {
|
|
8126
|
+
const read = readGrant(claudeGrantDir(mendCliHome()));
|
|
8127
|
+
return read.kind === "missing" ? null : read.secret;
|
|
8128
|
+
});
|
|
7319
8129
|
case "env":
|
|
7320
8130
|
return envCommand(config, rest);
|
|
7321
8131
|
case "ssh":
|
|
@@ -7352,10 +8162,10 @@ var main = async () => {
|
|
|
7352
8162
|
case "--version":
|
|
7353
8163
|
case "-v":
|
|
7354
8164
|
for (const line of versionLines(cliVersion(), await fetchServerVersion(config.url)))
|
|
7355
|
-
|
|
8165
|
+
say5(line);
|
|
7356
8166
|
return;
|
|
7357
8167
|
default:
|
|
7358
|
-
return
|
|
8168
|
+
return fail5(`unknown command "${command}" \xB7 mend help lists them`);
|
|
7359
8169
|
}
|
|
7360
8170
|
};
|
|
7361
8171
|
await main();
|