@sealant/mend 0.28.0 → 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 +4 -3
- package/dist/{chunk-7JPOKREB.js → chunk-F6AJNFVE.js} +238 -8
- package/dist/{dashboard-4WQC5MZ4.js → dashboard-ZKCPU2QJ.js} +1 -1
- package/dist/main.js +1470 -662
- 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 +1 -1
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +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" },
|
|
@@ -1015,7 +1147,166 @@ var COMMANDS = [
|
|
|
1015
1147
|
],
|
|
1016
1148
|
see: ["keys mode", "adopt"]
|
|
1017
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
|
+
},
|
|
1018
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
|
+
},
|
|
1019
1310
|
{
|
|
1020
1311
|
name: "server",
|
|
1021
1312
|
section: "this machine",
|
|
@@ -1081,7 +1372,7 @@ var COMMANDS = [
|
|
|
1081
1372
|
{ command: "mend server setup", text: "localhost, using the current local Docker context" },
|
|
1082
1373
|
{
|
|
1083
1374
|
command: "mend server setup --context orbstack --bind 0.0.0.0 --url http://100.70.80.90:3105",
|
|
1084
|
-
text: "explicit tailnet
|
|
1375
|
+
text: "explicit private-network exposure (a tailnet, a LAN, a VPN)"
|
|
1085
1376
|
},
|
|
1086
1377
|
{
|
|
1087
1378
|
command: "mend server setup --version 0.25.0 --assets-dir ./release-assets --offline",
|
|
@@ -1518,7 +1809,7 @@ var manFileName = (doc) => doc === null ? "mend.1" : `mend-${doc.name.replace(/
|
|
|
1518
1809
|
|
|
1519
1810
|
// src/login.ts
|
|
1520
1811
|
import { spawn } from "node:child_process";
|
|
1521
|
-
import * as
|
|
1812
|
+
import * as os2 from "node:os";
|
|
1522
1813
|
|
|
1523
1814
|
// src/pair.ts
|
|
1524
1815
|
var paint = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
@@ -1710,7 +2001,7 @@ var resolveServerUrl = async (args, deps) => {
|
|
|
1710
2001
|
};
|
|
1711
2002
|
var loginCommand = async (args, deps) => {
|
|
1712
2003
|
const base = await resolveServerUrl(args, deps);
|
|
1713
|
-
const started = await post(`${base}/api/cli/auth`, { name:
|
|
2004
|
+
const started = await post(`${base}/api/cli/auth`, { name: os2.hostname() });
|
|
1714
2005
|
if (started.status === 429) {
|
|
1715
2006
|
return fail2(
|
|
1716
2007
|
`the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
|
|
@@ -1760,15 +2051,338 @@ var loginCommand = async (args, deps) => {
|
|
|
1760
2051
|
return fail2("the authorize request expired before anyone approved \u2014 run mend login again");
|
|
1761
2052
|
};
|
|
1762
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
|
+
|
|
1763
2377
|
// src/server-setup.ts
|
|
1764
2378
|
import { createHmac, randomBytes } from "node:crypto";
|
|
1765
2379
|
import { readFile } from "node:fs/promises";
|
|
1766
2380
|
import * as net2 from "node:net";
|
|
1767
|
-
import * as
|
|
1768
|
-
import * as
|
|
2381
|
+
import * as os4 from "node:os";
|
|
2382
|
+
import * as path7 from "node:path";
|
|
1769
2383
|
|
|
1770
2384
|
// src/server-docker-volumes.ts
|
|
1771
|
-
import { createHash } from "node:crypto";
|
|
2385
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1772
2386
|
var MEND_DOCKER_NAMESPACE = {
|
|
1773
2387
|
project: "mend",
|
|
1774
2388
|
store: "mend-store",
|
|
@@ -1796,15 +2410,15 @@ var ServerVolumeOwnershipError = class extends Error {
|
|
|
1796
2410
|
this.operation = operation;
|
|
1797
2411
|
}
|
|
1798
2412
|
};
|
|
1799
|
-
var
|
|
2413
|
+
var fail4 = (reason, operation) => ({ _tag: "error", error: new ServerVolumeOwnershipError(reason, operation) });
|
|
1800
2414
|
var docker = async (runtime, context, args) => {
|
|
1801
2415
|
try {
|
|
1802
2416
|
const output = await runtime.run("docker", ["--context", context, ...args]);
|
|
1803
2417
|
if (output.status !== 0 || output.error !== void 0)
|
|
1804
|
-
return
|
|
2418
|
+
return fail4("docker", args.slice(0, 2).join(" "));
|
|
1805
2419
|
return { _tag: "ok", value: output.stdout };
|
|
1806
2420
|
} catch {
|
|
1807
|
-
return
|
|
2421
|
+
return fail4("docker", args.slice(0, 2).join(" "));
|
|
1808
2422
|
}
|
|
1809
2423
|
};
|
|
1810
2424
|
var listNames = async (runtime, context, kind, project2) => {
|
|
@@ -1823,11 +2437,11 @@ var listNames = async (runtime, context, kind, project2) => {
|
|
|
1823
2437
|
for (const line of output.value.trim().split("\n").filter(Boolean)) {
|
|
1824
2438
|
const value = JSON.parse(line);
|
|
1825
2439
|
if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value))
|
|
1826
|
-
return
|
|
2440
|
+
return fail4("inspection", `${kind} list`);
|
|
1827
2441
|
names.push(value);
|
|
1828
2442
|
}
|
|
1829
2443
|
} catch {
|
|
1830
|
-
return
|
|
2444
|
+
return fail4("inspection", `${kind} list`);
|
|
1831
2445
|
}
|
|
1832
2446
|
return { _tag: "ok", value: names };
|
|
1833
2447
|
};
|
|
@@ -1843,16 +2457,16 @@ var inspectOwner = async (runtime, context, name, owner) => {
|
|
|
1843
2457
|
try {
|
|
1844
2458
|
const volume = JSON.parse(output.value);
|
|
1845
2459
|
if (typeof volume !== "object" || volume === null || !("Name" in volume) || volume.Name !== name || !("Labels" in volume))
|
|
1846
|
-
return
|
|
2460
|
+
return fail4("inspection", "volume inspect");
|
|
1847
2461
|
const labels = volume.Labels;
|
|
1848
|
-
if (labels === null) return
|
|
2462
|
+
if (labels === null) return fail4("conflict", "volume owner");
|
|
1849
2463
|
if (typeof labels !== "object" || Array.isArray(labels) || !Object.values(labels).every((v) => typeof v === "string"))
|
|
1850
|
-
return
|
|
2464
|
+
return fail4("inspection", "volume labels");
|
|
1851
2465
|
if (!(SERVER_VOLUME_OWNER_LABEL in labels) || labels[SERVER_VOLUME_OWNER_LABEL] !== owner)
|
|
1852
|
-
return
|
|
2466
|
+
return fail4("conflict", "volume owner");
|
|
1853
2467
|
return { _tag: "ok", value: void 0 };
|
|
1854
2468
|
} catch {
|
|
1855
|
-
return
|
|
2469
|
+
return fail4("inspection", "volume inspect");
|
|
1856
2470
|
}
|
|
1857
2471
|
};
|
|
1858
2472
|
var ensureVolume = async (runtime, context, name, owner) => {
|
|
@@ -1891,38 +2505,38 @@ var inspectForeignProject = async (runtime, context, kind, name, project2) => {
|
|
|
1891
2505
|
try {
|
|
1892
2506
|
const resource = JSON.parse(output.value);
|
|
1893
2507
|
if (typeof resource !== "object" || resource === null || !("Name" in resource) || resource.Name !== (kind === "container" ? `/${name}` : name) || !("Labels" in resource))
|
|
1894
|
-
return
|
|
2508
|
+
return fail4("inspection", `${kind} inspect`);
|
|
1895
2509
|
const labels = resource.Labels;
|
|
1896
|
-
if (labels === null) return
|
|
2510
|
+
if (labels === null) return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1897
2511
|
if (typeof labels !== "object" || Array.isArray(labels) || !Object.values(labels).every((value) => typeof value === "string"))
|
|
1898
|
-
return
|
|
2512
|
+
return fail4("inspection", `${kind} labels`);
|
|
1899
2513
|
if (!("com.docker.compose.project" in labels))
|
|
1900
|
-
return
|
|
2514
|
+
return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1901
2515
|
const foreignProject = labels["com.docker.compose.project"];
|
|
1902
2516
|
if (typeof foreignProject !== "string" || !/^[a-z0-9][a-z0-9_-]*$/.test(foreignProject))
|
|
1903
|
-
return
|
|
2517
|
+
return fail4("inspection", `${kind} project label`);
|
|
1904
2518
|
if (foreignProject === project2 || !inProjectNamespace(name, foreignProject))
|
|
1905
|
-
return
|
|
2519
|
+
return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1906
2520
|
return { _tag: "ok", value: void 0 };
|
|
1907
2521
|
} catch {
|
|
1908
|
-
return
|
|
2522
|
+
return fail4("inspection", `${kind} inspect`);
|
|
1909
2523
|
}
|
|
1910
2524
|
};
|
|
1911
2525
|
var refuseOldData = async (runtime, context, namespace, volumes) => {
|
|
1912
2526
|
if (volumes.some(
|
|
1913
2527
|
(name) => secondaryVolumesOf(namespace).includes(name) || name.startsWith(`${namespace.project}_`)
|
|
1914
2528
|
))
|
|
1915
|
-
return
|
|
2529
|
+
return fail4("unowned-data", "existing volumes without anchor");
|
|
1916
2530
|
for (const kind of ["volume", "container", "network"]) {
|
|
1917
2531
|
const labelled = await listNames(runtime, context, kind, namespace.project);
|
|
1918
2532
|
if (labelled._tag === "error") return labelled;
|
|
1919
|
-
if (labelled.value.length > 0) return
|
|
2533
|
+
if (labelled.value.length > 0) return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1920
2534
|
if (kind !== "volume") {
|
|
1921
2535
|
const named = await listNames(runtime, context, kind);
|
|
1922
2536
|
if (named._tag === "error") return named;
|
|
1923
2537
|
for (const name of named.value) {
|
|
1924
2538
|
if (reservedResourceName(name, kind, namespace))
|
|
1925
|
-
return
|
|
2539
|
+
return fail4("unowned-data", `existing ${kind} without anchor`);
|
|
1926
2540
|
if (name.startsWith(`${namespace.project}_`) || name.startsWith(`${namespace.project}-`)) {
|
|
1927
2541
|
const foreign = await inspectForeignProject(
|
|
1928
2542
|
runtime,
|
|
@@ -1943,8 +2557,8 @@ var parseOwnershipInput = (input) => {
|
|
|
1943
2557
|
if (!input.dockerContext.trim() || input.identityBytes.byteLength === 0 || !/^[a-z0-9][a-z0-9_-]*$/.test(namespace.project) || ![namespace.store, ...secondaryVolumesOf(namespace)].every(
|
|
1944
2558
|
(name) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/.test(name)
|
|
1945
2559
|
) || (/* @__PURE__ */ new Set([namespace.store, ...secondaryVolumesOf(namespace)])).size !== 1 + secondaryVolumesOf(namespace).length)
|
|
1946
|
-
return
|
|
1947
|
-
const owner =
|
|
2560
|
+
return fail4("invalid-input", "ownership inputs");
|
|
2561
|
+
const owner = createHash2("sha256").update(input.identityBytes).digest("hex");
|
|
1948
2562
|
return { _tag: "ok", value: { owner, namespace } };
|
|
1949
2563
|
};
|
|
1950
2564
|
var ownership = async (runtime, input, mode) => {
|
|
@@ -1957,7 +2571,7 @@ var ownership = async (runtime, input, mode) => {
|
|
|
1957
2571
|
const anchor = await inspectOwner(runtime, input.dockerContext, namespace.store, owner);
|
|
1958
2572
|
if (anchor._tag === "error") return anchor;
|
|
1959
2573
|
} else {
|
|
1960
|
-
if (mode === "verify") return
|
|
2574
|
+
if (mode === "verify") return fail4("missing", "anchor");
|
|
1961
2575
|
const empty = await refuseOldData(runtime, input.dockerContext, namespace, volumes.value);
|
|
1962
2576
|
if (empty._tag === "error") return empty;
|
|
1963
2577
|
const anchor = await ensureVolume(runtime, input.dockerContext, namespace.store, owner);
|
|
@@ -1971,7 +2585,7 @@ var ownership = async (runtime, input, mode) => {
|
|
|
1971
2585
|
const owned = await inspectOwner(runtime, input.dockerContext, name, owner);
|
|
1972
2586
|
if (owned._tag === "error") return owned;
|
|
1973
2587
|
} else {
|
|
1974
|
-
if (mode === "verify") return
|
|
2588
|
+
if (mode === "verify") return fail4("missing", role);
|
|
1975
2589
|
const created = await ensureVolume(runtime, input.dockerContext, name, owner);
|
|
1976
2590
|
if (created._tag === "error") return created;
|
|
1977
2591
|
}
|
|
@@ -1983,8 +2597,8 @@ var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "v
|
|
|
1983
2597
|
|
|
1984
2598
|
// src/server-runtime.ts
|
|
1985
2599
|
import { spawn as spawn2 } from "node:child_process";
|
|
1986
|
-
import * as
|
|
1987
|
-
import * as
|
|
2600
|
+
import * as fs5 from "node:fs";
|
|
2601
|
+
import * as path5 from "node:path";
|
|
1988
2602
|
var serverProcessDeadlines = {
|
|
1989
2603
|
ordinary: 3e4,
|
|
1990
2604
|
pull: 10 * 6e4,
|
|
@@ -2030,7 +2644,7 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
|
|
|
2030
2644
|
return new Promise((resolve4) => {
|
|
2031
2645
|
let fd;
|
|
2032
2646
|
try {
|
|
2033
|
-
if (options.stdoutFile !== void 0) fd =
|
|
2647
|
+
if (options.stdoutFile !== void 0) fd = fs5.openSync(options.stdoutFile, "wx", 384);
|
|
2034
2648
|
const child = spawn2(command, [...args], {
|
|
2035
2649
|
env,
|
|
2036
2650
|
detached: process.platform !== "win32",
|
|
@@ -2081,12 +2695,12 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
|
|
|
2081
2695
|
child.once("close", (status) => {
|
|
2082
2696
|
globalThis.clearTimeout(timer);
|
|
2083
2697
|
try {
|
|
2084
|
-
if (fd !== void 0)
|
|
2698
|
+
if (fd !== void 0) fs5.fsyncSync(fd);
|
|
2085
2699
|
} catch {
|
|
2086
2700
|
failure2 ??= "Could not durably write process output.";
|
|
2087
2701
|
} finally {
|
|
2088
2702
|
try {
|
|
2089
|
-
if (fd !== void 0)
|
|
2703
|
+
if (fd !== void 0) fs5.closeSync(fd);
|
|
2090
2704
|
} catch {
|
|
2091
2705
|
failure2 ??= "Could not close process output file.";
|
|
2092
2706
|
}
|
|
@@ -2100,7 +2714,7 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
|
|
|
2100
2714
|
});
|
|
2101
2715
|
} catch (cause) {
|
|
2102
2716
|
try {
|
|
2103
|
-
if (fd !== void 0)
|
|
2717
|
+
if (fd !== void 0) fs5.closeSync(fd);
|
|
2104
2718
|
} catch {
|
|
2105
2719
|
}
|
|
2106
2720
|
resolve4({
|
|
@@ -2121,17 +2735,17 @@ var serverComposeArgs = (installation, command) => [
|
|
|
2121
2735
|
"--project-directory",
|
|
2122
2736
|
installation.directory,
|
|
2123
2737
|
"--env-file",
|
|
2124
|
-
|
|
2738
|
+
path5.join(installation.directory, "server.env"),
|
|
2125
2739
|
"-f",
|
|
2126
|
-
|
|
2740
|
+
path5.join(installation.directory, "compose.yaml"),
|
|
2127
2741
|
...command
|
|
2128
2742
|
];
|
|
2129
2743
|
|
|
2130
2744
|
// src/server-store.ts
|
|
2131
2745
|
import { randomUUID } from "node:crypto";
|
|
2132
|
-
import * as
|
|
2133
|
-
import * as
|
|
2134
|
-
import * as
|
|
2746
|
+
import * as fs6 from "node:fs";
|
|
2747
|
+
import * as os3 from "node:os";
|
|
2748
|
+
import * as path6 from "node:path";
|
|
2135
2749
|
var ServerStoreError = class extends Error {
|
|
2136
2750
|
_tag = "ServerStoreError";
|
|
2137
2751
|
};
|
|
@@ -2152,28 +2766,28 @@ var attempt = (operation) => {
|
|
|
2152
2766
|
var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
|
|
2153
2767
|
var readOptional = (file) => {
|
|
2154
2768
|
try {
|
|
2155
|
-
return
|
|
2769
|
+
return fs6.readFileSync(file, "utf8");
|
|
2156
2770
|
} catch (cause) {
|
|
2157
2771
|
if (hasCode(cause, "ENOENT")) return null;
|
|
2158
2772
|
throw cause;
|
|
2159
2773
|
}
|
|
2160
2774
|
};
|
|
2161
2775
|
var syncDirectory = (directory) => {
|
|
2162
|
-
const fd =
|
|
2776
|
+
const fd = fs6.openSync(directory, "r");
|
|
2163
2777
|
try {
|
|
2164
|
-
|
|
2778
|
+
fs6.fsyncSync(fd);
|
|
2165
2779
|
} finally {
|
|
2166
|
-
|
|
2780
|
+
fs6.closeSync(fd);
|
|
2167
2781
|
}
|
|
2168
2782
|
};
|
|
2169
2783
|
var writeDurable = (file, content, mode = 384) => {
|
|
2170
|
-
const fd =
|
|
2784
|
+
const fd = fs6.openSync(file, "wx", mode);
|
|
2171
2785
|
try {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2786
|
+
fs6.writeFileSync(fd, content, "utf8");
|
|
2787
|
+
fs6.fchmodSync(fd, mode);
|
|
2788
|
+
fs6.fsyncSync(fd);
|
|
2175
2789
|
} finally {
|
|
2176
|
-
|
|
2790
|
+
fs6.closeSync(fd);
|
|
2177
2791
|
}
|
|
2178
2792
|
};
|
|
2179
2793
|
var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
|
|
@@ -2193,11 +2807,11 @@ var parseLockOwner = (raw) => {
|
|
|
2193
2807
|
};
|
|
2194
2808
|
var describeOwner = (lockDir) => {
|
|
2195
2809
|
try {
|
|
2196
|
-
const raw = JSON.parse(
|
|
2810
|
+
const raw = JSON.parse(fs6.readFileSync(path6.join(lockDir, "owner.json"), "utf8"));
|
|
2197
2811
|
const owner = parseLockOwner(raw);
|
|
2198
2812
|
if (owner === null) return "owner metadata is invalid";
|
|
2199
2813
|
const label = `PID ${owner.pid} on ${owner.hostname}`;
|
|
2200
|
-
if (owner.hostname !==
|
|
2814
|
+
if (owner.hostname !== os3.hostname()) return label;
|
|
2201
2815
|
try {
|
|
2202
2816
|
process.kill(owner.pid, 0);
|
|
2203
2817
|
return `${label} is still live`;
|
|
@@ -2213,28 +2827,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
|
|
|
2213
2827
|
);
|
|
2214
2828
|
var acquireLock = (configDir, create) => {
|
|
2215
2829
|
if (create) {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
} else if (!
|
|
2830
|
+
fs6.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
2831
|
+
fs6.chmodSync(configDir, 448);
|
|
2832
|
+
} else if (!fs6.existsSync(configDir)) {
|
|
2219
2833
|
throw new ServerStoreError(
|
|
2220
2834
|
"No Mend server is configured. Run mend server setup explicitly to install one."
|
|
2221
2835
|
);
|
|
2222
2836
|
}
|
|
2223
|
-
const lockDir =
|
|
2837
|
+
const lockDir = path6.join(configDir, "server.lock");
|
|
2224
2838
|
try {
|
|
2225
|
-
|
|
2839
|
+
fs6.mkdirSync(lockDir, { mode: 448 });
|
|
2226
2840
|
} catch (cause) {
|
|
2227
2841
|
if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
|
|
2228
2842
|
throw cause;
|
|
2229
2843
|
}
|
|
2230
|
-
const owned =
|
|
2231
|
-
const ownerFile =
|
|
2232
|
-
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() })}
|
|
2233
2847
|
`;
|
|
2234
2848
|
let open = true;
|
|
2235
2849
|
const assertOwned = () => {
|
|
2236
|
-
const current =
|
|
2237
|
-
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) {
|
|
2238
2852
|
throw new ServerStoreError(
|
|
2239
2853
|
"Server lock ownership was lost. Stop and check for another server command before retrying."
|
|
2240
2854
|
);
|
|
@@ -2250,8 +2864,8 @@ var acquireLock = (configDir, create) => {
|
|
|
2250
2864
|
release: () => {
|
|
2251
2865
|
const result = attempt(() => {
|
|
2252
2866
|
assertOwned();
|
|
2253
|
-
|
|
2254
|
-
|
|
2867
|
+
fs6.unlinkSync(ownerFile);
|
|
2868
|
+
fs6.rmdirSync(lockDir);
|
|
2255
2869
|
});
|
|
2256
2870
|
open = false;
|
|
2257
2871
|
return result;
|
|
@@ -2260,7 +2874,7 @@ var acquireLock = (configDir, create) => {
|
|
|
2260
2874
|
};
|
|
2261
2875
|
var readIdentity = (paths) => {
|
|
2262
2876
|
const identity = readOptional(paths.identity);
|
|
2263
|
-
if (identity === null && (
|
|
2877
|
+
if (identity === null && (fs6.existsSync(paths.active) || fs6.existsSync(paths.generations) && fs6.readdirSync(paths.generations).length > 0)) {
|
|
2264
2878
|
throw new ServerStoreError(
|
|
2265
2879
|
"Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
|
|
2266
2880
|
);
|
|
@@ -2270,7 +2884,7 @@ var readIdentity = (paths) => {
|
|
|
2270
2884
|
var readActive = (paths) => {
|
|
2271
2885
|
let target;
|
|
2272
2886
|
try {
|
|
2273
|
-
target =
|
|
2887
|
+
target = fs6.readlinkSync(paths.active);
|
|
2274
2888
|
} catch (cause) {
|
|
2275
2889
|
if (hasCode(cause, "ENOENT")) return null;
|
|
2276
2890
|
throw cause;
|
|
@@ -2280,13 +2894,13 @@ var readActive = (paths) => {
|
|
|
2280
2894
|
"Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
|
|
2281
2895
|
);
|
|
2282
2896
|
}
|
|
2283
|
-
const directory =
|
|
2897
|
+
const directory = path6.join(paths.configDir, target);
|
|
2284
2898
|
const files = {
|
|
2285
|
-
identity:
|
|
2286
|
-
config:
|
|
2287
|
-
env:
|
|
2288
|
-
compose:
|
|
2289
|
-
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")
|
|
2290
2904
|
};
|
|
2291
2905
|
if (readIdentity(paths) !== files.identity) {
|
|
2292
2906
|
throw new ServerStoreError(
|
|
@@ -2296,20 +2910,20 @@ var readActive = (paths) => {
|
|
|
2296
2910
|
return { directory, files };
|
|
2297
2911
|
};
|
|
2298
2912
|
var publishIdentity = (paths, identity) => {
|
|
2299
|
-
const temporary =
|
|
2913
|
+
const temporary = path6.join(paths.configDir, `.identity-${randomUUID()}`);
|
|
2300
2914
|
writeDurable(temporary, identity);
|
|
2301
|
-
|
|
2915
|
+
fs6.linkSync(temporary, paths.identity);
|
|
2302
2916
|
syncDirectory(paths.configDir);
|
|
2303
|
-
|
|
2917
|
+
fs6.unlinkSync(temporary);
|
|
2304
2918
|
};
|
|
2305
2919
|
var prepareGeneration = (paths, files) => {
|
|
2306
|
-
|
|
2920
|
+
fs6.mkdirSync(paths.generations, { recursive: true, mode: 448 });
|
|
2307
2921
|
const generationName = `gen-${randomUUID()}`;
|
|
2308
|
-
const directory =
|
|
2309
|
-
|
|
2922
|
+
const directory = path6.join(paths.generations, generationName);
|
|
2923
|
+
fs6.mkdirSync(directory, { mode: 448 });
|
|
2310
2924
|
for (const key of fileKeys) {
|
|
2311
2925
|
writeDurable(
|
|
2312
|
-
|
|
2926
|
+
path6.join(directory, fileNames[key]),
|
|
2313
2927
|
files[key],
|
|
2314
2928
|
key === "postgresInit" ? 493 : 384
|
|
2315
2929
|
);
|
|
@@ -2320,18 +2934,18 @@ var prepareGeneration = (paths, files) => {
|
|
|
2320
2934
|
return { directory, files };
|
|
2321
2935
|
};
|
|
2322
2936
|
var activateGeneration = (paths, generation) => {
|
|
2323
|
-
const relative3 =
|
|
2324
|
-
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")) {
|
|
2325
2939
|
throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
|
|
2326
2940
|
}
|
|
2327
2941
|
for (const key of fileKeys) {
|
|
2328
|
-
if (
|
|
2942
|
+
if (fs6.readFileSync(path6.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
|
|
2329
2943
|
throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
|
|
2330
2944
|
}
|
|
2331
2945
|
}
|
|
2332
|
-
const pointer =
|
|
2333
|
-
|
|
2334
|
-
|
|
2946
|
+
const pointer = path6.join(paths.configDir, `.active-${randomUUID()}`);
|
|
2947
|
+
fs6.symlinkSync(relative3, pointer);
|
|
2948
|
+
fs6.renameSync(pointer, paths.active);
|
|
2335
2949
|
syncDirectory(paths.configDir);
|
|
2336
2950
|
};
|
|
2337
2951
|
var prepareFiles = (paths, files) => {
|
|
@@ -2340,7 +2954,7 @@ var prepareFiles = (paths, files) => {
|
|
|
2340
2954
|
throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
|
|
2341
2955
|
}
|
|
2342
2956
|
const active = readActive(paths);
|
|
2343
|
-
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)
|
|
2344
2958
|
return active;
|
|
2345
2959
|
if (identity === null) publishIdentity(paths, files.identity);
|
|
2346
2960
|
return prepareGeneration(paths, files);
|
|
@@ -2353,9 +2967,9 @@ var commitGeneration = (paths, files) => {
|
|
|
2353
2967
|
var createStore = (configDir, lock) => {
|
|
2354
2968
|
const paths = {
|
|
2355
2969
|
configDir,
|
|
2356
|
-
identity:
|
|
2357
|
-
generations:
|
|
2358
|
-
active:
|
|
2970
|
+
identity: path6.join(configDir, "identity.env"),
|
|
2971
|
+
generations: path6.join(configDir, "generations"),
|
|
2972
|
+
active: path6.join(configDir, "active")
|
|
2359
2973
|
};
|
|
2360
2974
|
const whileOwned = (operation) => attempt(() => {
|
|
2361
2975
|
lock.assertOwned();
|
|
@@ -2368,13 +2982,13 @@ var createStore = (configDir, lock) => {
|
|
|
2368
2982
|
prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
|
|
2369
2983
|
activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
|
|
2370
2984
|
createBackup: (previous, target) => whileOwned(() => {
|
|
2371
|
-
const backups =
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
const directory =
|
|
2375
|
-
|
|
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 });
|
|
2376
2990
|
writeDurable(
|
|
2377
|
-
|
|
2991
|
+
path6.join(directory, "recovery.json"),
|
|
2378
2992
|
`${JSON.stringify(
|
|
2379
2993
|
{
|
|
2380
2994
|
previousGeneration: previous.directory,
|
|
@@ -2390,21 +3004,21 @@ var createStore = (configDir, lock) => {
|
|
|
2390
3004
|
syncDirectory(directory);
|
|
2391
3005
|
syncDirectory(backups);
|
|
2392
3006
|
syncDirectory(configDir);
|
|
2393
|
-
const partialFile =
|
|
3007
|
+
const partialFile = path6.join(directory, "database.sql.partial");
|
|
2394
3008
|
return {
|
|
2395
3009
|
directory,
|
|
2396
3010
|
partialFile,
|
|
2397
3011
|
complete: () => whileOwned(() => {
|
|
2398
|
-
if (
|
|
3012
|
+
if (fs6.statSync(partialFile).size === 0)
|
|
2399
3013
|
throw new ServerStoreError("Database backup is empty; refusing target startup.");
|
|
2400
|
-
const fd =
|
|
3014
|
+
const fd = fs6.openSync(partialFile, "r");
|
|
2401
3015
|
try {
|
|
2402
|
-
|
|
2403
|
-
|
|
3016
|
+
fs6.fchmodSync(fd, 384);
|
|
3017
|
+
fs6.fsyncSync(fd);
|
|
2404
3018
|
} finally {
|
|
2405
|
-
|
|
3019
|
+
fs6.closeSync(fd);
|
|
2406
3020
|
}
|
|
2407
|
-
|
|
3021
|
+
fs6.renameSync(partialFile, path6.join(directory, "database.sql"));
|
|
2408
3022
|
syncDirectory(directory);
|
|
2409
3023
|
})
|
|
2410
3024
|
};
|
|
@@ -2413,7 +3027,7 @@ var createStore = (configDir, lock) => {
|
|
|
2413
3027
|
};
|
|
2414
3028
|
var refuseFlatLayout = (configDir) => {
|
|
2415
3029
|
if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
|
|
2416
|
-
(name) =>
|
|
3030
|
+
(name) => fs6.existsSync(path6.join(configDir, name))
|
|
2417
3031
|
)) {
|
|
2418
3032
|
throw new ServerStoreError(
|
|
2419
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."
|
|
@@ -2421,7 +3035,7 @@ var refuseFlatLayout = (configDir) => {
|
|
|
2421
3035
|
}
|
|
2422
3036
|
};
|
|
2423
3037
|
var withServerStore = async (directory, operation, options = {}) => {
|
|
2424
|
-
const configDir =
|
|
3038
|
+
const configDir = path6.resolve(directory);
|
|
2425
3039
|
let lock;
|
|
2426
3040
|
let result;
|
|
2427
3041
|
try {
|
|
@@ -2438,9 +3052,9 @@ var withServerStore = async (directory, operation, options = {}) => {
|
|
|
2438
3052
|
};
|
|
2439
3053
|
|
|
2440
3054
|
// src/version.ts
|
|
2441
|
-
import * as
|
|
3055
|
+
import * as fs7 from "node:fs";
|
|
2442
3056
|
var cliVersion = () => {
|
|
2443
|
-
const raw =
|
|
3057
|
+
const raw = fs7.readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
2444
3058
|
const parsed = JSON.parse(raw);
|
|
2445
3059
|
if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
|
|
2446
3060
|
const version = parsed.version;
|
|
@@ -3119,7 +3733,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
|
|
|
3119
3733
|
return version;
|
|
3120
3734
|
};
|
|
3121
3735
|
var parseDockerSocketPath = (socket) => {
|
|
3122
|
-
if (!
|
|
3736
|
+
if (!path7.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
|
|
3123
3737
|
throw setupError(
|
|
3124
3738
|
"--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
|
|
3125
3739
|
);
|
|
@@ -3145,8 +3759,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
|
|
|
3145
3759
|
if (options.assetsDir !== void 0) {
|
|
3146
3760
|
try {
|
|
3147
3761
|
const [compose2, postgresInit2] = await Promise.all([
|
|
3148
|
-
readFile(
|
|
3149
|
-
readFile(
|
|
3762
|
+
readFile(path7.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
|
|
3763
|
+
readFile(path7.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
|
|
3150
3764
|
]);
|
|
3151
3765
|
validateComposeAsset(compose2);
|
|
3152
3766
|
validatePostgresAsset(postgresInit2);
|
|
@@ -3659,10 +4273,10 @@ var manageServer = async (command, args, runtime, store) => {
|
|
|
3659
4273
|
};
|
|
3660
4274
|
var nodeServerRuntime = () => {
|
|
3661
4275
|
const environment = { ...process.env };
|
|
3662
|
-
const home =
|
|
3663
|
-
const configHome = environment["XDG_CONFIG_HOME"] ??
|
|
4276
|
+
const home = os4.homedir();
|
|
4277
|
+
const configHome = environment["XDG_CONFIG_HOME"] ?? path7.join(home, ".config");
|
|
3664
4278
|
return {
|
|
3665
|
-
configDir:
|
|
4279
|
+
configDir: path7.join(configHome, "mend"),
|
|
3666
4280
|
platform: process.platform,
|
|
3667
4281
|
cliVersion: cliVersion(),
|
|
3668
4282
|
run: (command, args, options) => runServerProcess(command, args, environment, options),
|
|
@@ -3888,8 +4502,8 @@ var renderMendToml = (proposals) => {
|
|
|
3888
4502
|
};
|
|
3889
4503
|
|
|
3890
4504
|
// src/skills.ts
|
|
3891
|
-
import * as
|
|
3892
|
-
import * as
|
|
4505
|
+
import * as fs8 from "node:fs";
|
|
4506
|
+
import * as path8 from "node:path";
|
|
3893
4507
|
var DEFAULT_SKILLS_DIR = ".agents/skills";
|
|
3894
4508
|
var MAX_SKILL_FILES = 64;
|
|
3895
4509
|
var MAX_SKILL_FILE_BYTES = 512 * 1024;
|
|
@@ -3912,12 +4526,12 @@ var readFrontmatterDescription = (contents) => {
|
|
|
3912
4526
|
var walkFiles = (root) => {
|
|
3913
4527
|
const found = [];
|
|
3914
4528
|
const visit = (dir) => {
|
|
3915
|
-
for (const entry of
|
|
4529
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
3916
4530
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3917
|
-
const absolute =
|
|
4531
|
+
const absolute = path8.join(dir, entry.name);
|
|
3918
4532
|
let stat;
|
|
3919
4533
|
try {
|
|
3920
|
-
stat =
|
|
4534
|
+
stat = fs8.statSync(absolute);
|
|
3921
4535
|
} catch {
|
|
3922
4536
|
continue;
|
|
3923
4537
|
}
|
|
@@ -3931,7 +4545,7 @@ var walkFiles = (root) => {
|
|
|
3931
4545
|
var scanSkillLibrary = (root) => {
|
|
3932
4546
|
let entries;
|
|
3933
4547
|
try {
|
|
3934
|
-
entries =
|
|
4548
|
+
entries = fs8.readdirSync(root, { withFileTypes: true });
|
|
3935
4549
|
} catch {
|
|
3936
4550
|
return { error: `${root} is not a readable directory` };
|
|
3937
4551
|
}
|
|
@@ -3939,23 +4553,23 @@ var scanSkillLibrary = (root) => {
|
|
|
3939
4553
|
const notes = [];
|
|
3940
4554
|
for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
|
|
3941
4555
|
if (entry.name.startsWith(".")) continue;
|
|
3942
|
-
const skillDir =
|
|
4556
|
+
const skillDir = path8.join(root, entry.name);
|
|
3943
4557
|
let stat;
|
|
3944
4558
|
try {
|
|
3945
|
-
stat =
|
|
4559
|
+
stat = fs8.statSync(skillDir);
|
|
3946
4560
|
} catch {
|
|
3947
4561
|
continue;
|
|
3948
4562
|
}
|
|
3949
4563
|
if (!stat.isDirectory()) continue;
|
|
3950
|
-
if (!
|
|
4564
|
+
if (!fs8.existsSync(path8.join(skillDir, "SKILL.md"))) {
|
|
3951
4565
|
notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
|
|
3952
4566
|
continue;
|
|
3953
4567
|
}
|
|
3954
4568
|
const files = [];
|
|
3955
4569
|
let bytes = 0;
|
|
3956
4570
|
for (const absolute of walkFiles(skillDir)) {
|
|
3957
|
-
const relative3 =
|
|
3958
|
-
const buffer =
|
|
4571
|
+
const relative3 = path8.relative(skillDir, absolute).split(path8.sep).join("/");
|
|
4572
|
+
const buffer = fs8.readFileSync(absolute);
|
|
3959
4573
|
if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
|
|
3960
4574
|
notes.push({ skill: entry.name, message: `${relative3} is over 512KB \u2014 skipped` });
|
|
3961
4575
|
continue;
|
|
@@ -3990,16 +4604,16 @@ var scanSkillLibrary = (root) => {
|
|
|
3990
4604
|
};
|
|
3991
4605
|
|
|
3992
4606
|
// src/ssh-setup.ts
|
|
3993
|
-
import * as
|
|
3994
|
-
import * as
|
|
4607
|
+
import * as os6 from "node:os";
|
|
4608
|
+
import * as path10 from "node:path";
|
|
3995
4609
|
|
|
3996
4610
|
// ../../packages/workspace-ssh/src/index.ts
|
|
3997
|
-
import { spawnSync } from "node:child_process";
|
|
3998
|
-
import { createHash as
|
|
3999
|
-
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";
|
|
4000
4614
|
import { isIP as isIP2 } from "node:net";
|
|
4001
|
-
import * as
|
|
4002
|
-
import * as
|
|
4615
|
+
import * as os5 from "node:os";
|
|
4616
|
+
import * as path9 from "node:path";
|
|
4003
4617
|
var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
|
|
4004
4618
|
var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
|
|
4005
4619
|
var WorkspaceSshError = class extends Error {
|
|
@@ -4017,7 +4631,7 @@ var WorkspaceSshError = class extends Error {
|
|
|
4017
4631
|
var success = (value) => ({ ok: true, value });
|
|
4018
4632
|
var failure = (error) => ({ ok: false, error });
|
|
4019
4633
|
var stripIpv6Brackets = (hostname6) => hostname6.startsWith("[") && hostname6.endsWith("]") ? hostname6.slice(1, -1) : hostname6;
|
|
4020
|
-
var hashIdentity = (identity) =>
|
|
4634
|
+
var hashIdentity = (identity) => createHash3("sha256").update(identity).digest("hex").slice(0, 24);
|
|
4021
4635
|
var aliasLabel = (hostname6) => {
|
|
4022
4636
|
const label = hostname6.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
4023
4637
|
return label === "" ? "server" : label;
|
|
@@ -4086,8 +4700,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
|
|
|
4086
4700
|
var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
|
|
4087
4701
|
var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
|
|
4088
4702
|
var parseIdentityPath = (value) => {
|
|
4089
|
-
const resolved =
|
|
4090
|
-
value.startsWith("~/") ?
|
|
4703
|
+
const resolved = path9.resolve(
|
|
4704
|
+
value.startsWith("~/") ? path9.join(os5.homedir(), value.slice(2)) : value
|
|
4091
4705
|
);
|
|
4092
4706
|
if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
|
|
4093
4707
|
return failure(
|
|
@@ -4222,7 +4836,7 @@ var workspaceSshPublicKeyFingerprint = (publicKey) => {
|
|
|
4222
4836
|
return failure(new WorkspaceSshError("key", "SSH public key has no encoded key data."));
|
|
4223
4837
|
}
|
|
4224
4838
|
try {
|
|
4225
|
-
const digest =
|
|
4839
|
+
const digest = createHash3("sha256").update(Buffer.from(encoded, "base64")).digest("base64");
|
|
4226
4840
|
return success(`SHA256:${digest.replace(/=+$/, "")}`);
|
|
4227
4841
|
} catch (cause) {
|
|
4228
4842
|
return failure(
|
|
@@ -4233,7 +4847,7 @@ var workspaceSshPublicKeyFingerprint = (publicKey) => {
|
|
|
4233
4847
|
var publicKeyIdentity = (publicKey) => publicKey.trim().split(/\s+/).slice(0, 2).join(" ");
|
|
4234
4848
|
var agentKeys = () => {
|
|
4235
4849
|
if (!process.env["SSH_AUTH_SOCK"]) return [];
|
|
4236
|
-
const listed =
|
|
4850
|
+
const listed = spawnSync2("ssh-add", ["-L"], {
|
|
4237
4851
|
encoding: "utf8",
|
|
4238
4852
|
timeout: 3e3,
|
|
4239
4853
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4243,7 +4857,7 @@ var agentKeys = () => {
|
|
|
4243
4857
|
};
|
|
4244
4858
|
var readText = (file) => {
|
|
4245
4859
|
try {
|
|
4246
|
-
return success(
|
|
4860
|
+
return success(fs9.readFileSync(file, "utf8"));
|
|
4247
4861
|
} catch (cause) {
|
|
4248
4862
|
const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
|
|
4249
4863
|
return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
|
|
@@ -4270,7 +4884,7 @@ var readRequiredKey = (keyPath, source) => {
|
|
|
4270
4884
|
const privatePath = publicPath.slice(0, -".pub".length);
|
|
4271
4885
|
const read = readText(publicPath);
|
|
4272
4886
|
if (!read.ok) return read;
|
|
4273
|
-
const derived =
|
|
4887
|
+
const derived = spawnSync2("ssh-keygen", ["-y", "-P", "", "-f", privatePath], {
|
|
4274
4888
|
encoding: "utf8",
|
|
4275
4889
|
timeout: 3e3,
|
|
4276
4890
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4301,19 +4915,19 @@ var readRequiredKey = (keyPath, source) => {
|
|
|
4301
4915
|
var readOptionalKey = (keyPath) => {
|
|
4302
4916
|
const parsed = parseIdentityPath(keyPath);
|
|
4303
4917
|
if (!parsed.ok) return parsed;
|
|
4304
|
-
if (!
|
|
4918
|
+
if (!fs9.existsSync(parsed.value) && !fs9.existsSync(publicKeyPath(parsed.value)))
|
|
4305
4919
|
return success(null);
|
|
4306
4920
|
return readRequiredKey(parsed.value, "existing");
|
|
4307
4921
|
};
|
|
4308
4922
|
var pinAgentKey = (configHome, publicKey) => {
|
|
4309
|
-
const identityFile =
|
|
4923
|
+
const identityFile = path9.resolve(
|
|
4310
4924
|
configHome,
|
|
4311
4925
|
"ssh",
|
|
4312
4926
|
`agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
|
|
4313
4927
|
);
|
|
4314
4928
|
try {
|
|
4315
|
-
|
|
4316
|
-
|
|
4929
|
+
fs9.mkdirSync(path9.dirname(identityFile), { recursive: true, mode: 448 });
|
|
4930
|
+
fs9.writeFileSync(identityFile, `${publicKey}
|
|
4317
4931
|
`, { mode: 384 });
|
|
4318
4932
|
return success({ publicKey, identityFile, source: "agent" });
|
|
4319
4933
|
} catch (cause) {
|
|
@@ -4324,15 +4938,15 @@ var pinAgentKey = (configHome, publicKey) => {
|
|
|
4324
4938
|
};
|
|
4325
4939
|
var generateDedicatedKey = (privatePath) => {
|
|
4326
4940
|
try {
|
|
4327
|
-
|
|
4941
|
+
fs9.mkdirSync(path9.dirname(privatePath), { recursive: true, mode: 448 });
|
|
4328
4942
|
} catch (cause) {
|
|
4329
4943
|
return failure(
|
|
4330
|
-
new WorkspaceSshError("key", `Could not create ${
|
|
4944
|
+
new WorkspaceSshError("key", `Could not create ${path9.dirname(privatePath)}.`, cause)
|
|
4331
4945
|
);
|
|
4332
4946
|
}
|
|
4333
|
-
const generated =
|
|
4947
|
+
const generated = spawnSync2(
|
|
4334
4948
|
"ssh-keygen",
|
|
4335
|
-
["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${
|
|
4949
|
+
["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os5.hostname()}`],
|
|
4336
4950
|
{ encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
|
|
4337
4951
|
);
|
|
4338
4952
|
if (generated.status !== 0) {
|
|
@@ -4356,7 +4970,7 @@ var generateDedicatedKey = (privatePath) => {
|
|
|
4356
4970
|
return fresh;
|
|
4357
4971
|
};
|
|
4358
4972
|
var dedicatedKey = (configHome, create) => {
|
|
4359
|
-
const privatePath =
|
|
4973
|
+
const privatePath = path9.join(configHome, "ssh", "id_ed25519");
|
|
4360
4974
|
const existing = readOptionalKey(privatePath);
|
|
4361
4975
|
if (existing.ok === false) return failure(existing.error);
|
|
4362
4976
|
if (existing.value !== null || !create) return existing;
|
|
@@ -4404,10 +5018,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
|
|
|
4404
5018
|
const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
|
|
4405
5019
|
if (reconciled.ok === false) return failure(reconciled.error);
|
|
4406
5020
|
try {
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
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);
|
|
4411
5025
|
return success(void 0);
|
|
4412
5026
|
} catch (cause) {
|
|
4413
5027
|
return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
|
|
@@ -4416,23 +5030,23 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
|
|
|
4416
5030
|
|
|
4417
5031
|
// src/ssh-setup.ts
|
|
4418
5032
|
var ansi = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
4419
|
-
var
|
|
4420
|
-
var
|
|
5033
|
+
var dim4 = ansi("2");
|
|
5034
|
+
var green4 = ansi("32");
|
|
4421
5035
|
var warn = ansi("33");
|
|
4422
|
-
var
|
|
4423
|
-
var sshConfigPath = () =>
|
|
5036
|
+
var say4 = (line) => console.log(line);
|
|
5037
|
+
var sshConfigPath = () => path10.join(os6.homedir(), ".ssh", "config");
|
|
4424
5038
|
var flagValue = (args, flag) => {
|
|
4425
5039
|
const index = args.indexOf(flag);
|
|
4426
5040
|
return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
|
|
4427
5041
|
};
|
|
4428
5042
|
var showFailure = (message) => {
|
|
4429
|
-
|
|
5043
|
+
say4(`mend: ${message}`);
|
|
4430
5044
|
process.exitCode = 1;
|
|
4431
5045
|
};
|
|
4432
5046
|
var showStatus = async (api2, cliHome, serverUrl, args) => {
|
|
4433
5047
|
const view = await api2("GET", "/workspace-ssh");
|
|
4434
5048
|
if (view.gateway === null) {
|
|
4435
|
-
|
|
5049
|
+
say4(`workspace ssh ${warn("no gateway")} ${dim4("\xB7 this deployment exposes none")}`);
|
|
4436
5050
|
return;
|
|
4437
5051
|
}
|
|
4438
5052
|
const parsedTarget = parseWorkspaceSshTarget({
|
|
@@ -4455,16 +5069,16 @@ var showStatus = async (api2, cliHome, serverUrl, args) => {
|
|
|
4455
5069
|
key: picked.value,
|
|
4456
5070
|
registeredFingerprints: view.keys.map((key) => key.fingerprint)
|
|
4457
5071
|
});
|
|
4458
|
-
|
|
4459
|
-
`gateway ${parsedTarget.value.hostname}:${parsedTarget.value.port} ${
|
|
5072
|
+
say4(
|
|
5073
|
+
`gateway ${parsedTarget.value.hostname}:${parsedTarget.value.port} ${dim4(`\xB7 published for ${serverUrl}`)}`
|
|
4460
5074
|
);
|
|
4461
|
-
|
|
4462
|
-
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}`)}`
|
|
4463
5077
|
);
|
|
4464
|
-
|
|
4465
|
-
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")}`
|
|
4466
5080
|
);
|
|
4467
|
-
|
|
5081
|
+
say4(
|
|
4468
5082
|
"host trust not checked \xB7 status checks config and client-key registration, not an SSH connection"
|
|
4469
5083
|
);
|
|
4470
5084
|
};
|
|
@@ -4490,7 +5104,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
|
|
|
4490
5104
|
if (picked.value === null) return showFailure("No workspace SSH key is available.");
|
|
4491
5105
|
const registered = await api2("POST", "/workspace-ssh/keys", {
|
|
4492
5106
|
publicKey: picked.value.publicKey,
|
|
4493
|
-
name:
|
|
5107
|
+
name: os6.hostname()
|
|
4494
5108
|
});
|
|
4495
5109
|
const sourceLabel = {
|
|
4496
5110
|
explicit: "from --key",
|
|
@@ -4498,22 +5112,22 @@ var setup = async (api2, cliHome, serverUrl, args) => {
|
|
|
4498
5112
|
existing: "existing selected key",
|
|
4499
5113
|
generated: "generated dedicated key"
|
|
4500
5114
|
}[picked.value.source];
|
|
4501
|
-
|
|
5115
|
+
say4(`key ${green4("\u25CF")} ${registered.fingerprint} ${dim4(`\xB7 ${sourceLabel}`)}`);
|
|
4502
5116
|
const written = writeWorkspaceSshConfig(
|
|
4503
5117
|
sshConfigPath(),
|
|
4504
5118
|
parsedTarget.value,
|
|
4505
5119
|
picked.value.identityFile
|
|
4506
5120
|
);
|
|
4507
5121
|
if (written.ok === false) return showFailure(written.error.message);
|
|
4508
|
-
|
|
4509
|
-
`ssh config ${
|
|
5122
|
+
say4(
|
|
5123
|
+
`ssh config ${green4("\u25CF")} Host ${parsedTarget.value.alias} ${dim4(`\xB7 ${sshConfigPath()}`)}`
|
|
4510
5124
|
);
|
|
4511
|
-
|
|
5125
|
+
say4(
|
|
4512
5126
|
"host trust not checked \xB7 SSH verifies the gateway when you connect; setup does not replace known_hosts entries"
|
|
4513
5127
|
);
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
`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")}`
|
|
4517
5131
|
);
|
|
4518
5132
|
};
|
|
4519
5133
|
var sshCommand = async (args, api2, cliHome, serverUrl) => {
|
|
@@ -4532,8 +5146,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
|
|
|
4532
5146
|
};
|
|
4533
5147
|
|
|
4534
5148
|
// src/uninstall.ts
|
|
4535
|
-
import * as
|
|
4536
|
-
import * as
|
|
5149
|
+
import * as fs10 from "node:fs";
|
|
5150
|
+
import * as path11 from "node:path";
|
|
4537
5151
|
var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
|
|
4538
5152
|
var parseUninstallArgs = (args) => {
|
|
4539
5153
|
let scope = null;
|
|
@@ -4555,14 +5169,14 @@ var parseUninstallArgs = (args) => {
|
|
|
4555
5169
|
var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
|
|
4556
5170
|
var countEntries = (directory) => {
|
|
4557
5171
|
try {
|
|
4558
|
-
return
|
|
5172
|
+
return fs10.readdirSync(directory).length;
|
|
4559
5173
|
} catch {
|
|
4560
5174
|
return 0;
|
|
4561
5175
|
}
|
|
4562
5176
|
};
|
|
4563
5177
|
var exists = (file) => {
|
|
4564
5178
|
try {
|
|
4565
|
-
|
|
5179
|
+
fs10.lstatSync(file);
|
|
4566
5180
|
return true;
|
|
4567
5181
|
} catch {
|
|
4568
5182
|
return false;
|
|
@@ -4571,7 +5185,7 @@ var exists = (file) => {
|
|
|
4571
5185
|
var managedBlockCount = (sshConfigFile) => {
|
|
4572
5186
|
let config;
|
|
4573
5187
|
try {
|
|
4574
|
-
config =
|
|
5188
|
+
config = fs10.readFileSync(sshConfigFile, "utf8");
|
|
4575
5189
|
} catch {
|
|
4576
5190
|
return 0;
|
|
4577
5191
|
}
|
|
@@ -4595,27 +5209,27 @@ var describeUninstall = async (runtime, scope) => {
|
|
|
4595
5209
|
if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
|
|
4596
5210
|
server = "none";
|
|
4597
5211
|
} else if (read.value === null) {
|
|
4598
|
-
server = exists(
|
|
5212
|
+
server = exists(path11.join(configDir, "identity.env")) ? {
|
|
4599
5213
|
version: "(no active generation)",
|
|
4600
5214
|
appUrl: "",
|
|
4601
5215
|
dockerContext: "",
|
|
4602
|
-
generations: countEntries(
|
|
4603
|
-
backups: countEntries(
|
|
5216
|
+
generations: countEntries(path11.join(configDir, "generations")),
|
|
5217
|
+
backups: countEntries(path11.join(configDir, "backups"))
|
|
4604
5218
|
} : "none";
|
|
4605
5219
|
} else {
|
|
4606
5220
|
server = {
|
|
4607
5221
|
version: read.value.config.serverVersion,
|
|
4608
5222
|
appUrl: read.value.config.appUrl,
|
|
4609
5223
|
dockerContext: read.value.config.dockerContext,
|
|
4610
|
-
generations: countEntries(
|
|
4611
|
-
backups: countEntries(
|
|
5224
|
+
generations: countEntries(path11.join(configDir, "generations")),
|
|
5225
|
+
backups: countEntries(path11.join(configDir, "backups"))
|
|
4612
5226
|
};
|
|
4613
5227
|
}
|
|
4614
5228
|
}
|
|
4615
5229
|
let home = null;
|
|
4616
5230
|
if (scope !== "server") {
|
|
4617
|
-
const cliConfig =
|
|
4618
|
-
const sshDirectory =
|
|
5231
|
+
const cliConfig = path11.join(runtime.cliHome, "cli.json");
|
|
5232
|
+
const sshDirectory = path11.join(runtime.cliHome, "ssh");
|
|
4619
5233
|
home = {
|
|
4620
5234
|
cliConfig: exists(cliConfig) ? cliConfig : null,
|
|
4621
5235
|
sshDirectory: exists(sshDirectory) ? sshDirectory : null,
|
|
@@ -4747,7 +5361,7 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
4747
5361
|
}
|
|
4748
5362
|
}
|
|
4749
5363
|
for (const name of SERVER_FILES) {
|
|
4750
|
-
|
|
5364
|
+
fs10.rmSync(path11.join(configDir, name), { recursive: true, force: true });
|
|
4751
5365
|
}
|
|
4752
5366
|
server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
|
|
4753
5367
|
},
|
|
@@ -4758,20 +5372,20 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
4758
5372
|
var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
4759
5373
|
const { server } = runtime;
|
|
4760
5374
|
if (plan.cliConfig !== null) {
|
|
4761
|
-
|
|
5375
|
+
fs10.rmSync(plan.cliConfig, { force: true });
|
|
4762
5376
|
server.writeLine(`removed ${plan.cliConfig}`);
|
|
4763
5377
|
}
|
|
4764
5378
|
if (plan.sshDirectory !== null) {
|
|
4765
|
-
|
|
5379
|
+
fs10.rmSync(plan.sshDirectory, { recursive: true, force: true });
|
|
4766
5380
|
server.writeLine(`removed ${plan.sshDirectory}`);
|
|
4767
5381
|
}
|
|
4768
5382
|
if (plan.managedSshBlocks > 0) {
|
|
4769
5383
|
try {
|
|
4770
|
-
const before =
|
|
5384
|
+
const before = fs10.readFileSync(runtime.sshConfigFile, "utf8");
|
|
4771
5385
|
const stripped = stripManagedWorkspaceSshBlocks(before);
|
|
4772
5386
|
if (!stripped.ok) throw stripped.error;
|
|
4773
5387
|
if (stripped.value.removed > 0) {
|
|
4774
|
-
|
|
5388
|
+
fs10.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
|
|
4775
5389
|
server.writeLine(
|
|
4776
5390
|
`removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
|
|
4777
5391
|
);
|
|
@@ -4784,12 +5398,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
|
4784
5398
|
}
|
|
4785
5399
|
let remaining = [];
|
|
4786
5400
|
try {
|
|
4787
|
-
remaining =
|
|
5401
|
+
remaining = fs10.readdirSync(runtime.cliHome);
|
|
4788
5402
|
} catch {
|
|
4789
5403
|
return;
|
|
4790
5404
|
}
|
|
4791
5405
|
if (remaining.length === 0) {
|
|
4792
|
-
|
|
5406
|
+
fs10.rmdirSync(runtime.cliHome);
|
|
4793
5407
|
server.writeLine(`removed ${runtime.cliHome}`);
|
|
4794
5408
|
} else {
|
|
4795
5409
|
leftovers.push(
|
|
@@ -4817,24 +5431,57 @@ var executeUninstall = async (runtime, plan) => {
|
|
|
4817
5431
|
return { failures, leftovers };
|
|
4818
5432
|
};
|
|
4819
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
|
+
|
|
4820
5467
|
// src/main.ts
|
|
4821
5468
|
var mendCliHome = () => {
|
|
4822
5469
|
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
4823
|
-
const preferred =
|
|
4824
|
-
xdg === void 0 || xdg === "" ?
|
|
5470
|
+
const preferred = path12.join(
|
|
5471
|
+
xdg === void 0 || xdg === "" ? path12.join(os7.homedir(), ".config") : xdg,
|
|
4825
5472
|
"mend"
|
|
4826
5473
|
);
|
|
4827
|
-
const legacy =
|
|
4828
|
-
return !
|
|
5474
|
+
const legacy = path12.join(os7.homedir(), ".mend");
|
|
5475
|
+
return !fs11.existsSync(preferred) && fs11.existsSync(legacy) ? legacy : preferred;
|
|
4829
5476
|
};
|
|
4830
|
-
var CONFIG_PATH =
|
|
5477
|
+
var CONFIG_PATH = path12.join(mendCliHome(), "cli.json");
|
|
4831
5478
|
var loadConfig = () => {
|
|
4832
5479
|
let fileConfig = {};
|
|
4833
|
-
if (
|
|
5480
|
+
if (fs11.existsSync(CONFIG_PATH)) {
|
|
4834
5481
|
try {
|
|
4835
|
-
fileConfig = JSON.parse(
|
|
5482
|
+
fileConfig = JSON.parse(fs11.readFileSync(CONFIG_PATH, "utf8"));
|
|
4836
5483
|
} catch {
|
|
4837
|
-
|
|
5484
|
+
fail5(`could not parse ${CONFIG_PATH}`);
|
|
4838
5485
|
}
|
|
4839
5486
|
}
|
|
4840
5487
|
const configuredUrl = process.env["MEND_URL"] ?? fileConfig.url ?? null;
|
|
@@ -4845,7 +5492,7 @@ var loadConfig = () => {
|
|
|
4845
5492
|
deviceId: fileConfig.deviceId ?? null
|
|
4846
5493
|
};
|
|
4847
5494
|
};
|
|
4848
|
-
var
|
|
5495
|
+
var fail5 = (message) => {
|
|
4849
5496
|
process.stderr.write(`mend: ${message}
|
|
4850
5497
|
`);
|
|
4851
5498
|
process.exit(1);
|
|
@@ -4854,18 +5501,18 @@ var parseMendUrl = (value) => {
|
|
|
4854
5501
|
try {
|
|
4855
5502
|
return new URL(value);
|
|
4856
5503
|
} catch {
|
|
4857
|
-
return
|
|
5504
|
+
return fail5(`"${value}" is not a valid Mend URL`);
|
|
4858
5505
|
}
|
|
4859
5506
|
};
|
|
4860
|
-
var
|
|
4861
|
-
var
|
|
4862
|
-
var
|
|
4863
|
-
var amber =
|
|
4864
|
-
var cobalt2 =
|
|
4865
|
-
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}
|
|
4866
5513
|
`);
|
|
4867
5514
|
var detachKeyEnabled = process.env["MEND_DETACH_KEY"] !== "none";
|
|
4868
|
-
var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${
|
|
5515
|
+
var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${dim5("Ctrl+]")}` : "";
|
|
4869
5516
|
var herdrAgentOf = (harness) => {
|
|
4870
5517
|
if (harness === "codex" || harness === "claude" || harness === "opencode") return harness;
|
|
4871
5518
|
return null;
|
|
@@ -4933,9 +5580,42 @@ var api = async (config, method, route, body) => {
|
|
|
4933
5580
|
try {
|
|
4934
5581
|
return await request(config, method, route, body);
|
|
4935
5582
|
} catch (error) {
|
|
4936
|
-
return
|
|
5583
|
+
return fail5(error instanceof Error ? error.message : String(error));
|
|
4937
5584
|
}
|
|
4938
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
|
+
});
|
|
4939
5619
|
var boundApi = (config) => (method, route, body) => api(config, method, route, body);
|
|
4940
5620
|
var withSpinner = async (label, work) => {
|
|
4941
5621
|
if (process.stdout.isTTY !== true) return work;
|
|
@@ -4944,7 +5624,7 @@ var withSpinner = async (label, work) => {
|
|
|
4944
5624
|
let frame = 0;
|
|
4945
5625
|
const timer = setInterval(() => {
|
|
4946
5626
|
const seconds = Math.round((Date.now() - started) / 1e3);
|
|
4947
|
-
process.stdout.write(`\r ${frames[frame % frames.length]} ${label} ${
|
|
5627
|
+
process.stdout.write(`\r ${frames[frame % frames.length]} ${label} ${dim5(`${seconds}s`)} `);
|
|
4948
5628
|
frame += 1;
|
|
4949
5629
|
}, 120);
|
|
4950
5630
|
try {
|
|
@@ -4958,66 +5638,74 @@ var adopt = async (config, args) => {
|
|
|
4958
5638
|
const nameFlag = args.indexOf("--name");
|
|
4959
5639
|
const name = nameFlag !== -1 && args[nameFlag + 1] !== void 0 ? String(args[nameFlag + 1]) : null;
|
|
4960
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";
|
|
4961
5645
|
const positional = args.filter(
|
|
4962
5646
|
(a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1) && (authFlagIndex === -1 || i !== authFlagIndex + 1)
|
|
4963
5647
|
);
|
|
4964
5648
|
const source = positional[0] ?? gitOriginUrl(process.cwd());
|
|
4965
5649
|
if (source === null) {
|
|
4966
|
-
return
|
|
5650
|
+
return fail5(
|
|
4967
5651
|
"No Git URL was given and this repository has no origin. Run mend adopt <git-url>."
|
|
4968
5652
|
);
|
|
4969
5653
|
}
|
|
4970
5654
|
const sourceIssue = repositoryCloneUrlIssue(source);
|
|
4971
|
-
if (sourceIssue !== null) return
|
|
4972
|
-
const projectName = name ?? normalizeProjectName(
|
|
5655
|
+
if (sourceIssue !== null) return fail5(sourceIssue);
|
|
5656
|
+
const projectName = name ?? normalizeProjectName(path12.basename(source, ".git"));
|
|
4973
5657
|
const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
|
|
4974
5658
|
if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
|
|
4975
|
-
return
|
|
5659
|
+
return fail5(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
|
|
4976
5660
|
}
|
|
4977
5661
|
const project2 = await api(config, "POST", "/projects", {
|
|
4978
5662
|
name: projectName,
|
|
4979
5663
|
source,
|
|
5664
|
+
visibility,
|
|
4980
5665
|
...auth === null ? {} : { gitAuthMode: auth }
|
|
4981
5666
|
});
|
|
4982
|
-
|
|
4983
|
-
|
|
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
|
+
);
|
|
4984
5672
|
if (project2.gitAuthMode === "mend-key") {
|
|
4985
|
-
|
|
5673
|
+
say5(`${dim5(" git auth")} mend key ${dim5("(your Mend key signed this clone)")}`);
|
|
4986
5674
|
} else if (project2.gitAuthMode === "bridge") {
|
|
4987
|
-
|
|
5675
|
+
say5(`${dim5(" git auth")} bridge ${dim5("(signed through the connected `mend keys share`)")}`);
|
|
4988
5676
|
}
|
|
4989
|
-
|
|
4990
|
-
`${
|
|
5677
|
+
say5(
|
|
5678
|
+
`${dim5(" sessions start with:")} mend codex ${dim5("(from anywhere \u2014")} --project ${project2.name}${dim5(")")}`
|
|
4991
5679
|
);
|
|
4992
5680
|
};
|
|
4993
5681
|
var findProject = async (config, explicit, adoptCwd = false) => {
|
|
4994
5682
|
const projects = await api(config, "GET", "/projects");
|
|
4995
5683
|
if (explicit !== null) {
|
|
4996
5684
|
const named = projects.find((p) => p.name === explicit);
|
|
4997
|
-
if (named === void 0) return
|
|
5685
|
+
if (named === void 0) return fail5(`no adopted project named "${explicit}"`);
|
|
4998
5686
|
return named;
|
|
4999
5687
|
}
|
|
5000
5688
|
const cwd = process.cwd();
|
|
5001
5689
|
const project2 = matchProjectByCwd(projects, cwdFacts(cwd));
|
|
5002
5690
|
if (project2 !== void 0) return project2;
|
|
5003
5691
|
if (!adoptCwd) {
|
|
5004
|
-
return
|
|
5692
|
+
return fail5(
|
|
5005
5693
|
`no adopted project matches ${cwd} \u2014 run "mend adopt" here first, or name one with --project`
|
|
5006
5694
|
);
|
|
5007
5695
|
}
|
|
5008
5696
|
const facts = cwdFacts(cwd);
|
|
5009
5697
|
if (facts.repoRoot === null || facts.originUrl === null) {
|
|
5010
|
-
return
|
|
5698
|
+
return fail5(
|
|
5011
5699
|
`${cwd} does not match an adopted project with a Git origin \u2014 run "mend adopt <git-url>" first`
|
|
5012
5700
|
);
|
|
5013
5701
|
}
|
|
5014
5702
|
const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
|
|
5015
|
-
if (sourceIssue !== null) return
|
|
5703
|
+
if (sourceIssue !== null) return fail5(sourceIssue);
|
|
5016
5704
|
const adopted = await api(config, "POST", "/projects", {
|
|
5017
|
-
name: normalizeProjectName(
|
|
5705
|
+
name: normalizeProjectName(path12.basename(facts.repoRoot)),
|
|
5018
5706
|
source: facts.originUrl
|
|
5019
5707
|
});
|
|
5020
|
-
|
|
5708
|
+
say5(`${green5("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim5(adopted.storePath)}`);
|
|
5021
5709
|
return adopted;
|
|
5022
5710
|
};
|
|
5023
5711
|
var resolvedBackgroundSessions = async (config, project2) => {
|
|
@@ -5039,7 +5727,7 @@ var askWorktreeName = async () => {
|
|
|
5039
5727
|
const readline = await import("node:readline/promises");
|
|
5040
5728
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
5041
5729
|
try {
|
|
5042
|
-
const answer = (await rl.question(` worktree name ${
|
|
5730
|
+
const answer = (await rl.question(` worktree name ${dim5("(enter for auto)")}: `)).trim();
|
|
5043
5731
|
return answer === "" ? null : normalizeProjectName(answer);
|
|
5044
5732
|
} finally {
|
|
5045
5733
|
rl.close();
|
|
@@ -5047,35 +5735,35 @@ var askWorktreeName = async () => {
|
|
|
5047
5735
|
};
|
|
5048
5736
|
var launch = async (config, harness, args) => {
|
|
5049
5737
|
const parsed = parseLaunchArgs(args);
|
|
5050
|
-
if (parsed.error !== null) return
|
|
5738
|
+
if (parsed.error !== null) return fail5(parsed.error);
|
|
5051
5739
|
const structured = parsed.prompt !== null || parsed.model !== null || parsed.effort !== null || parsed.ask || parsed.fast;
|
|
5052
5740
|
if (harness === "run" && structured) {
|
|
5053
|
-
return
|
|
5741
|
+
return fail5(`mend run takes no prompt or harness flags \xB7 ${usageOf("run")}`);
|
|
5054
5742
|
}
|
|
5055
5743
|
if (harness === "run" && (parsed.detach || parsed.foreground)) {
|
|
5056
|
-
return
|
|
5744
|
+
return fail5(
|
|
5057
5745
|
"mend run takes no lifecycle flags \u2014 it tails the record; Ctrl+C stops watching, not the command"
|
|
5058
5746
|
);
|
|
5059
5747
|
}
|
|
5060
5748
|
const argv = harness === "run" ? parsed.custom : HARNESS_COMMANDS[harness] ?? [];
|
|
5061
5749
|
if (argv.length === 0) {
|
|
5062
|
-
return
|
|
5750
|
+
return fail5(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
|
|
5063
5751
|
}
|
|
5064
5752
|
const project2 = await findProject(config, parsed.project, true);
|
|
5065
|
-
|
|
5066
|
-
`${
|
|
5753
|
+
say5(
|
|
5754
|
+
`${green5("\u2713")} project ${project2.name} ${dim5(`\xB7 ${project2.defaultBranch}${parsed.project === null ? " \xB7 from cwd" : ""}`)}`
|
|
5067
5755
|
);
|
|
5068
5756
|
let worktreeName;
|
|
5069
5757
|
if (parsed.worktree !== null) {
|
|
5070
5758
|
const detail = await api(config, "GET", `/projects/${project2.id}`);
|
|
5071
5759
|
const existing = detail.worktrees ?? [];
|
|
5072
5760
|
if (detail.worktrees === void 0) {
|
|
5073
|
-
return
|
|
5761
|
+
return fail5("this server predates shared worktrees \u2014 use --name instead");
|
|
5074
5762
|
}
|
|
5075
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);
|
|
5076
5764
|
if (match === void 0) {
|
|
5077
5765
|
const names = existing.map((worktree) => worktree.name).join(", ");
|
|
5078
|
-
return
|
|
5766
|
+
return fail5(
|
|
5079
5767
|
`no worktree matches "${parsed.worktree}" in ${project2.name}${names === "" ? "" : ` \u2014 have: ${names}`}`
|
|
5080
5768
|
);
|
|
5081
5769
|
}
|
|
@@ -5092,8 +5780,8 @@ var launch = async (config, harness, args) => {
|
|
|
5092
5780
|
const members = (detail?.sessions ?? []).filter(
|
|
5093
5781
|
(candidate) => candidate.worktreeId === joined.id
|
|
5094
5782
|
).length;
|
|
5095
|
-
|
|
5096
|
-
`${
|
|
5783
|
+
say5(
|
|
5784
|
+
`${green5("\u2713")} joins worktree ${joined.name} ${dim5(`\xB7 ${members} session${members === 1 ? "" : "s"} \xB7 branch ${joined.branch}`)}`
|
|
5097
5785
|
);
|
|
5098
5786
|
}
|
|
5099
5787
|
}
|
|
@@ -5118,12 +5806,12 @@ var launch = async (config, harness, args) => {
|
|
|
5118
5806
|
base: parsed.base
|
|
5119
5807
|
});
|
|
5120
5808
|
createdSessionId = session.id;
|
|
5121
|
-
|
|
5809
|
+
say5(`${green5("\u2713")} worktree ${session.worktree} ${dim5(`\xB7 branch ${session.branch}`)}`);
|
|
5122
5810
|
const baseWord = session.baseRef === null ? "" : `${session.baseRef} `;
|
|
5123
|
-
|
|
5124
|
-
`${
|
|
5811
|
+
say5(
|
|
5812
|
+
`${green5("\u2713")} base ${baseWord}${dim5(session.baseSha.slice(0, 12))} \xB7 session ${dim5(session.id.slice(0, 8))}`
|
|
5125
5813
|
);
|
|
5126
|
-
|
|
5814
|
+
say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
5127
5815
|
if (harness === "run") {
|
|
5128
5816
|
return supervisedRun(config, session, argv);
|
|
5129
5817
|
}
|
|
@@ -5139,23 +5827,28 @@ var launch = async (config, harness, args) => {
|
|
|
5139
5827
|
api(config, "POST", `/sessions/${session.id}/launch`, launchBody)
|
|
5140
5828
|
);
|
|
5141
5829
|
if (lifecycle === "detach") {
|
|
5142
|
-
|
|
5143
|
-
|
|
5830
|
+
say5(`${green5("\u2713 recording")} \xB7 running detached`);
|
|
5831
|
+
say5(`${cobalt2(" attach")} \xB7 mend attach ${session.id.slice(0, 8)}`);
|
|
5144
5832
|
return;
|
|
5145
5833
|
}
|
|
5146
|
-
|
|
5147
|
-
|
|
5834
|
+
say5(`${green5("\u2713 recording")} \xB7 workspace mounts the worktree${detachHint()}`);
|
|
5835
|
+
say5("");
|
|
5148
5836
|
attachOwnsSignals = true;
|
|
5149
5837
|
await attachOrExit(config, session.id, session.harness, lifecycle);
|
|
5150
5838
|
exitAfterSessionEnd(config, session.id);
|
|
5151
5839
|
};
|
|
5152
5840
|
var attachTty = async (config, sessionId, harness, from, processId, options) => {
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
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
|
+
}
|
|
5159
5852
|
const ws = new WebSocket(url);
|
|
5160
5853
|
ws.binaryType = "arraybuffer";
|
|
5161
5854
|
try {
|
|
@@ -5299,44 +5992,44 @@ var stopSessionQuickly = async (config, sessionId) => {
|
|
|
5299
5992
|
var stopAndExit = async (config, sessionId) => {
|
|
5300
5993
|
const id8 = sessionId.slice(0, 8);
|
|
5301
5994
|
if (await stopSessionQuickly(config, sessionId)) {
|
|
5302
|
-
|
|
5303
|
-
|
|
5995
|
+
say5(`${green5("\u2713")} stopped \xB7 ${id8}`);
|
|
5996
|
+
say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
|
|
5304
5997
|
process.exit(0);
|
|
5305
5998
|
}
|
|
5306
|
-
|
|
5999
|
+
say5(`${amber("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
|
|
5307
6000
|
process.exit(1);
|
|
5308
6001
|
};
|
|
5309
6002
|
var finishAttach = async (config, sessionId, outcome, mode = "background") => {
|
|
5310
6003
|
const id8 = sessionId.slice(0, 8);
|
|
5311
6004
|
if (outcome === "unavailable") {
|
|
5312
|
-
return
|
|
6005
|
+
return fail5(`tty attach unavailable: could not connect to ${config.url}`);
|
|
5313
6006
|
}
|
|
5314
6007
|
if (outcome === "detached") {
|
|
5315
|
-
|
|
5316
|
-
|
|
6008
|
+
say5("");
|
|
6009
|
+
say5(`${amber("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
5317
6010
|
process.exit(0);
|
|
5318
6011
|
}
|
|
5319
6012
|
if (outcome === "interrupted") {
|
|
5320
|
-
|
|
6013
|
+
say5("");
|
|
5321
6014
|
if (mode === "foreground") return stopAndExit(config, sessionId);
|
|
5322
|
-
|
|
6015
|
+
say5(`${amber("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
5323
6016
|
process.exit(0);
|
|
5324
6017
|
}
|
|
5325
6018
|
if (outcome === "dropped") {
|
|
5326
|
-
|
|
6019
|
+
say5("");
|
|
5327
6020
|
if (mode === "foreground") {
|
|
5328
6021
|
let live;
|
|
5329
6022
|
try {
|
|
5330
6023
|
const detail = await request(config, "GET", `/sessions/${sessionId}`);
|
|
5331
6024
|
live = agentIsLive(detail.session, detail.currentAgent);
|
|
5332
6025
|
} catch {
|
|
5333
|
-
|
|
6026
|
+
say5(`${amber("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
|
|
5334
6027
|
process.exit(1);
|
|
5335
6028
|
}
|
|
5336
6029
|
if (!live) return;
|
|
5337
6030
|
return stopAndExit(config, sessionId);
|
|
5338
6031
|
}
|
|
5339
|
-
|
|
6032
|
+
say5(`${amber("disconnected")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
5340
6033
|
process.exit(0);
|
|
5341
6034
|
}
|
|
5342
6035
|
};
|
|
@@ -5347,9 +6040,9 @@ var attachOrExit = async (config, sessionId, harness, mode = "background") => fi
|
|
|
5347
6040
|
mode
|
|
5348
6041
|
);
|
|
5349
6042
|
var exitAfterSessionEnd = (config, sessionId) => {
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
6043
|
+
say5("");
|
|
6044
|
+
say5(`${green5("\u2713")} session ended`);
|
|
6045
|
+
say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
|
|
5353
6046
|
process.exit(0);
|
|
5354
6047
|
};
|
|
5355
6048
|
var attach = async (config, args) => {
|
|
@@ -5360,26 +6053,26 @@ var attach = async (config, args) => {
|
|
|
5360
6053
|
}
|
|
5361
6054
|
const sessions = await api(config, "GET", "/sessions");
|
|
5362
6055
|
const match = sessions.find((s) => s.id.startsWith(prefix));
|
|
5363
|
-
if (match === void 0) return
|
|
6056
|
+
if (match === void 0) return fail5(`no active session matches "${prefix}"`);
|
|
5364
6057
|
return attachPicked(config, match);
|
|
5365
6058
|
};
|
|
5366
6059
|
var attachPicked = async (config, session) => {
|
|
5367
|
-
|
|
5368
|
-
`${
|
|
6060
|
+
say5(
|
|
6061
|
+
`${green5("\u2713")} attaching to ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim5(session.id.slice(0, 8))}${detachHint()}`
|
|
5369
6062
|
);
|
|
5370
|
-
|
|
6063
|
+
say5("");
|
|
5371
6064
|
const outcome = await attachTty(config, session.id, session.harness, 0n, void 0, {
|
|
5372
6065
|
handleSignals: true
|
|
5373
6066
|
});
|
|
5374
6067
|
if (outcome === "unavailable") {
|
|
5375
6068
|
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
5376
6069
|
if (detail.currentAgent?.kind === "agent-protocol" && agentIsLive(detail.session, detail.currentAgent)) {
|
|
5377
|
-
|
|
6070
|
+
say5(`${amber("taking over")} from the protocol session`);
|
|
5378
6071
|
await withSpinner(
|
|
5379
6072
|
"reopening as a terminal \u2014 same conversation\u2026",
|
|
5380
6073
|
api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
|
|
5381
6074
|
);
|
|
5382
|
-
|
|
6075
|
+
say5("");
|
|
5383
6076
|
await attachOrExit(config, session.id, session.harness);
|
|
5384
6077
|
return exitAfterSessionEnd(config, session.id);
|
|
5385
6078
|
}
|
|
@@ -5398,7 +6091,7 @@ var stopCommand = async (config, args) => {
|
|
|
5398
6091
|
return;
|
|
5399
6092
|
}
|
|
5400
6093
|
if (!all && prefix === void 0) {
|
|
5401
|
-
return
|
|
6094
|
+
return fail5(usageOf("stop"));
|
|
5402
6095
|
}
|
|
5403
6096
|
const [sessions, projects] = await Promise.all([
|
|
5404
6097
|
api(config, "GET", "/sessions"),
|
|
@@ -5407,21 +6100,21 @@ var stopCommand = async (config, args) => {
|
|
|
5407
6100
|
let scoped = sessions;
|
|
5408
6101
|
if (projectName !== null) {
|
|
5409
6102
|
const project2 = projects.find((p) => p.name === projectName);
|
|
5410
|
-
if (project2 === void 0) return
|
|
6103
|
+
if (project2 === void 0) return fail5(`no project named "${projectName}"`);
|
|
5411
6104
|
scoped = sessions.filter((s) => s.projectId === project2.id);
|
|
5412
6105
|
}
|
|
5413
6106
|
let targets;
|
|
5414
6107
|
if (all) {
|
|
5415
6108
|
targets = scoped;
|
|
5416
6109
|
if (targets.length === 0) {
|
|
5417
|
-
|
|
6110
|
+
say5("no active sessions");
|
|
5418
6111
|
return;
|
|
5419
6112
|
}
|
|
5420
6113
|
} else {
|
|
5421
6114
|
const matches = scoped.filter((s) => s.id.startsWith(prefix ?? ""));
|
|
5422
|
-
if (matches.length === 0) return
|
|
6115
|
+
if (matches.length === 0) return fail5(`no active session matches "${prefix}"`);
|
|
5423
6116
|
if (matches.length > 1) {
|
|
5424
|
-
return
|
|
6117
|
+
return fail5(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
|
|
5425
6118
|
}
|
|
5426
6119
|
targets = matches;
|
|
5427
6120
|
}
|
|
@@ -5430,20 +6123,20 @@ var stopCommand = async (config, args) => {
|
|
|
5430
6123
|
var stopSessions = async (config, targets, summarise) => {
|
|
5431
6124
|
for (const session of targets) {
|
|
5432
6125
|
await api(config, "POST", `/sessions/${session.id}/stop`);
|
|
5433
|
-
|
|
5434
|
-
`${
|
|
6126
|
+
say5(
|
|
6127
|
+
`${green5("\u2713")} stopped \xB7 ${session.harness} \xB7 ${dim5(session.id.slice(0, 8))} \xB7 ${session.branch}`
|
|
5435
6128
|
);
|
|
5436
|
-
|
|
6129
|
+
say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
5437
6130
|
}
|
|
5438
6131
|
if (summarise) {
|
|
5439
|
-
|
|
6132
|
+
say5(`${green5("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
|
|
5440
6133
|
}
|
|
5441
6134
|
};
|
|
5442
6135
|
var pickSessionInteractively = async (rows) => {
|
|
5443
|
-
|
|
6136
|
+
say5(dim5("more than one live session \u2014 pick one:"));
|
|
5444
6137
|
rows.forEach((row, index) => {
|
|
5445
|
-
|
|
5446
|
-
` ${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)}`
|
|
5447
6140
|
);
|
|
5448
6141
|
});
|
|
5449
6142
|
const readline = await import("node:readline/promises");
|
|
@@ -5451,17 +6144,17 @@ var pickSessionInteractively = async (rows) => {
|
|
|
5451
6144
|
const answer = await rl.question(" session #: ");
|
|
5452
6145
|
rl.close();
|
|
5453
6146
|
const chosen = rows[Number(answer.trim()) - 1];
|
|
5454
|
-
if (chosen === void 0) return
|
|
6147
|
+
if (chosen === void 0) return fail5(`"${answer.trim()}" is not one of the choices`);
|
|
5455
6148
|
return chosen.session;
|
|
5456
6149
|
};
|
|
5457
6150
|
var resolveLiveSession = async (config, prefix, command) => {
|
|
5458
6151
|
const sessions = await api(config, "GET", "/sessions?retained=1");
|
|
5459
6152
|
if (prefix !== void 0) {
|
|
5460
6153
|
const matches = sessions.filter((s) => s.id.startsWith(prefix));
|
|
5461
|
-
if (matches.length === 0) return
|
|
6154
|
+
if (matches.length === 0) return fail5(`no live or retained session matches "${prefix}"`);
|
|
5462
6155
|
const exact = matches[0];
|
|
5463
6156
|
if (matches.length > 1 || exact === void 0) {
|
|
5464
|
-
return
|
|
6157
|
+
return fail5(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
|
|
5465
6158
|
}
|
|
5466
6159
|
return exact;
|
|
5467
6160
|
}
|
|
@@ -5470,11 +6163,11 @@ var resolveLiveSession = async (config, prefix, command) => {
|
|
|
5470
6163
|
const candidates = project2 === void 0 ? sessions : sessions.filter((s) => s.projectId === project2.id);
|
|
5471
6164
|
const only = candidates[0];
|
|
5472
6165
|
if (only === void 0) {
|
|
5473
|
-
return
|
|
6166
|
+
return fail5("no live or retained session \u2014 start one with mend codex|claude|opencode");
|
|
5474
6167
|
}
|
|
5475
6168
|
if (candidates.length === 1) return only;
|
|
5476
6169
|
if (process.stdin.isTTY !== true) {
|
|
5477
|
-
return
|
|
6170
|
+
return fail5(`several live sessions \u2014 name one: mend ${command} <session-id-prefix>`);
|
|
5478
6171
|
}
|
|
5479
6172
|
const nameById = new Map(projects.map((p) => [p.id, p.name]));
|
|
5480
6173
|
return pickSessionInteractively(
|
|
@@ -5487,26 +6180,26 @@ var resolveLiveSession = async (config, prefix, command) => {
|
|
|
5487
6180
|
var shellCommand = async (config, args) => {
|
|
5488
6181
|
const prefix = args.find((a) => !a.startsWith("--"));
|
|
5489
6182
|
const session = await resolveLiveSession(config, prefix, "shell");
|
|
5490
|
-
|
|
5491
|
-
`${
|
|
6183
|
+
say5(
|
|
6184
|
+
`${green5("\u2713")} shell in ${session.harness} session ${dim5(session.id.slice(0, 8))} \xB7 ${dim5(session.branch)}${detachHint()}`
|
|
5492
6185
|
);
|
|
5493
6186
|
const shellProcess = await withSpinner(
|
|
5494
6187
|
"opening a shell in the session workspace\u2026",
|
|
5495
6188
|
api(config, "POST", `/sessions/${session.id}/shell`)
|
|
5496
6189
|
);
|
|
5497
|
-
|
|
6190
|
+
say5("");
|
|
5498
6191
|
const outcome = await attachTty(config, session.id, "shell", 0n, shellProcess.id, {
|
|
5499
6192
|
handleSignals: true
|
|
5500
6193
|
});
|
|
5501
6194
|
if (outcome === "unavailable") {
|
|
5502
|
-
return
|
|
6195
|
+
return fail5(`tty attach unavailable: could not connect to ${config.url}`);
|
|
5503
6196
|
}
|
|
5504
|
-
|
|
6197
|
+
say5("");
|
|
5505
6198
|
if (outcome === "detached" || outcome === "interrupted" || outcome === "dropped") {
|
|
5506
|
-
|
|
6199
|
+
say5(`${amber("detached")} \u2014 the shell keeps running and holds the workspace open`);
|
|
5507
6200
|
process.exit(0);
|
|
5508
6201
|
}
|
|
5509
|
-
|
|
6202
|
+
say5(`${green5("\u2713")} shell ended`);
|
|
5510
6203
|
process.exit(0);
|
|
5511
6204
|
};
|
|
5512
6205
|
var flattenService = (view) => {
|
|
@@ -5547,30 +6240,30 @@ var serviceUrl = (service) => service.browserUrl ?? (service.authority === null
|
|
|
5547
6240
|
var printServiceAccess = (config, service, tunneling = false) => {
|
|
5548
6241
|
const gate = "no Mend sign-in on this port \u2014 network reach is the only gate";
|
|
5549
6242
|
if (serverIsLocal(config)) {
|
|
5550
|
-
if (service.authority !== null)
|
|
6243
|
+
if (service.authority !== null) say5(` ${cobalt2(serviceUrl(service))} ${dim5(gate)}`);
|
|
5551
6244
|
return;
|
|
5552
6245
|
}
|
|
5553
6246
|
if (service.protocol === "udp") {
|
|
5554
6247
|
if (service.authority !== null) {
|
|
5555
|
-
|
|
6248
|
+
say5(` ${cobalt2(serviceUrl(service))} ${dim5(`on the server's network \xB7 ${gate}`)}`);
|
|
5556
6249
|
}
|
|
5557
6250
|
return;
|
|
5558
6251
|
}
|
|
5559
6252
|
if (!tunneling) {
|
|
5560
6253
|
const local = service.hostPort ?? service.workspacePort;
|
|
5561
|
-
|
|
5562
|
-
` ${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`)}`
|
|
5563
6256
|
);
|
|
5564
6257
|
}
|
|
5565
6258
|
if (service.authority !== null) {
|
|
5566
|
-
|
|
6259
|
+
say5(` ${dim5(`server-side listener ${service.authority} \xB7 ${gate}`)}`);
|
|
5567
6260
|
}
|
|
5568
6261
|
};
|
|
5569
6262
|
var printWorkspaceTtlFailure = (service) => {
|
|
5570
6263
|
if (service.workspaceTtlRenewalError === null) return;
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
6264
|
+
say5(amber(` workspace TTL renewal failed \xB7 ${service.workspaceTtlRenewalError}`));
|
|
6265
|
+
say5(
|
|
6266
|
+
dim5(
|
|
5574
6267
|
` last renewed ${service.workspaceTtlRenewedAt ?? "unknown"} \xB7 known expiry ${service.workspaceExpiresAt ?? "unknown"} \xB7 failed ${service.workspaceTtlRenewalFailedAt ?? "unknown"}`
|
|
5575
6268
|
)
|
|
5576
6269
|
);
|
|
@@ -5580,11 +6273,11 @@ var printServiceEndpoint = (config, service, tunneling = false) => {
|
|
|
5580
6273
|
printWorkspaceTtlFailure(service);
|
|
5581
6274
|
};
|
|
5582
6275
|
var printService = (config, service) => {
|
|
5583
|
-
const status = service.status === "reachable" ?
|
|
6276
|
+
const status = service.status === "reachable" ? green5(service.status) : amber(service.status);
|
|
5584
6277
|
const port = `:${service.workspacePort ?? "?"}${service.protocol === "udp" ? "/udp" : ""}`;
|
|
5585
6278
|
const statusPad = " ".repeat(Math.max(1, 12 - service.status.length));
|
|
5586
|
-
|
|
5587
|
-
`${(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))}`
|
|
5588
6281
|
);
|
|
5589
6282
|
printServiceAccess(config, service);
|
|
5590
6283
|
printWorkspaceTtlFailure(service);
|
|
@@ -5595,21 +6288,21 @@ var serviceAdd = async (config, args) => {
|
|
|
5595
6288
|
const protocol = args.includes("--udp") ? "udp" : "tcp";
|
|
5596
6289
|
const http = args.includes("--http");
|
|
5597
6290
|
const https = args.includes("--https");
|
|
5598
|
-
if (http && https) return
|
|
6291
|
+
if (http && https) return fail5("Choose either --http or --https, not both.");
|
|
5599
6292
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
5600
6293
|
if (protocol === "udp" && browserScheme !== null) {
|
|
5601
|
-
return
|
|
6294
|
+
return fail5("UDP Services cannot use --http or --https");
|
|
5602
6295
|
}
|
|
5603
6296
|
const positional = args.filter(
|
|
5604
6297
|
(a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1)
|
|
5605
6298
|
);
|
|
5606
6299
|
const portRaw = positional.find((a) => /^\d+$/.test(a));
|
|
5607
6300
|
if (portRaw === void 0) {
|
|
5608
|
-
return
|
|
6301
|
+
return fail5(usageOf("service add"));
|
|
5609
6302
|
}
|
|
5610
6303
|
const port = Number(portRaw);
|
|
5611
6304
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5612
|
-
return
|
|
6305
|
+
return fail5(`"${portRaw}" is not a port`);
|
|
5613
6306
|
}
|
|
5614
6307
|
const prefix = positional.find((a) => a !== portRaw);
|
|
5615
6308
|
const session = await resolveLiveSession(config, prefix, "service add");
|
|
@@ -5619,52 +6312,52 @@ var serviceAdd = async (config, args) => {
|
|
|
5619
6312
|
protocol,
|
|
5620
6313
|
browserScheme
|
|
5621
6314
|
});
|
|
5622
|
-
|
|
6315
|
+
say5(`${green5("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
|
|
5623
6316
|
printServiceEndpoint(config, service);
|
|
5624
6317
|
if (protocol === "udp") {
|
|
5625
|
-
|
|
6318
|
+
say5(dim5(` udp \u2014 a reply is the only reachability signal; silence just relays`));
|
|
5626
6319
|
} else if (service.status !== "reachable") {
|
|
5627
|
-
|
|
6320
|
+
say5(dim5(` nothing answered on :${port} yet \u2014 the URL goes live when something listens`));
|
|
5628
6321
|
}
|
|
5629
6322
|
};
|
|
5630
6323
|
var serviceList = async (config) => {
|
|
5631
6324
|
const services = await fetchServices(config);
|
|
5632
6325
|
if (services.length === 0) {
|
|
5633
|
-
|
|
6326
|
+
say5(dim5("no live services \u2014 mend service add <port> adopts a listening one"));
|
|
5634
6327
|
return;
|
|
5635
6328
|
}
|
|
5636
6329
|
for (const service of services) printService(config, service);
|
|
5637
6330
|
};
|
|
5638
6331
|
var serviceStop = async (config, args) => {
|
|
5639
6332
|
const needle = args.find((a) => !a.startsWith("--"));
|
|
5640
|
-
if (needle === void 0) return
|
|
6333
|
+
if (needle === void 0) return fail5(usageOf("service stop"));
|
|
5641
6334
|
const services = await fetchServices(config);
|
|
5642
6335
|
const matches = services.filter(
|
|
5643
6336
|
(service) => service.label === needle || service.id.startsWith(needle)
|
|
5644
6337
|
);
|
|
5645
|
-
if (matches.length === 0) return
|
|
6338
|
+
if (matches.length === 0) return fail5(`no live service matches "${needle}"`);
|
|
5646
6339
|
const match = matches[0];
|
|
5647
6340
|
if (matches.length > 1 || match === void 0) {
|
|
5648
|
-
return
|
|
6341
|
+
return fail5(`"${needle}" is ambiguous \u2014 use more of the id`);
|
|
5649
6342
|
}
|
|
5650
6343
|
const stoppedService = await mutateService(config, "POST", `/services/${match.id}/stop`);
|
|
5651
|
-
|
|
6344
|
+
say5(`${green5("\u2713")} stopped \xB7 ${stoppedService.label ?? stoppedService.id.slice(0, 8)}`);
|
|
5652
6345
|
};
|
|
5653
6346
|
var findLiveService = async (config, needle) => {
|
|
5654
6347
|
const services = await fetchServices(config);
|
|
5655
6348
|
const matches = services.filter(
|
|
5656
6349
|
(service) => service.label === needle || service.id.startsWith(needle)
|
|
5657
6350
|
);
|
|
5658
|
-
if (matches.length === 0) return
|
|
6351
|
+
if (matches.length === 0) return fail5(`no live service matches "${needle}"`);
|
|
5659
6352
|
const match = matches[0];
|
|
5660
6353
|
if (matches.length > 1 || match === void 0) {
|
|
5661
|
-
return
|
|
6354
|
+
return fail5(`"${needle}" is ambiguous \u2014 use more of the id`);
|
|
5662
6355
|
}
|
|
5663
6356
|
return match;
|
|
5664
6357
|
};
|
|
5665
6358
|
var willAutoConnect = (config, service, optOut) => !optOut && !serverIsLocal(config) && service.protocol !== "udp";
|
|
5666
6359
|
var autoConnect = async (config, service) => {
|
|
5667
|
-
|
|
6360
|
+
say5(dim5(" remote server \u2014 tunneling the port here \xB7 Ctrl-C stops the tunnel, not the Service"));
|
|
5668
6361
|
await tunnelServices(config, [service], null);
|
|
5669
6362
|
};
|
|
5670
6363
|
var serviceRun = async (config, args) => {
|
|
@@ -5673,7 +6366,7 @@ var serviceRun = async (config, args) => {
|
|
|
5673
6366
|
if (dashdash === -1) {
|
|
5674
6367
|
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
5675
6368
|
const name2 = positionals.at(-1);
|
|
5676
|
-
if (name2 === void 0) return
|
|
6369
|
+
if (name2 === void 0) return fail5(usage);
|
|
5677
6370
|
const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
|
|
5678
6371
|
const session2 = await resolveLiveSession(config, prefix2, "service run");
|
|
5679
6372
|
const recipes = await api(
|
|
@@ -5684,7 +6377,7 @@ var serviceRun = async (config, args) => {
|
|
|
5684
6377
|
const recipe = recipes.find((entry) => entry.name === name2);
|
|
5685
6378
|
if (recipe === void 0) {
|
|
5686
6379
|
const known = recipes.map((entry) => entry.name).join(", ");
|
|
5687
|
-
return
|
|
6380
|
+
return fail5(
|
|
5688
6381
|
recipes.length === 0 ? `no mend.toml recipes in this worktree \u2014 declare [service.${name2}] first` : `no recipe named "${name2}" \u2014 declared: ${known}`
|
|
5689
6382
|
);
|
|
5690
6383
|
}
|
|
@@ -5695,26 +6388,26 @@ var serviceRun = async (config, args) => {
|
|
|
5695
6388
|
})
|
|
5696
6389
|
);
|
|
5697
6390
|
const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect"));
|
|
5698
|
-
|
|
6391
|
+
say5(`${green5("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
|
|
5699
6392
|
printServiceEndpoint(config, service2, tunneling2);
|
|
5700
|
-
|
|
6393
|
+
say5(dim5(` logs: mend service logs ${service2.label ?? service2.id.slice(0, 8)}`));
|
|
5701
6394
|
if (tunneling2) await autoConnect(config, service2);
|
|
5702
6395
|
return;
|
|
5703
6396
|
}
|
|
5704
6397
|
const argv = args.slice(dashdash + 1);
|
|
5705
|
-
if (argv.length === 0) return
|
|
6398
|
+
if (argv.length === 0) return fail5(usage);
|
|
5706
6399
|
const head = args.slice(0, dashdash);
|
|
5707
6400
|
const portFlag = head.indexOf("--port");
|
|
5708
6401
|
const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
|
|
5709
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) return
|
|
6402
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return fail5(usage);
|
|
5710
6403
|
const nameFlag = head.indexOf("--name");
|
|
5711
6404
|
const name = nameFlag !== -1 && head[nameFlag + 1] !== void 0 ? String(head[nameFlag + 1]) : null;
|
|
5712
6405
|
const protocol = head.includes("--udp") ? "udp" : "tcp";
|
|
5713
6406
|
const http = head.includes("--http");
|
|
5714
6407
|
const https = head.includes("--https");
|
|
5715
|
-
if (http && https) return
|
|
6408
|
+
if (http && https) return fail5(usage);
|
|
5716
6409
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
5717
|
-
if (protocol === "udp" && browserScheme !== null) return
|
|
6410
|
+
if (protocol === "udp" && browserScheme !== null) return fail5(usage);
|
|
5718
6411
|
const prefix = head.find(
|
|
5719
6412
|
(a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
|
|
5720
6413
|
);
|
|
@@ -5730,9 +6423,9 @@ var serviceRun = async (config, args) => {
|
|
|
5730
6423
|
})
|
|
5731
6424
|
);
|
|
5732
6425
|
const tunneling = willAutoConnect(config, service, head.includes("--no-connect"));
|
|
5733
|
-
|
|
6426
|
+
say5(`${green5("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
|
|
5734
6427
|
printServiceEndpoint(config, service, tunneling);
|
|
5735
|
-
|
|
6428
|
+
say5(dim5(` logs: mend service logs ${service.label ?? service.id.slice(0, 8)}`));
|
|
5736
6429
|
if (tunneling) await autoConnect(config, service);
|
|
5737
6430
|
};
|
|
5738
6431
|
var serviceLogs = async (config, args) => {
|
|
@@ -5742,32 +6435,32 @@ var serviceLogs = async (config, args) => {
|
|
|
5742
6435
|
(argument, index) => !argument.startsWith("--") && index !== fromFlag + 1
|
|
5743
6436
|
);
|
|
5744
6437
|
if (needle === void 0 || from === void 0 || !/^(0|[1-9]\d*)$/.test(from)) {
|
|
5745
|
-
return
|
|
6438
|
+
return fail5(usageOf("service logs"));
|
|
5746
6439
|
}
|
|
5747
6440
|
const everything = await fetchServiceViews(config, true);
|
|
5748
6441
|
const matches = everything.filter(
|
|
5749
6442
|
(view2) => view2.service.name === needle || view2.service.id.startsWith(needle) || view2.attempts.some((attempt3) => attempt3.id.startsWith(needle))
|
|
5750
6443
|
);
|
|
5751
|
-
if (matches.length === 0) return
|
|
6444
|
+
if (matches.length === 0) return fail5(`no service matches "${needle}"`);
|
|
5752
6445
|
const view = matches.find((candidate) => {
|
|
5753
6446
|
const current = candidate.service.currentAttemptId === null ? null : candidate.attempts.find((attempt3) => attempt3.id === candidate.service.currentAttemptId);
|
|
5754
6447
|
return current !== null && current !== void 0 && current.exitedAt === null;
|
|
5755
6448
|
}) ?? matches[0];
|
|
5756
|
-
if (view === void 0) return
|
|
6449
|
+
if (view === void 0) return fail5(`no service matches "${needle}"`);
|
|
5757
6450
|
const currentAttempt = view.service.currentAttemptId === null ? null : view.attempts.find((candidate) => candidate.id === view.service.currentAttemptId) ?? null;
|
|
5758
6451
|
const attempt2 = view.attempts.find((candidate) => candidate.id.startsWith(needle)) ?? currentAttempt ?? view.attempts.findLast((candidate) => candidate.sealantSessionId !== null) ?? null;
|
|
5759
6452
|
if (attempt2?.sealantSessionId === null || attempt2 === null) {
|
|
5760
|
-
return
|
|
6453
|
+
return fail5(
|
|
5761
6454
|
`"${needle}" is an adopted port \u2014 no process of Mend's, no logs. mend service run supervises.`
|
|
5762
6455
|
);
|
|
5763
6456
|
}
|
|
5764
6457
|
const label = view.service.name;
|
|
5765
|
-
|
|
5766
|
-
|
|
6458
|
+
say5(
|
|
6459
|
+
dim5(
|
|
5767
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}`
|
|
5768
6461
|
)
|
|
5769
6462
|
);
|
|
5770
|
-
|
|
6463
|
+
say5("");
|
|
5771
6464
|
let cursor = from;
|
|
5772
6465
|
let telemetryReported = false;
|
|
5773
6466
|
for (; ; ) {
|
|
@@ -5777,7 +6470,7 @@ var serviceLogs = async (config, args) => {
|
|
|
5777
6470
|
`/processes/${attempt2.id}/logs?from=${encodeURIComponent(cursor)}&limit=256`
|
|
5778
6471
|
);
|
|
5779
6472
|
if (!telemetryReported) {
|
|
5780
|
-
|
|
6473
|
+
say5(dim5(`telemetry loss: ${page.telemetryLoss} \xB7 ${page.telemetryNote}`));
|
|
5781
6474
|
telemetryReported = true;
|
|
5782
6475
|
}
|
|
5783
6476
|
for (const chunk of page.chunks) {
|
|
@@ -5789,29 +6482,29 @@ var serviceLogs = async (config, args) => {
|
|
|
5789
6482
|
if (ended && !advanced) break;
|
|
5790
6483
|
if (!advanced) await new Promise((resolve4) => setTimeout(resolve4, 250));
|
|
5791
6484
|
}
|
|
5792
|
-
|
|
5793
|
-
|
|
6485
|
+
say5("");
|
|
6486
|
+
say5(dim5(`stream ended \xB7 next sequence ${cursor}`));
|
|
5794
6487
|
};
|
|
5795
6488
|
var serviceRestart = async (config, args) => {
|
|
5796
6489
|
const needle = args.find((a) => !a.startsWith("--"));
|
|
5797
|
-
if (needle === void 0) return
|
|
6490
|
+
if (needle === void 0) return fail5(usageOf("service restart"));
|
|
5798
6491
|
const service = await findLiveService(config, needle);
|
|
5799
6492
|
const restarted = await withSpinner(
|
|
5800
6493
|
`restarting ${service.label ?? service.id.slice(0, 8)}\u2026`,
|
|
5801
6494
|
mutateService(config, "POST", `/services/${service.id}/restart`)
|
|
5802
6495
|
);
|
|
5803
|
-
|
|
6496
|
+
say5(`${green5("\u2713")} restarted \xB7 ${restarted.status}`);
|
|
5804
6497
|
printServiceEndpoint(config, restarted);
|
|
5805
6498
|
};
|
|
5806
6499
|
var serviceInit = async (args) => {
|
|
5807
6500
|
const root = gitTopLevel(process.cwd()) ?? process.cwd();
|
|
5808
|
-
const target =
|
|
5809
|
-
if (
|
|
5810
|
-
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)`);
|
|
5811
6504
|
}
|
|
5812
|
-
const rootFiles =
|
|
6505
|
+
const rootFiles = fs11.readdirSync(root);
|
|
5813
6506
|
const proposals = [];
|
|
5814
|
-
const readRoot = (name) =>
|
|
6507
|
+
const readRoot = (name) => fs11.readFileSync(path12.join(root, name), "utf8");
|
|
5815
6508
|
if (rootFiles.includes("package.json")) {
|
|
5816
6509
|
proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
|
|
5817
6510
|
}
|
|
@@ -5821,13 +6514,13 @@ var serviceInit = async (args) => {
|
|
|
5821
6514
|
);
|
|
5822
6515
|
for (const glob of globs) {
|
|
5823
6516
|
const parent = glob.replace(/\/?\*+$/, "");
|
|
5824
|
-
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];
|
|
5825
6518
|
for (const dir of dirs) {
|
|
5826
|
-
const manifest =
|
|
5827
|
-
if (!
|
|
6519
|
+
const manifest = path12.join(root, dir, "package.json");
|
|
6520
|
+
if (!fs11.existsSync(manifest)) continue;
|
|
5828
6521
|
for (const proposal of proposeFromWorkspacePackage(
|
|
5829
|
-
|
|
5830
|
-
|
|
6522
|
+
path12.basename(dir),
|
|
6523
|
+
fs11.readFileSync(manifest, "utf8"),
|
|
5831
6524
|
rootFiles
|
|
5832
6525
|
)) {
|
|
5833
6526
|
if (!proposals.some((existing) => existing.name === proposal.name)) {
|
|
@@ -5849,58 +6542,60 @@ var serviceInit = async (args) => {
|
|
|
5849
6542
|
}
|
|
5850
6543
|
}
|
|
5851
6544
|
if (proposals.length === 0) {
|
|
5852
|
-
return
|
|
6545
|
+
return fail5(
|
|
5853
6546
|
"nothing to propose \u2014 no server-ish package.json script with a nameable port, no compose ports"
|
|
5854
6547
|
);
|
|
5855
6548
|
}
|
|
5856
6549
|
const toml = renderMendToml(proposals);
|
|
5857
|
-
|
|
5858
|
-
|
|
6550
|
+
say5(dim5(`proposed ${target}:`));
|
|
6551
|
+
say5("");
|
|
5859
6552
|
process.stdout.write(toml);
|
|
5860
|
-
|
|
6553
|
+
say5("");
|
|
5861
6554
|
if (!args.includes("--yes")) {
|
|
5862
6555
|
if (process.stdin.isTTY !== true) {
|
|
5863
|
-
return
|
|
6556
|
+
return fail5("non-interactive \u2014 pass --yes to write the file");
|
|
5864
6557
|
}
|
|
5865
6558
|
const readline = await import("node:readline/promises");
|
|
5866
6559
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
5867
6560
|
const answer = await rl.question("write it? [y/N] ");
|
|
5868
6561
|
rl.close();
|
|
5869
6562
|
if (answer.trim().toLowerCase() !== "y") {
|
|
5870
|
-
|
|
6563
|
+
say5(dim5("nothing written"));
|
|
5871
6564
|
return;
|
|
5872
6565
|
}
|
|
5873
6566
|
}
|
|
5874
|
-
|
|
5875
|
-
|
|
6567
|
+
fs11.writeFileSync(target, toml);
|
|
6568
|
+
say5(`${green5("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
|
|
5876
6569
|
};
|
|
5877
6570
|
var tunnelServices = async (config, services, portOverride) => {
|
|
5878
|
-
const tunnelUrl = (serviceId) => {
|
|
5879
|
-
const url = parseMendUrl(`${config.url}/api/service-tunnel`);
|
|
5880
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
5881
|
-
url.searchParams.set("service", serviceId);
|
|
5882
|
-
if (config.token !== null) url.searchParams.set("token", config.token);
|
|
5883
|
-
return url;
|
|
5884
|
-
};
|
|
6571
|
+
const tunnelUrl = (serviceId) => socketUrl(config, "service-tunnel", { service: serviceId });
|
|
5885
6572
|
for (const service of services) {
|
|
5886
6573
|
const port = portOverride ?? service.hostPort ?? service.workspacePort;
|
|
5887
6574
|
const server = net3.createServer((socket) => {
|
|
5888
6575
|
socket.pause();
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
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
|
+
);
|
|
5904
6599
|
});
|
|
5905
6600
|
await new Promise((resolve4, reject) => {
|
|
5906
6601
|
server.once("error", (error) => {
|
|
@@ -5911,12 +6606,12 @@ var tunnelServices = async (config, services, portOverride) => {
|
|
|
5911
6606
|
);
|
|
5912
6607
|
});
|
|
5913
6608
|
server.listen(port, "127.0.0.1", () => resolve4());
|
|
5914
|
-
}).catch((error) =>
|
|
5915
|
-
|
|
5916
|
-
`${
|
|
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})`)}`
|
|
5917
6612
|
);
|
|
5918
6613
|
}
|
|
5919
|
-
|
|
6614
|
+
say5(dim5(" connections are authenticated as you \xB7 Ctrl-C stops"));
|
|
5920
6615
|
await new Promise(() => {
|
|
5921
6616
|
});
|
|
5922
6617
|
};
|
|
@@ -5924,18 +6619,18 @@ var serviceConnect = async (config, args) => {
|
|
|
5924
6619
|
const portFlag = args.indexOf("--port");
|
|
5925
6620
|
const portOverride = portFlag === -1 ? null : Number(args[portFlag + 1]);
|
|
5926
6621
|
if (portOverride !== null && !Number.isInteger(portOverride)) {
|
|
5927
|
-
return
|
|
6622
|
+
return fail5("--port takes a port number");
|
|
5928
6623
|
}
|
|
5929
6624
|
const names = args.filter((a, i) => !a.startsWith("--") && i !== portFlag + 1);
|
|
5930
6625
|
const live = (await fetchServices(config)).filter((s) => s.protocol === "tcp");
|
|
5931
6626
|
const picked = names.length === 0 ? live : live.filter((s) => names.some((n) => s.label === n || s.id.startsWith(n)));
|
|
5932
6627
|
if (picked.length === 0) {
|
|
5933
|
-
return
|
|
6628
|
+
return fail5(
|
|
5934
6629
|
names.length === 0 ? "no live TCP services \u2014 mend service run starts one" : `no live TCP service matches "${names.join('", "')}"`
|
|
5935
6630
|
);
|
|
5936
6631
|
}
|
|
5937
6632
|
if (portOverride !== null && picked.length !== 1) {
|
|
5938
|
-
return
|
|
6633
|
+
return fail5("--port applies to exactly one service \u2014 name it");
|
|
5939
6634
|
}
|
|
5940
6635
|
await tunnelServices(config, picked, portOverride);
|
|
5941
6636
|
};
|
|
@@ -5964,23 +6659,23 @@ var serviceCommand = async (config, args) => {
|
|
|
5964
6659
|
}
|
|
5965
6660
|
};
|
|
5966
6661
|
var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
|
|
5967
|
-
var readIfExists = (file) =>
|
|
6662
|
+
var readIfExists = (file) => fs11.existsSync(file) ? fs11.readFileSync(file, "utf8") : null;
|
|
5968
6663
|
var localCredential = (provider) => {
|
|
5969
|
-
const home =
|
|
6664
|
+
const home = os7.homedir();
|
|
5970
6665
|
switch (provider) {
|
|
5971
6666
|
case "codex":
|
|
5972
6667
|
return readIfExists(
|
|
5973
|
-
|
|
6668
|
+
path12.join(process.env["CODEX_HOME"] ?? path12.join(home, ".codex"), "auth.json")
|
|
5974
6669
|
);
|
|
5975
6670
|
case "claude":
|
|
5976
6671
|
return readIfExists(
|
|
5977
|
-
|
|
5978
|
-
process.env["CLAUDE_CONFIG_DIR"] ??
|
|
6672
|
+
path12.join(
|
|
6673
|
+
process.env["CLAUDE_CONFIG_DIR"] ?? path12.join(home, ".claude"),
|
|
5979
6674
|
".credentials.json"
|
|
5980
6675
|
)
|
|
5981
6676
|
);
|
|
5982
6677
|
case "github": {
|
|
5983
|
-
const result =
|
|
6678
|
+
const result = spawnSync3("gh", ["auth", "token"], { encoding: "utf8" });
|
|
5984
6679
|
const token = result.status === 0 ? result.stdout.trim() : "";
|
|
5985
6680
|
return token === "" ? null : token;
|
|
5986
6681
|
}
|
|
@@ -6012,16 +6707,94 @@ var accountsCommand = async (config) => {
|
|
|
6012
6707
|
);
|
|
6013
6708
|
}
|
|
6014
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
|
+
};
|
|
6015
6788
|
var connectCommand = async (config, args) => {
|
|
6016
6789
|
const [providerArg, ...flags] = args;
|
|
6017
6790
|
if (!isProvider(providerArg)) {
|
|
6018
|
-
return
|
|
6791
|
+
return fail5(usageOf("connect"));
|
|
6019
6792
|
}
|
|
6020
6793
|
const provider = providerArg;
|
|
6021
6794
|
if (flags.includes("--remove")) {
|
|
6022
6795
|
const identity = await api(config, "GET", "/me/sealant");
|
|
6023
6796
|
const account2 = identity.accounts.find((row) => row.provider === provider);
|
|
6024
|
-
if (account2 === void 0) return
|
|
6797
|
+
if (account2 === void 0) return fail5(`${provider}: nothing connected`);
|
|
6025
6798
|
await api(config, "DELETE", `/me/sealant/accounts/${account2.id}`);
|
|
6026
6799
|
process.stdout.write(`${provider}: disconnected
|
|
6027
6800
|
`);
|
|
@@ -6029,35 +6802,60 @@ var connectCommand = async (config, args) => {
|
|
|
6029
6802
|
}
|
|
6030
6803
|
let secret;
|
|
6031
6804
|
if (flags.includes("--from-stdin")) {
|
|
6032
|
-
secret =
|
|
6033
|
-
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;
|
|
6034
6810
|
} else {
|
|
6035
6811
|
secret = localCredential(provider);
|
|
6036
6812
|
if (secret === null) {
|
|
6037
|
-
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
|
|
6038
|
-
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
|
+
);
|
|
6039
6822
|
}
|
|
6040
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
|
+
}
|
|
6041
6828
|
const account = await withSpinner(
|
|
6042
6829
|
`connecting ${provider}`,
|
|
6043
|
-
api(config, "POST", "/me/sealant/accounts", {
|
|
6830
|
+
api(config, "POST", "/me/sealant/accounts", {
|
|
6831
|
+
provider,
|
|
6832
|
+
secret: narrowed.secret
|
|
6833
|
+
})
|
|
6044
6834
|
);
|
|
6045
6835
|
process.stdout.write(`${accountLine(account)}
|
|
6046
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
|
+
}
|
|
6047
6845
|
};
|
|
6048
|
-
var
|
|
6846
|
+
var takeFlagValue4 = (args, flag) => {
|
|
6049
6847
|
const at = args.indexOf(flag);
|
|
6050
6848
|
return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
|
|
6051
6849
|
};
|
|
6052
6850
|
var saveCliConfig = (next) => {
|
|
6053
|
-
|
|
6054
|
-
|
|
6851
|
+
fs11.mkdirSync(path12.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
|
|
6852
|
+
fs11.writeFileSync(
|
|
6055
6853
|
CONFIG_PATH,
|
|
6056
6854
|
`${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
|
|
6057
6855
|
`,
|
|
6058
6856
|
{ mode: 384 }
|
|
6059
6857
|
);
|
|
6060
|
-
|
|
6858
|
+
fs11.chmodSync(CONFIG_PATH, 384);
|
|
6061
6859
|
};
|
|
6062
6860
|
var login = async (config, args) => {
|
|
6063
6861
|
await loginCommand(args, {
|
|
@@ -6065,25 +6863,25 @@ var login = async (config, args) => {
|
|
|
6065
6863
|
defaultUrl: config.url,
|
|
6066
6864
|
save: (next) => {
|
|
6067
6865
|
saveCliConfig({ url: next.url, token: next.token, deviceId: next.deviceId });
|
|
6068
|
-
|
|
6866
|
+
say5(dim5(` token saved to ${CONFIG_PATH} (0600)`));
|
|
6069
6867
|
}
|
|
6070
6868
|
});
|
|
6071
6869
|
};
|
|
6072
6870
|
var logout = async (config) => {
|
|
6073
|
-
if (!
|
|
6074
|
-
|
|
6871
|
+
if (!fs11.existsSync(CONFIG_PATH) && config.token === null) {
|
|
6872
|
+
say5(dim5("nothing saved \u2014 already signed out"));
|
|
6075
6873
|
return;
|
|
6076
6874
|
}
|
|
6077
6875
|
if (config.token !== null && config.deviceId !== null) {
|
|
6078
6876
|
try {
|
|
6079
6877
|
await request(config, "DELETE", `/me/devices/${config.deviceId}`);
|
|
6080
|
-
|
|
6878
|
+
say5(`${green5("\u2713")} device revoked on ${config.url}`);
|
|
6081
6879
|
} catch {
|
|
6082
|
-
|
|
6880
|
+
say5(dim5(" could not revoke on the server \u2014 end it under Settings \u2192 Devices"));
|
|
6083
6881
|
}
|
|
6084
6882
|
}
|
|
6085
6883
|
saveCliConfig({ ...config, token: null, deviceId: null });
|
|
6086
|
-
|
|
6884
|
+
say5(`${green5("\u2713")} signed out \xB7 ${dim5(`token removed from ${CONFIG_PATH}`)}`);
|
|
6087
6885
|
};
|
|
6088
6886
|
var UNINSTALL_CHOICES = [
|
|
6089
6887
|
{ scope: "all", text: "everything \xB7 the server on this machine and this machine's Mend files" },
|
|
@@ -6095,20 +6893,20 @@ var UNINSTALL_CHOICES = [
|
|
|
6095
6893
|
];
|
|
6096
6894
|
var askUninstallScope = async () => {
|
|
6097
6895
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
6098
|
-
return
|
|
6896
|
+
return fail5(`${UNINSTALL_USAGE} \xB7 no terminal to ask on`);
|
|
6099
6897
|
}
|
|
6100
|
-
|
|
6101
|
-
UNINSTALL_CHOICES.forEach((choice, index) =>
|
|
6898
|
+
say5("what should go?");
|
|
6899
|
+
UNINSTALL_CHOICES.forEach((choice, index) => say5(` ${index + 1}. ${choice.text}`));
|
|
6102
6900
|
const readline = await import("node:readline/promises");
|
|
6103
6901
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6104
6902
|
const answer = (await rl.question(" 1, 2 or 3: ")).trim();
|
|
6105
6903
|
rl.close();
|
|
6106
6904
|
const chosen = UNINSTALL_CHOICES[Number(answer) - 1];
|
|
6107
|
-
if (chosen === void 0) return
|
|
6905
|
+
if (chosen === void 0) return fail5(`"${answer}" is not one of the choices`);
|
|
6108
6906
|
return chosen.scope;
|
|
6109
6907
|
};
|
|
6110
6908
|
var confirmUninstall = async (word) => {
|
|
6111
|
-
if (process.stdin.isTTY !== true) return
|
|
6909
|
+
if (process.stdin.isTTY !== true) return fail5("non-interactive \u2014 pass --yes to remove");
|
|
6112
6910
|
const readline = await import("node:readline/promises");
|
|
6113
6911
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6114
6912
|
const answer = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
|
|
@@ -6117,14 +6915,14 @@ var confirmUninstall = async (word) => {
|
|
|
6117
6915
|
};
|
|
6118
6916
|
var uninstallCommand = async (config, args) => {
|
|
6119
6917
|
const parsed = parseUninstallArgs(args);
|
|
6120
|
-
if ("error" in parsed) return
|
|
6918
|
+
if ("error" in parsed) return fail5(parsed.error);
|
|
6121
6919
|
const scope = parsed.scope ?? await askUninstallScope();
|
|
6122
6920
|
const server = nodeServerRuntime();
|
|
6123
6921
|
const runtime = {
|
|
6124
6922
|
server,
|
|
6125
6923
|
cliHome: mendCliHome(),
|
|
6126
|
-
sshConfigFile:
|
|
6127
|
-
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 },
|
|
6128
6926
|
revokeDevice: async () => {
|
|
6129
6927
|
if (config.deviceId === null) return "no device id saved";
|
|
6130
6928
|
try {
|
|
@@ -6136,42 +6934,42 @@ var uninstallCommand = async (config, args) => {
|
|
|
6136
6934
|
}
|
|
6137
6935
|
};
|
|
6138
6936
|
const plan = await describeUninstall(runtime, scope);
|
|
6139
|
-
|
|
6140
|
-
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}`);
|
|
6141
6939
|
const nothing = (plan.server === null || plan.server === "none") && (plan.home === null || plan.home.cliConfig === null && plan.home.sshDirectory === null && plan.home.managedSshBlocks === 0);
|
|
6142
6940
|
if (nothing) {
|
|
6143
|
-
|
|
6941
|
+
say5(dim5("nothing to remove"));
|
|
6144
6942
|
return;
|
|
6145
6943
|
}
|
|
6146
6944
|
if (planDeletesData(plan)) {
|
|
6147
|
-
|
|
6945
|
+
say5("repositories, worktrees, the database and its backups are deleted with the server");
|
|
6148
6946
|
}
|
|
6149
6947
|
if (!parsed.yes && !await confirmUninstall(planDeletesData(plan) ? "delete" : "y")) {
|
|
6150
|
-
|
|
6948
|
+
say5(dim5("nothing removed"));
|
|
6151
6949
|
return;
|
|
6152
6950
|
}
|
|
6153
6951
|
const outcome = await executeUninstall(runtime, plan);
|
|
6154
|
-
for (const line of outcome.leftovers)
|
|
6952
|
+
for (const line of outcome.leftovers) say5(dim5(` kept \xB7 ${line}`));
|
|
6155
6953
|
if (outcome.failures.length > 0) {
|
|
6156
|
-
return
|
|
6954
|
+
return fail5(outcome.failures.join("\n"));
|
|
6157
6955
|
}
|
|
6158
|
-
|
|
6159
|
-
`${
|
|
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"}`
|
|
6160
6958
|
);
|
|
6161
6959
|
};
|
|
6162
6960
|
var printGitKey = (key) => {
|
|
6163
6961
|
if (key.publicKey === null) return;
|
|
6164
|
-
|
|
6165
|
-
if (key.fingerprint !== null)
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
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)"));
|
|
6169
6967
|
};
|
|
6170
6968
|
var keysShow = async (config) => {
|
|
6171
6969
|
const key = await api(config, "GET", "/keys/git");
|
|
6172
6970
|
if (!key.exists) {
|
|
6173
|
-
|
|
6174
|
-
`no Mend key yet ${
|
|
6971
|
+
say5(
|
|
6972
|
+
`no Mend key yet ${dim5("\u2014 mend keys init generates one (ed25519, stays on the server host)")}`
|
|
6175
6973
|
);
|
|
6176
6974
|
return;
|
|
6177
6975
|
}
|
|
@@ -6179,46 +6977,41 @@ var keysShow = async (config) => {
|
|
|
6179
6977
|
};
|
|
6180
6978
|
var keysInit = async (config) => {
|
|
6181
6979
|
const key = await api(config, "POST", "/keys/git");
|
|
6182
|
-
|
|
6980
|
+
say5(`${green5("\u2713")} Mend key ready ${dim5("(private half stays on the server host)")}`);
|
|
6183
6981
|
printGitKey(key);
|
|
6184
6982
|
};
|
|
6185
6983
|
var keysShare = async (config) => {
|
|
6186
6984
|
const agentSock = process.env["SSH_AUTH_SOCK"];
|
|
6187
6985
|
if (agentSock === void 0 || agentSock === "") {
|
|
6188
|
-
return
|
|
6986
|
+
return fail5("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
|
|
6189
6987
|
}
|
|
6190
|
-
const url = bridgeUrlOf(
|
|
6191
|
-
parseMendUrl(config.url).toString().replace(/\/$/, ""),
|
|
6192
|
-
config.token,
|
|
6193
|
-
os6.hostname()
|
|
6194
|
-
);
|
|
6195
6988
|
await shareAgent({
|
|
6196
|
-
url,
|
|
6989
|
+
url: () => socketUrl(config, "keys-bridge", { host: os7.hostname() }),
|
|
6197
6990
|
agentSock,
|
|
6198
6991
|
signal: new AbortController().signal,
|
|
6199
6992
|
onEvent: (event) => {
|
|
6200
6993
|
switch (event.kind) {
|
|
6201
6994
|
case "connected":
|
|
6202
|
-
|
|
6203
|
-
|
|
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`));
|
|
6204
6997
|
return;
|
|
6205
6998
|
case "disconnected":
|
|
6206
|
-
|
|
6207
|
-
|
|
6999
|
+
say5(
|
|
7000
|
+
dim5(`not connected \u2014 retrying in ${Math.round(event.retryMs / 1e3)}s (Ctrl-C stops)`)
|
|
6208
7001
|
);
|
|
6209
7002
|
return;
|
|
6210
7003
|
case "sign-requested":
|
|
6211
|
-
|
|
6212
|
-
|
|
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"));
|
|
6213
7006
|
return;
|
|
6214
7007
|
case "identities-requested":
|
|
6215
|
-
|
|
7008
|
+
say5(dim5(` identities requested (${event.context})`));
|
|
6216
7009
|
return;
|
|
6217
7010
|
case "signed":
|
|
6218
|
-
|
|
7011
|
+
say5(`${green5("\u2713")} signed ${dim5(`(${event.seconds.toFixed(1)}s)`)}`);
|
|
6219
7012
|
return;
|
|
6220
7013
|
case "not-signed":
|
|
6221
|
-
|
|
7014
|
+
say5(`${amber("\u2717")} not signed \u2014 ${event.message}`);
|
|
6222
7015
|
return;
|
|
6223
7016
|
}
|
|
6224
7017
|
}
|
|
@@ -6227,19 +7020,19 @@ var keysShare = async (config) => {
|
|
|
6227
7020
|
var keysMode = async (config, args) => {
|
|
6228
7021
|
const [value] = args;
|
|
6229
7022
|
if (value !== void 0 && value !== "mend-key" && value !== "bridge") {
|
|
6230
|
-
return
|
|
7023
|
+
return fail5(`keys mode takes mend-key or bridge, not "${value}"`);
|
|
6231
7024
|
}
|
|
6232
7025
|
const access = value === void 0 ? await api(config, "GET", "/me/git-access") : await api(config, "PUT", "/me/git-access", { mode: value });
|
|
6233
|
-
if (value !== void 0)
|
|
7026
|
+
if (value !== void 0) say5(`${green5("\u2713")} git access \xB7 ${value}`);
|
|
6234
7027
|
if (access.mode === "mend-key") {
|
|
6235
|
-
|
|
7028
|
+
say5(`mend-key ${dim5("\u2014 your Mend key on the server signs; works whenever the server is up")}`);
|
|
6236
7029
|
if (access.key.exists) printGitKey(access.key);
|
|
6237
|
-
else
|
|
7030
|
+
else say5(dim5(" no key yet \u2014 mend keys init creates it"));
|
|
6238
7031
|
return;
|
|
6239
7032
|
}
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
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)")
|
|
6243
7036
|
);
|
|
6244
7037
|
};
|
|
6245
7038
|
var keysCommand = async (config, args) => {
|
|
@@ -6255,75 +7048,75 @@ var keysCommand = async (config, args) => {
|
|
|
6255
7048
|
case void 0:
|
|
6256
7049
|
return keysShow(config);
|
|
6257
7050
|
default:
|
|
6258
|
-
return
|
|
7051
|
+
return fail5(`unknown keys command "${verb}" \xB7 mend help keys lists them`);
|
|
6259
7052
|
}
|
|
6260
7053
|
};
|
|
6261
7054
|
var formatDotfileBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
6262
7055
|
var dotfilesShow = async (config) => {
|
|
6263
7056
|
const dotfiles = await api(config, "GET", "/dotfiles");
|
|
6264
7057
|
if (dotfiles.repository === null) {
|
|
6265
|
-
|
|
7058
|
+
say5(`repo ${dim5("none")}`);
|
|
6266
7059
|
} else {
|
|
6267
7060
|
const branch = dotfiles.repository.ref ?? "default branch";
|
|
6268
7061
|
const subdir = dotfiles.repository.subdirectory === null ? [] : [`${dotfiles.repository.subdirectory}/`];
|
|
6269
7062
|
const bootstrap = dotfiles.repository.bootstrap ? "install.sh on" : "install.sh off";
|
|
6270
|
-
|
|
6271
|
-
`repo ${dotfiles.repository.url} ${
|
|
7063
|
+
say5(
|
|
7064
|
+
`repo ${dotfiles.repository.url} ${dim5(`(${[branch, ...subdir, bootstrap].join(" \xB7 ")})`)}`
|
|
6272
7065
|
);
|
|
6273
7066
|
}
|
|
6274
7067
|
if (dotfiles.snapshot === null) {
|
|
6275
|
-
|
|
7068
|
+
say5(`snapshot ${dim5("none \u2014 sync from this machine: mend dotfiles sync --all")}`);
|
|
6276
7069
|
return;
|
|
6277
7070
|
}
|
|
6278
7071
|
const snapshot = dotfiles.snapshot;
|
|
6279
|
-
|
|
6280
|
-
`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))}`
|
|
6281
7074
|
);
|
|
6282
7075
|
for (const file of snapshot.files) {
|
|
6283
|
-
|
|
7076
|
+
say5(` ${file.path.padEnd(36)} ${dim5(formatDotfileBytes(file.bytes))}`);
|
|
6284
7077
|
}
|
|
6285
7078
|
};
|
|
6286
7079
|
var dotfilesSync = async (config, args) => {
|
|
6287
7080
|
const all = args.includes("--all");
|
|
6288
7081
|
const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
|
|
6289
|
-
const home =
|
|
7082
|
+
const home = os7.homedir();
|
|
6290
7083
|
if (!all && requestedPaths.length === 0) {
|
|
6291
7084
|
const found = scanDotfileCandidates(home);
|
|
6292
7085
|
if (found.length === 0) {
|
|
6293
|
-
|
|
7086
|
+
say5(dim5("no known config files found under ~"));
|
|
6294
7087
|
return;
|
|
6295
7088
|
}
|
|
6296
7089
|
let group = "";
|
|
6297
7090
|
for (const entry of found) {
|
|
6298
7091
|
if (entry.group !== group) {
|
|
6299
7092
|
group = entry.group;
|
|
6300
|
-
|
|
7093
|
+
say5(dim5(group));
|
|
6301
7094
|
}
|
|
6302
|
-
|
|
7095
|
+
say5(` ${entry.path.padEnd(36)} ${dim5(formatDotfileBytes(entry.bytes))}`);
|
|
6303
7096
|
}
|
|
6304
|
-
|
|
6305
|
-
|
|
7097
|
+
say5("");
|
|
7098
|
+
say5(
|
|
6306
7099
|
`sync everything with ${cobalt2("mend dotfiles sync --all")}, or pick: ${cobalt2("mend dotfiles sync .zshrc .gitconfig")}`
|
|
6307
7100
|
);
|
|
6308
7101
|
return;
|
|
6309
7102
|
}
|
|
6310
7103
|
const selected = all ? scanDotfileCandidates(home).map((entry) => entry.path) : requestedPaths;
|
|
6311
|
-
if (selected.length === 0) return
|
|
7104
|
+
if (selected.length === 0) return fail5("nothing to sync \u2014 no known config files found under ~");
|
|
6312
7105
|
const read = readSyncFiles(home, selected);
|
|
6313
|
-
if ("error" in read) return
|
|
7106
|
+
if ("error" in read) return fail5(read.error);
|
|
6314
7107
|
const result = await api(config, "POST", "/dotfiles/snapshot", {
|
|
6315
7108
|
files: read.files,
|
|
6316
|
-
source:
|
|
7109
|
+
source: os7.hostname(),
|
|
6317
7110
|
merge: false
|
|
6318
7111
|
});
|
|
6319
7112
|
const snapshot = result.snapshot;
|
|
6320
|
-
if (snapshot === null) return
|
|
6321
|
-
|
|
6322
|
-
`${
|
|
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")}`
|
|
6323
7116
|
);
|
|
6324
7117
|
};
|
|
6325
7118
|
var envLoad = async (config, args) => {
|
|
6326
|
-
const explicitProject =
|
|
7119
|
+
const explicitProject = takeFlagValue4(args, "--project");
|
|
6327
7120
|
const secretFlag = args.indexOf("--secret");
|
|
6328
7121
|
const secretArg = secretFlag === -1 ? void 0 : args[secretFlag + 1];
|
|
6329
7122
|
const secretNames = secretArg !== void 0 && !secretArg.startsWith("--") && !secretArg.includes("/") && !secretArg.includes(".") ? secretArg.split(",").map((s) => s.trim()).filter((s) => s !== "") : [];
|
|
@@ -6333,15 +7126,15 @@ var envLoad = async (config, args) => {
|
|
|
6333
7126
|
const positional = args.filter(
|
|
6334
7127
|
(arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
|
|
6335
7128
|
);
|
|
6336
|
-
const file =
|
|
7129
|
+
const file = path12.resolve(positional[0] ?? ".env");
|
|
6337
7130
|
let contents;
|
|
6338
7131
|
try {
|
|
6339
|
-
contents =
|
|
7132
|
+
contents = fs11.readFileSync(file, "utf8");
|
|
6340
7133
|
} catch {
|
|
6341
|
-
return
|
|
7134
|
+
return fail5(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
|
|
6342
7135
|
}
|
|
6343
7136
|
if (contents.trim() === "") {
|
|
6344
|
-
|
|
7137
|
+
say5(dim5(`${file} is empty`));
|
|
6345
7138
|
return;
|
|
6346
7139
|
}
|
|
6347
7140
|
const project2 = await findProject(config, explicitProject);
|
|
@@ -6352,46 +7145,46 @@ var envLoad = async (config, args) => {
|
|
|
6352
7145
|
{ contents, allSecret, secretNames }
|
|
6353
7146
|
);
|
|
6354
7147
|
if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
|
|
6355
|
-
|
|
7148
|
+
say5(dim5(`${path12.basename(file)} has no variables`));
|
|
6356
7149
|
return;
|
|
6357
7150
|
}
|
|
6358
|
-
|
|
6359
|
-
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);
|
|
6360
7153
|
const plaintextUrls = report.loaded.filter(
|
|
6361
7154
|
(entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
|
|
6362
7155
|
);
|
|
6363
7156
|
if (plaintextUrls.length > 0) {
|
|
6364
|
-
|
|
7157
|
+
say5(
|
|
6365
7158
|
amber(
|
|
6366
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(",")}`
|
|
6367
7160
|
)
|
|
6368
7161
|
);
|
|
6369
7162
|
}
|
|
6370
7163
|
if (report.malformedLines.length > 0) {
|
|
6371
|
-
|
|
7164
|
+
say5(
|
|
6372
7165
|
amber(
|
|
6373
7166
|
` skipped ${report.malformedLines.length} malformed line${report.malformedLines.length === 1 ? "" : "s"}: ${report.malformedLines.join(", ")}`
|
|
6374
7167
|
)
|
|
6375
7168
|
);
|
|
6376
7169
|
}
|
|
6377
|
-
|
|
6378
|
-
|
|
7170
|
+
say5(
|
|
7171
|
+
dim5(
|
|
6379
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`
|
|
6380
7173
|
)
|
|
6381
7174
|
);
|
|
6382
7175
|
};
|
|
6383
7176
|
var envShow = async (config, args) => {
|
|
6384
|
-
const project2 = await findProject(config,
|
|
7177
|
+
const project2 = await findProject(config, takeFlagValue4(args, "--project"));
|
|
6385
7178
|
const [environment, secrets, cluster] = await Promise.all([
|
|
6386
7179
|
api(config, "GET", `/projects/${project2.id}/environment`),
|
|
6387
7180
|
api(config, "GET", `/projects/${project2.id}/secrets`),
|
|
6388
7181
|
api(config, "GET", `/projects/${project2.id}/cluster-bindings`)
|
|
6389
7182
|
]);
|
|
6390
|
-
|
|
6391
|
-
`${project2.name} ${
|
|
7183
|
+
say5(
|
|
7184
|
+
`${project2.name} ${dim5(`\xB7 configuration r${environment.revision} \xB7 secrets r${secrets.revision} \xB7 cluster r${cluster.revision}`)}`
|
|
6392
7185
|
);
|
|
6393
7186
|
if (environment.variables.length === 0 && secrets.secrets.length === 0 && cluster.bindings.length === 0 && cluster.serviceAccount === null) {
|
|
6394
|
-
|
|
7187
|
+
say5(dim5(` nothing stored \u2014 load a file: ${cobalt2("mend env load")}`));
|
|
6395
7188
|
return;
|
|
6396
7189
|
}
|
|
6397
7190
|
const bindingNames = cluster.bindings.map((b) => `${b.kind}/${b.objectName}`);
|
|
@@ -6402,23 +7195,23 @@ var envShow = async (config, args) => {
|
|
|
6402
7195
|
...bindingNames.map((name) => name.length)
|
|
6403
7196
|
);
|
|
6404
7197
|
for (const variable of environment.variables) {
|
|
6405
|
-
|
|
7198
|
+
say5(` ${variable.name.padEnd(width)} configuration ${dim5("\xB7 plaintext")}`);
|
|
6406
7199
|
}
|
|
6407
7200
|
for (const secret of secrets.secrets) {
|
|
6408
|
-
|
|
7201
|
+
say5(` ${secret.name.padEnd(width)} secret ${dim5("\xB7 value set, never shown")}`);
|
|
6409
7202
|
}
|
|
6410
7203
|
for (const name of bindingNames) {
|
|
6411
|
-
|
|
6412
|
-
` ${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")}`
|
|
6413
7206
|
);
|
|
6414
7207
|
}
|
|
6415
7208
|
if (cluster.serviceAccount !== null) {
|
|
6416
|
-
|
|
6417
|
-
` ${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")}`
|
|
6418
7211
|
);
|
|
6419
7212
|
}
|
|
6420
7213
|
if (!cluster.clusterCapable && (cluster.bindings.length > 0 || cluster.serviceAccount !== null)) {
|
|
6421
|
-
|
|
7214
|
+
say5(
|
|
6422
7215
|
amber(
|
|
6423
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`
|
|
6424
7217
|
)
|
|
@@ -6426,54 +7219,54 @@ var envShow = async (config, args) => {
|
|
|
6426
7219
|
}
|
|
6427
7220
|
};
|
|
6428
7221
|
var envCluster = async (config, args) => {
|
|
6429
|
-
const explicitProject =
|
|
7222
|
+
const explicitProject = takeFlagValue4(args, "--project");
|
|
6430
7223
|
const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
|
|
6431
7224
|
const [verb, ...rest] = positional;
|
|
6432
7225
|
const usage = usageOf("env cluster");
|
|
6433
7226
|
const project2 = await findProject(config, explicitProject);
|
|
6434
7227
|
const route = `/projects/${project2.id}/cluster-bindings`;
|
|
6435
|
-
const appliesLine = () =>
|
|
6436
|
-
|
|
7228
|
+
const appliesLine = () => say5(
|
|
7229
|
+
dim5(
|
|
6437
7230
|
" applies from the next workspace launch; running workspaces keep what they started with"
|
|
6438
7231
|
)
|
|
6439
7232
|
);
|
|
6440
7233
|
if (verb === "add") {
|
|
6441
7234
|
const [kind, objectName] = rest;
|
|
6442
7235
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
6443
|
-
return
|
|
7236
|
+
return fail5(`env cluster add takes a kind and an object name
|
|
6444
7237
|
${usage}`);
|
|
6445
7238
|
}
|
|
6446
7239
|
const result = await api(config, "POST", route, {
|
|
6447
7240
|
kind,
|
|
6448
7241
|
objectName
|
|
6449
7242
|
});
|
|
6450
|
-
|
|
6451
|
-
|
|
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"));
|
|
6452
7245
|
return appliesLine();
|
|
6453
7246
|
}
|
|
6454
7247
|
if (verb === "remove") {
|
|
6455
7248
|
const [ref] = rest;
|
|
6456
7249
|
const [kind, objectName] = ref?.split("/", 2) ?? [];
|
|
6457
7250
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
6458
|
-
return
|
|
7251
|
+
return fail5(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
|
|
6459
7252
|
${usage}`);
|
|
6460
7253
|
}
|
|
6461
7254
|
const snapshot = await api(config, "GET", route);
|
|
6462
7255
|
const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
|
|
6463
|
-
if (binding === void 0) return
|
|
7256
|
+
if (binding === void 0) return fail5(`${kind}/${objectName} is not bound on ${project2.name}`);
|
|
6464
7257
|
const result = await api(
|
|
6465
7258
|
config,
|
|
6466
7259
|
"DELETE",
|
|
6467
7260
|
`${route}/${binding.id}`
|
|
6468
7261
|
);
|
|
6469
|
-
|
|
7262
|
+
say5(`${green5("\u2713")} removed ${kind}/${objectName} ${dim5(`\xB7 cluster r${result.revision}`)}`);
|
|
6470
7263
|
return appliesLine();
|
|
6471
7264
|
}
|
|
6472
7265
|
if (verb === "sa") {
|
|
6473
7266
|
const clear = args.includes("--clear");
|
|
6474
7267
|
const [name] = rest;
|
|
6475
7268
|
if (!clear && name === void 0)
|
|
6476
|
-
return
|
|
7269
|
+
return fail5(`env cluster sa takes a name or --clear
|
|
6477
7270
|
${usage}`);
|
|
6478
7271
|
const result = await api(
|
|
6479
7272
|
config,
|
|
@@ -6481,11 +7274,11 @@ ${usage}`);
|
|
|
6481
7274
|
`${route}/service-account`,
|
|
6482
7275
|
{ serviceAccount: clear ? null : name }
|
|
6483
7276
|
);
|
|
6484
|
-
|
|
6485
|
-
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}`)}`
|
|
6486
7279
|
);
|
|
6487
7280
|
if (result.serviceAccount !== null) {
|
|
6488
|
-
|
|
7281
|
+
say5(
|
|
6489
7282
|
amber(
|
|
6490
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"
|
|
6491
7284
|
)
|
|
@@ -6493,7 +7286,7 @@ ${usage}`);
|
|
|
6493
7286
|
}
|
|
6494
7287
|
return appliesLine();
|
|
6495
7288
|
}
|
|
6496
|
-
return
|
|
7289
|
+
return fail5(`unknown env cluster command "${verb ?? ""}"
|
|
6497
7290
|
${usage}`);
|
|
6498
7291
|
};
|
|
6499
7292
|
var envCommand = async (config, args) => {
|
|
@@ -6507,7 +7300,7 @@ var envCommand = async (config, args) => {
|
|
|
6507
7300
|
case void 0:
|
|
6508
7301
|
return envShow(config, rest);
|
|
6509
7302
|
default:
|
|
6510
|
-
return
|
|
7303
|
+
return fail5(`unknown env command "${verb}" \xB7 mend help env lists them`);
|
|
6511
7304
|
}
|
|
6512
7305
|
};
|
|
6513
7306
|
var dotfilesCommand = async (config, args) => {
|
|
@@ -6519,12 +7312,12 @@ var dotfilesCommand = async (config, args) => {
|
|
|
6519
7312
|
case void 0:
|
|
6520
7313
|
return dotfilesShow(config);
|
|
6521
7314
|
default:
|
|
6522
|
-
return
|
|
7315
|
+
return fail5(`unknown dotfiles command "${verb}" \xB7 mend help dotfiles lists them`);
|
|
6523
7316
|
}
|
|
6524
7317
|
};
|
|
6525
7318
|
var skillsList = async (config, args) => {
|
|
6526
7319
|
const forProject = args.includes("--project");
|
|
6527
|
-
const project2 = forProject ? await findProject(config,
|
|
7320
|
+
const project2 = forProject ? await findProject(config, takeFlagValue4(args, "--project")) : null;
|
|
6528
7321
|
const skills = await api(
|
|
6529
7322
|
config,
|
|
6530
7323
|
"GET",
|
|
@@ -6532,25 +7325,25 @@ var skillsList = async (config, args) => {
|
|
|
6532
7325
|
);
|
|
6533
7326
|
const library = project2 === null ? "your library" : project2.name;
|
|
6534
7327
|
if (skills.length === 0) {
|
|
6535
|
-
|
|
7328
|
+
say5(dim5(`no skills in ${library} \u2014 push a local one: mend skills push`));
|
|
6536
7329
|
return;
|
|
6537
7330
|
}
|
|
6538
7331
|
for (const skill of skills) {
|
|
6539
|
-
|
|
6540
|
-
` ${skill.name.padEnd(28)} ${
|
|
7332
|
+
say5(
|
|
7333
|
+
` ${skill.name.padEnd(28)} ${dim5(`${skill.fileCount} file${skill.fileCount === 1 ? "" : "s"} \xB7 ${formatDotfileBytes(skill.bytes)}`)}`
|
|
6541
7334
|
);
|
|
6542
|
-
if (skill.description !== "")
|
|
7335
|
+
if (skill.description !== "") say5(` ${dim5(skill.description)}`);
|
|
6543
7336
|
}
|
|
6544
7337
|
};
|
|
6545
7338
|
var skillsPush = async (config, args) => {
|
|
6546
|
-
const dirFlag =
|
|
6547
|
-
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);
|
|
6548
7341
|
const scanned = scanSkillLibrary(root);
|
|
6549
|
-
if ("error" in scanned) return
|
|
6550
|
-
for (const note of scanned.notes)
|
|
6551
|
-
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}`);
|
|
6552
7345
|
const forProject = args.includes("--project");
|
|
6553
|
-
const project2 = forProject ? await findProject(config,
|
|
7346
|
+
const project2 = forProject ? await findProject(config, takeFlagValue4(args, "--project")) : null;
|
|
6554
7347
|
const report = await api(config, "POST", "/skills/sync", {
|
|
6555
7348
|
scope: project2 === null ? "user" : "project",
|
|
6556
7349
|
projectId: project2?.id ?? null,
|
|
@@ -6568,10 +7361,10 @@ var skillsPush = async (config, args) => {
|
|
|
6568
7361
|
`${report.unchanged.length} unchanged`,
|
|
6569
7362
|
...report.removed.length > 0 ? [`${report.removed.length} removed`] : []
|
|
6570
7363
|
].join(" \xB7 ");
|
|
6571
|
-
|
|
6572
|
-
`${
|
|
7364
|
+
say5(
|
|
7365
|
+
`${green5("pushed")} ${scanned.skills.length} skill${scanned.skills.length === 1 ? "" : "s"} from ${root} to ${library} \xB7 ${dim5(counts)}`
|
|
6573
7366
|
);
|
|
6574
|
-
|
|
7367
|
+
say5(dim5("sessions receive them from the next launch"));
|
|
6575
7368
|
};
|
|
6576
7369
|
var skillsCommand = async (config, args) => {
|
|
6577
7370
|
const [verb, ...rest] = args;
|
|
@@ -6584,7 +7377,7 @@ var skillsCommand = async (config, args) => {
|
|
|
6584
7377
|
return skillsList(config, args);
|
|
6585
7378
|
default:
|
|
6586
7379
|
if (verb.startsWith("--")) return skillsList(config, args);
|
|
6587
|
-
return
|
|
7380
|
+
return fail5(`unknown skills command "${verb}" \xB7 mend help skills lists them`);
|
|
6588
7381
|
}
|
|
6589
7382
|
};
|
|
6590
7383
|
var completeCommand = async (config, args) => {
|
|
@@ -6688,7 +7481,7 @@ var completionsCommand = (args) => {
|
|
|
6688
7481
|
process.stdout.write(BASH_COMPLETIONS);
|
|
6689
7482
|
return;
|
|
6690
7483
|
default:
|
|
6691
|
-
return
|
|
7484
|
+
return fail5(`${usageOf("completions")} \xB7 e.g. mend completions zsh > "$fpath[1]/_mend"`);
|
|
6692
7485
|
}
|
|
6693
7486
|
};
|
|
6694
7487
|
var supervisedRun = async (config, session, argv) => {
|
|
@@ -6696,15 +7489,15 @@ var supervisedRun = async (config, session, argv) => {
|
|
|
6696
7489
|
"provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
|
|
6697
7490
|
api(config, "POST", `/sessions/${session.id}/launch`, { argv })
|
|
6698
7491
|
);
|
|
6699
|
-
|
|
6700
|
-
`${
|
|
7492
|
+
say5(
|
|
7493
|
+
`${green5("\u2713 recording")} \xB7 run ${dim5(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
|
|
6701
7494
|
);
|
|
6702
|
-
|
|
6703
|
-
|
|
7495
|
+
say5(dim5(` ${argv.join(" ")} \u2014 live record:`));
|
|
7496
|
+
say5("");
|
|
6704
7497
|
const headers = {};
|
|
6705
7498
|
if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
|
|
6706
7499
|
const response = await fetch(`${config.url}/api/events`, { headers });
|
|
6707
|
-
if (response.body === null) return
|
|
7500
|
+
if (response.body === null) return fail5("event stream unavailable");
|
|
6708
7501
|
const reader = response.body.getReader();
|
|
6709
7502
|
const decoder = new TextDecoder();
|
|
6710
7503
|
let buffer = "";
|
|
@@ -6726,7 +7519,7 @@ var supervisedRun = async (config, session, argv) => {
|
|
|
6726
7519
|
}
|
|
6727
7520
|
if (event.sessionId !== session.id) continue;
|
|
6728
7521
|
if (event.type === "session-progress" && event.line !== void 0) {
|
|
6729
|
-
|
|
7522
|
+
say5(dim5(` ${event.line}`));
|
|
6730
7523
|
}
|
|
6731
7524
|
if (event.type === "session") {
|
|
6732
7525
|
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
@@ -6736,9 +7529,9 @@ var supervisedRun = async (config, session, argv) => {
|
|
|
6736
7529
|
}
|
|
6737
7530
|
}
|
|
6738
7531
|
await reader.cancel().catch(() => void 0);
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
7532
|
+
say5("");
|
|
7533
|
+
say5(`${green5("\u2713")} session ${settled ?? "settled"} \xB7 recorded \xB7 checkpoint taken`);
|
|
7534
|
+
say5(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
6742
7535
|
process.exit(settled === "failed" ? 1 : 0);
|
|
6743
7536
|
};
|
|
6744
7537
|
var continueSession = async (config, args) => {
|
|
@@ -6761,20 +7554,20 @@ var continueSession = async (config, args) => {
|
|
|
6761
7554
|
}
|
|
6762
7555
|
}
|
|
6763
7556
|
if (sessionId === null || followUp === null) {
|
|
6764
|
-
return
|
|
7557
|
+
return fail5("no session with a pending follow-up \u2014 send one from the review first");
|
|
6765
7558
|
}
|
|
6766
7559
|
} else {
|
|
6767
7560
|
followUp = await api(config, "GET", `/sessions/${sessionId}/follow-up`);
|
|
6768
|
-
if (followUp === null) return
|
|
7561
|
+
if (followUp === null) return fail5(`session ${sessionId} has no pending follow-up`);
|
|
6769
7562
|
}
|
|
6770
7563
|
const detail = await api(config, "GET", `/sessions/${sessionId}`);
|
|
6771
7564
|
const session = detail.session;
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
for (const line of followUp.instruction.split("\n").slice(0, 6))
|
|
6775
|
-
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"));
|
|
6776
7569
|
if (followUp.reviewSliceId === null || followUp.checkpointAId === null || followUp.checkpointBId === null || followUp.diffDigest === null || followUp.idempotencyKey === null) {
|
|
6777
|
-
return
|
|
7570
|
+
return fail5("legacy follow-up \u2014 recreate it from a pinned Review before delivery");
|
|
6778
7571
|
}
|
|
6779
7572
|
const delivered = await withSpinner(
|
|
6780
7573
|
"delivering persisted Review bundle \u2014 retries reconcile one process\u2026",
|
|
@@ -6789,16 +7582,16 @@ var continueSession = async (config, args) => {
|
|
|
6789
7582
|
})
|
|
6790
7583
|
);
|
|
6791
7584
|
if (delivered.status === "pending") {
|
|
6792
|
-
return
|
|
7585
|
+
return fail5("the session is active \u2014 the follow-up remains pending");
|
|
6793
7586
|
}
|
|
6794
7587
|
if (delivered.status === "delivery_failed") {
|
|
6795
|
-
return
|
|
7588
|
+
return fail5(delivered.deliveryError ?? "delivery failed before process membership finalized");
|
|
6796
7589
|
}
|
|
6797
|
-
|
|
6798
|
-
`${
|
|
7590
|
+
say5(
|
|
7591
|
+
`${green5("\u2713 recording")} \xB7 delivered to run ${delivered.deliverySealantRunId ?? "unknown"}${detachHint()}`
|
|
6799
7592
|
);
|
|
6800
|
-
|
|
6801
|
-
|
|
7593
|
+
say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
7594
|
+
say5("");
|
|
6802
7595
|
await attachOrExit(config, session.id, session.harness);
|
|
6803
7596
|
exitAfterSessionEnd(config, session.id);
|
|
6804
7597
|
};
|
|
@@ -6815,27 +7608,27 @@ var resumeCommand = async (config, args) => {
|
|
|
6815
7608
|
const detail = await api(config, "GET", `/projects/${project2.id}`);
|
|
6816
7609
|
const match = prefix === void 0 ? detail.sessions.find((s) => !agentLiveIn(detail, s)) : detail.sessions.find((s) => s.id.startsWith(prefix));
|
|
6817
7610
|
if (match === void 0) {
|
|
6818
|
-
return
|
|
7611
|
+
return fail5(
|
|
6819
7612
|
prefix === void 0 ? "no settled session to resume \u2014 mend status lists sessions" : `no session matches "${prefix}"`
|
|
6820
7613
|
);
|
|
6821
7614
|
}
|
|
6822
7615
|
if (agentLiveIn(detail, match)) {
|
|
6823
|
-
return
|
|
7616
|
+
return fail5(
|
|
6824
7617
|
`session ${match.id.slice(0, 8)} is live \u2014 attach: mend attach ${match.id.slice(0, 8)}`
|
|
6825
7618
|
);
|
|
6826
7619
|
}
|
|
6827
|
-
|
|
6828
|
-
`${
|
|
7620
|
+
say5(
|
|
7621
|
+
`${green5("\u2713")} resuming ${match.harness} \xB7 ${dim5(match.id.slice(0, 8))}${withHarness === null ? "" : ` ${dim5("as")} ${withHarness}`}`
|
|
6829
7622
|
);
|
|
6830
|
-
|
|
7623
|
+
say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
|
|
6831
7624
|
const priorAgent = detail.annotations.find((annotation) => annotation.sessionId === match.id)?.currentAgent ?? null;
|
|
6832
7625
|
const protocolPrior = priorAgent?.kind === "agent-protocol" && withHarness === null;
|
|
6833
7626
|
await withSpinner(
|
|
6834
7627
|
protocolPrior ? "reopening as a terminal \u2014 same conversation\u2026" : "resuming \u2014 a fresh workspace restores the saved session state\u2026",
|
|
6835
7628
|
protocolPrior ? api(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : api(config, "POST", `/sessions/${match.id}/resume`, { harness: withHarness })
|
|
6836
7629
|
);
|
|
6837
|
-
|
|
6838
|
-
|
|
7630
|
+
say5(`${green5("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
|
|
7631
|
+
say5("");
|
|
6839
7632
|
await attachOrExit(config, match.id, withHarness ?? match.harness);
|
|
6840
7633
|
exitAfterSessionEnd(config, match.id);
|
|
6841
7634
|
};
|
|
@@ -6849,7 +7642,7 @@ var resumeForRejoin = async (config, sessionId, label) => {
|
|
|
6849
7642
|
} catch (error) {
|
|
6850
7643
|
const refreshed = await api(config, "GET", `/sessions/${sessionId}`);
|
|
6851
7644
|
if (agentIsLive(refreshed.session, refreshed.currentAgent)) return false;
|
|
6852
|
-
return
|
|
7645
|
+
return fail5(error instanceof Error ? error.message : String(error));
|
|
6853
7646
|
}
|
|
6854
7647
|
};
|
|
6855
7648
|
var rejoinCommand = async (config, args) => {
|
|
@@ -6863,20 +7656,20 @@ var rejoinCommand = async (config, args) => {
|
|
|
6863
7656
|
const matches = prefix === void 0 ? newest === void 0 ? [] : [newest] : eligible.filter((session2) => session2.id.startsWith(prefix));
|
|
6864
7657
|
if (matches.length === 0) {
|
|
6865
7658
|
const harnessDescription = harness === null ? "" : ` ${harness}`;
|
|
6866
|
-
return
|
|
7659
|
+
return fail5(
|
|
6867
7660
|
prefix === void 0 ? `no${harnessDescription} session to rejoin in project ${project2.name}` : `no${harnessDescription} session matches "${prefix}" in project ${project2.name}`
|
|
6868
7661
|
);
|
|
6869
7662
|
}
|
|
6870
7663
|
if (matches.length > 1) {
|
|
6871
|
-
return
|
|
7664
|
+
return fail5(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
|
|
6872
7665
|
}
|
|
6873
7666
|
const session = matches[0];
|
|
6874
|
-
if (session === void 0) return
|
|
7667
|
+
if (session === void 0) return fail5("session selection failed");
|
|
6875
7668
|
const alreadyLive = agentLiveIn(detail, session);
|
|
6876
|
-
|
|
6877
|
-
`${
|
|
7669
|
+
say5(
|
|
7670
|
+
`${green5("\u2713")} rejoining ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim5(session.id.slice(0, 8))} \xB7 ${alreadyLive ? "already live" : "restoring"}`
|
|
6878
7671
|
);
|
|
6879
|
-
|
|
7672
|
+
say5(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
6880
7673
|
let restored = false;
|
|
6881
7674
|
const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ?? null;
|
|
6882
7675
|
if (currentAgent?.kind === "agent-protocol") {
|
|
@@ -6892,10 +7685,10 @@ var rejoinCommand = async (config, args) => {
|
|
|
6892
7685
|
"resuming \u2014 a fresh workspace restores the saved session state\u2026"
|
|
6893
7686
|
);
|
|
6894
7687
|
}
|
|
6895
|
-
|
|
6896
|
-
restored ? `${
|
|
7688
|
+
say5(
|
|
7689
|
+
restored ? `${green5("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}` : `${green5("\u2713 recording")} \xB7 attached to the live session${detachHint()}`
|
|
6897
7690
|
);
|
|
6898
|
-
|
|
7691
|
+
say5("");
|
|
6899
7692
|
let outcome = await attachTty(config, session.id, session.harness, 0n, void 0, {
|
|
6900
7693
|
handleSignals: true
|
|
6901
7694
|
});
|
|
@@ -6921,7 +7714,7 @@ var projectsCommand = async (config) => {
|
|
|
6921
7714
|
api(config, "GET", "/sessions")
|
|
6922
7715
|
]);
|
|
6923
7716
|
if (projects.length === 0) {
|
|
6924
|
-
|
|
7717
|
+
say5(dim5("no adopted projects \u2014 mend adopt brings one in"));
|
|
6925
7718
|
return;
|
|
6926
7719
|
}
|
|
6927
7720
|
const liveByProject = /* @__PURE__ */ new Map();
|
|
@@ -6933,30 +7726,30 @@ var projectsCommand = async (config) => {
|
|
|
6933
7726
|
const here = matchProjectByCwd(projects, cwdFacts(process.cwd()));
|
|
6934
7727
|
for (const project2 of projects) {
|
|
6935
7728
|
const live = liveByProject.get(project2.id) ?? 0;
|
|
6936
|
-
const liveLabel = live > 0 ?
|
|
7729
|
+
const liveLabel = live > 0 ? green5(`${live} live`) : dim5("\u2014");
|
|
6937
7730
|
const marker = project2.id === here?.id ? cobalt2("\u25B8 ") : " ";
|
|
6938
|
-
|
|
6939
|
-
`${marker}${project2.name.padEnd(nameWidth)} ${
|
|
7731
|
+
say5(
|
|
7732
|
+
`${marker}${project2.name.padEnd(nameWidth)} ${dim5(project2.defaultBranch.padEnd(branchWidth))} ${liveLabel} ${dim5(project2.storePath)}`
|
|
6940
7733
|
);
|
|
6941
7734
|
}
|
|
6942
|
-
if (here !== void 0)
|
|
7735
|
+
if (here !== void 0) say5(dim5(` \u25B8 ${here.name} is the cwd's project`));
|
|
6943
7736
|
};
|
|
6944
7737
|
var refreshCommand = async (config, args) => {
|
|
6945
|
-
const explicit = args.find((a) => !a.startsWith("--")) ??
|
|
7738
|
+
const explicit = args.find((a) => !a.startsWith("--")) ?? takeFlagValue4(args, "--project");
|
|
6946
7739
|
const project2 = await findProject(config, explicit ?? null);
|
|
6947
7740
|
const branches = await api(
|
|
6948
7741
|
config,
|
|
6949
7742
|
"POST",
|
|
6950
7743
|
`/projects/${project2.id}/refresh`
|
|
6951
7744
|
);
|
|
6952
|
-
|
|
7745
|
+
say5(`${green5("\u2713")} refreshed ${project2.name} ${dim5(`\xB7 ${branches.length} branches`)}`);
|
|
6953
7746
|
for (const branch of branches.slice(0, 12)) {
|
|
6954
7747
|
const marker = branch.isDefault ? cobalt2("\u25B8 ") : " ";
|
|
6955
|
-
|
|
6956
|
-
`${marker}${branch.name} ${
|
|
7748
|
+
say5(
|
|
7749
|
+
`${marker}${branch.name} ${dim5(branch.sha.slice(0, 12))} ${dim5(branch.committedAt.slice(0, 10))}`
|
|
6957
7750
|
);
|
|
6958
7751
|
}
|
|
6959
|
-
if (branches.length > 12)
|
|
7752
|
+
if (branches.length > 12) say5(dim5(` \u2026 ${branches.length - 12} more`));
|
|
6960
7753
|
};
|
|
6961
7754
|
var printSessionRow = (row) => {
|
|
6962
7755
|
const { session, annotation } = row;
|
|
@@ -6970,8 +7763,8 @@ var printSessionRow = (row) => {
|
|
|
6970
7763
|
if (annotation !== void 0 && annotation.pendingFollowUp)
|
|
6971
7764
|
facts.push(amber("follow-up pending"));
|
|
6972
7765
|
const base = session.baseRef === null ? session.baseSha.slice(0, 12) : session.baseRef;
|
|
6973
|
-
|
|
6974
|
-
`${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 "))}` : ""}`
|
|
6975
7768
|
);
|
|
6976
7769
|
};
|
|
6977
7770
|
var sessionsCommand = async (config, args) => {
|
|
@@ -6979,7 +7772,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
6979
7772
|
if (args.includes("--json=v2")) {
|
|
6980
7773
|
const projectFlag2 = args.indexOf("--project");
|
|
6981
7774
|
const projectName2 = projectFlag2 !== -1 && args[projectFlag2 + 1] !== void 0 ? String(args[projectFlag2 + 1]) : null;
|
|
6982
|
-
|
|
7775
|
+
say5(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
|
|
6983
7776
|
return;
|
|
6984
7777
|
}
|
|
6985
7778
|
const json = args.includes("--json");
|
|
@@ -6988,7 +7781,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
6988
7781
|
const projects = await api(config, "GET", "/projects");
|
|
6989
7782
|
const scope = projectName === null ? projects : projects.filter((p) => p.name === projectName);
|
|
6990
7783
|
if (projectName !== null && scope.length === 0) {
|
|
6991
|
-
return
|
|
7784
|
+
return fail5(`no adopted project named "${projectName}"`);
|
|
6992
7785
|
}
|
|
6993
7786
|
let rows;
|
|
6994
7787
|
if (all || projectName !== null) {
|
|
@@ -7015,11 +7808,11 @@ var sessionsCommand = async (config, args) => {
|
|
|
7015
7808
|
}
|
|
7016
7809
|
if (rows.length === 0) {
|
|
7017
7810
|
if (json) {
|
|
7018
|
-
|
|
7811
|
+
say5(JSON.stringify({ version: 1, sessions: [] }, null, 2));
|
|
7019
7812
|
return;
|
|
7020
7813
|
}
|
|
7021
|
-
|
|
7022
|
-
|
|
7814
|
+
say5(
|
|
7815
|
+
dim5(all ? "no sessions" : "no active sessions \u2014 mend sessions --all includes settled ones")
|
|
7023
7816
|
);
|
|
7024
7817
|
return;
|
|
7025
7818
|
}
|
|
@@ -7053,7 +7846,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
7053
7846
|
}
|
|
7054
7847
|
}))
|
|
7055
7848
|
};
|
|
7056
|
-
|
|
7849
|
+
say5(JSON.stringify(payload, null, 2));
|
|
7057
7850
|
return;
|
|
7058
7851
|
}
|
|
7059
7852
|
for (const row of rows) printSessionRow(row);
|
|
@@ -7133,11 +7926,11 @@ var worktreesCommand = async (config, args) => {
|
|
|
7133
7926
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
|
|
7134
7927
|
const payload = await buildWorktreesJson(config, projectName);
|
|
7135
7928
|
if (json) {
|
|
7136
|
-
|
|
7929
|
+
say5(JSON.stringify(payload, null, 2));
|
|
7137
7930
|
return;
|
|
7138
7931
|
}
|
|
7139
7932
|
if (payload.worktrees.length === 0) {
|
|
7140
|
-
|
|
7933
|
+
say5(dim5("no worktrees yet \u2014 mend claude --name <worktree> starts one"));
|
|
7141
7934
|
return;
|
|
7142
7935
|
}
|
|
7143
7936
|
for (const worktree of payload.worktrees) {
|
|
@@ -7145,19 +7938,19 @@ var worktreesCommand = async (config, args) => {
|
|
|
7145
7938
|
const facts = [
|
|
7146
7939
|
`${worktree.sessions.length} session${worktree.sessions.length === 1 ? "" : "s"}`
|
|
7147
7940
|
];
|
|
7148
|
-
if (live > 0) facts.push(
|
|
7941
|
+
if (live > 0) facts.push(green5(`${live} live`));
|
|
7149
7942
|
if (worktree.review !== null && worktree.review.openComments > 0) {
|
|
7150
7943
|
facts.push(amber(`${worktree.review.openComments} open`));
|
|
7151
7944
|
}
|
|
7152
7945
|
const base = worktree.baseRef ?? worktree.baseSha.slice(0, 12);
|
|
7153
|
-
|
|
7154
|
-
`${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 "))}`
|
|
7155
7948
|
);
|
|
7156
7949
|
for (const session of worktree.sessions) {
|
|
7157
7950
|
const status = session.status.padEnd(9);
|
|
7158
7951
|
const name = session.label ?? `session ${session.id.slice(0, 8)}`;
|
|
7159
|
-
|
|
7160
|
-
` \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}`
|
|
7161
7954
|
);
|
|
7162
7955
|
}
|
|
7163
7956
|
}
|
|
@@ -7172,24 +7965,24 @@ var hasNodeFfi = () => {
|
|
|
7172
7965
|
};
|
|
7173
7966
|
var dashboard = async (config) => {
|
|
7174
7967
|
if (process.stdout.isTTY !== true) {
|
|
7175
|
-
|
|
7968
|
+
say5(renderIndex());
|
|
7176
7969
|
return;
|
|
7177
7970
|
}
|
|
7178
7971
|
if (!hasNodeFfi()) {
|
|
7179
7972
|
const major = Number(process.versions.node.split(".")[0]);
|
|
7180
7973
|
if (Number.isNaN(major) || major < 26) {
|
|
7181
|
-
return
|
|
7974
|
+
return fail5(
|
|
7182
7975
|
`the dashboard needs Node >= 26 (node:ffi) \u2014 this is ${process.version}; every other command still works`
|
|
7183
7976
|
);
|
|
7184
7977
|
}
|
|
7185
|
-
const rerun =
|
|
7978
|
+
const rerun = spawnSync3(
|
|
7186
7979
|
process.execPath,
|
|
7187
7980
|
["--experimental-ffi", "--disable-warning=ExperimentalWarning", ...process.argv.slice(1)],
|
|
7188
7981
|
{ stdio: "inherit" }
|
|
7189
7982
|
);
|
|
7190
7983
|
process.exit(rerun.status ?? 0);
|
|
7191
7984
|
}
|
|
7192
|
-
const { runDashboard } = await import("./dashboard-
|
|
7985
|
+
const { runDashboard } = await import("./dashboard-ZKCPU2QJ.js");
|
|
7193
7986
|
const agentShare = await startShareIfBridge(config);
|
|
7194
7987
|
try {
|
|
7195
7988
|
await runDashboard({
|
|
@@ -7215,11 +8008,7 @@ var startShareIfBridge = async (config) => {
|
|
|
7215
8008
|
}
|
|
7216
8009
|
if (access.mode !== "bridge") return null;
|
|
7217
8010
|
return startAgentShare({
|
|
7218
|
-
url:
|
|
7219
|
-
parseMendUrl(config.url).toString().replace(/\/$/, ""),
|
|
7220
|
-
config.token,
|
|
7221
|
-
os6.hostname()
|
|
7222
|
-
),
|
|
8011
|
+
url: () => socketUrl(config, "keys-bridge", { host: os7.hostname() }),
|
|
7223
8012
|
agentSock
|
|
7224
8013
|
});
|
|
7225
8014
|
};
|
|
@@ -7233,38 +8022,38 @@ var withAgentShare = async (config, work) => {
|
|
|
7233
8022
|
};
|
|
7234
8023
|
var helpCommand = (words) => {
|
|
7235
8024
|
if (words.length === 0) {
|
|
7236
|
-
|
|
8025
|
+
say5(renderIndex());
|
|
7237
8026
|
return;
|
|
7238
8027
|
}
|
|
7239
8028
|
const doc = findCommand(words);
|
|
7240
8029
|
if (doc !== null && (doc.name.split(" ").length > 1 || words.length === 1)) {
|
|
7241
|
-
|
|
8030
|
+
say5(renderCommand(doc));
|
|
7242
8031
|
return;
|
|
7243
8032
|
}
|
|
7244
8033
|
const group = renderGroup(words[0] ?? "");
|
|
7245
8034
|
if (doc !== null) {
|
|
7246
|
-
|
|
8035
|
+
say5(renderCommand(doc));
|
|
7247
8036
|
return;
|
|
7248
8037
|
}
|
|
7249
8038
|
if (group !== null) {
|
|
7250
|
-
|
|
8039
|
+
say5(group);
|
|
7251
8040
|
return;
|
|
7252
8041
|
}
|
|
7253
|
-
return
|
|
8042
|
+
return fail5(`no command "${words.join(" ")}" \xB7 mend help lists them`);
|
|
7254
8043
|
};
|
|
7255
8044
|
var manCommand = (words) => {
|
|
7256
8045
|
const doc = words.length === 0 ? null : findCommand(words);
|
|
7257
8046
|
if (words.length > 0 && doc === null) {
|
|
7258
|
-
return
|
|
8047
|
+
return fail5(`no command "${words.join(" ")}" \xB7 mend help lists them`);
|
|
7259
8048
|
}
|
|
7260
8049
|
const version = cliVersion();
|
|
7261
8050
|
const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
|
|
7262
|
-
const file =
|
|
7263
|
-
|
|
7264
|
-
const result =
|
|
7265
|
-
|
|
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 });
|
|
7266
8055
|
if (result.error !== void 0 || result.status !== 0) {
|
|
7267
|
-
|
|
8056
|
+
say5(doc === null ? renderIndex() : renderCommand(doc));
|
|
7268
8057
|
}
|
|
7269
8058
|
};
|
|
7270
8059
|
var main = async () => {
|
|
@@ -7275,7 +8064,7 @@ var main = async () => {
|
|
|
7275
8064
|
}
|
|
7276
8065
|
if (command === "server") {
|
|
7277
8066
|
const result = await serverCommand(rest);
|
|
7278
|
-
if (result._tag === "error") return
|
|
8067
|
+
if (result._tag === "error") return fail5(result.message);
|
|
7279
8068
|
return;
|
|
7280
8069
|
}
|
|
7281
8070
|
const config = loadConfig();
|
|
@@ -7313,11 +8102,30 @@ var main = async () => {
|
|
|
7313
8102
|
return connectCommand(config, rest);
|
|
7314
8103
|
case "pair":
|
|
7315
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`);
|
|
7316
8121
|
// Hidden in the catalog: the installer renders its own QR through this.
|
|
7317
8122
|
case "qr":
|
|
7318
8123
|
return qrCommand(rest);
|
|
7319
8124
|
case "doctor":
|
|
7320
|
-
return doctorCommand(config, localCredential)
|
|
8125
|
+
return doctorCommand(config, localCredential, () => {
|
|
8126
|
+
const read = readGrant(claudeGrantDir(mendCliHome()));
|
|
8127
|
+
return read.kind === "missing" ? null : read.secret;
|
|
8128
|
+
});
|
|
7321
8129
|
case "env":
|
|
7322
8130
|
return envCommand(config, rest);
|
|
7323
8131
|
case "ssh":
|
|
@@ -7354,10 +8162,10 @@ var main = async () => {
|
|
|
7354
8162
|
case "--version":
|
|
7355
8163
|
case "-v":
|
|
7356
8164
|
for (const line of versionLines(cliVersion(), await fetchServerVersion(config.url)))
|
|
7357
|
-
|
|
8165
|
+
say5(line);
|
|
7358
8166
|
return;
|
|
7359
8167
|
default:
|
|
7360
|
-
return
|
|
8168
|
+
return fail5(`unknown command "${command}" \xB7 mend help lists them`);
|
|
7361
8169
|
}
|
|
7362
8170
|
};
|
|
7363
8171
|
await main();
|