@sealant/mend 0.36.0-next.643 → 0.36.0-next.646
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 -1
- package/dist/{chunk-2TDRYRGO.js → chunk-ASHAV3UV.js} +259 -0
- package/dist/{dashboard-2HF3KTTU.js → dashboard-4J2WMT3G.js} +134 -17
- package/dist/main.js +646 -461
- package/man/mend-accounts.1 +1 -1
- package/man/mend-adopt.1 +1 -1
- package/man/mend-attach.1 +1 -1
- package/man/mend-codex.1 +3 -1
- package/man/mend-completions.1 +1 -1
- package/man/mend-connect-pi.1 +1 -1
- package/man/mend-connect.1 +3 -1
- package/man/mend-continue.1 +1 -1
- package/man/mend-doctor.1 +1 -1
- package/man/mend-dotfiles-repo.1 +1 -1
- package/man/mend-dotfiles-sync.1 +1 -1
- package/man/mend-dotfiles.1 +1 -1
- package/man/mend-env-cluster.1 +1 -1
- package/man/mend-env-load.1 +1 -1
- package/man/mend-env-show.1 +1 -1
- package/man/mend-folder-create.1 +1 -1
- package/man/mend-folder-list.1 +1 -1
- package/man/mend-folder-push.1 +1 -1
- package/man/mend-folder-rm.1 +1 -1
- package/man/mend-git-author.1 +1 -1
- package/man/mend-help.1 +1 -1
- package/man/mend-invite.1 +1 -1
- package/man/mend-keys-init.1 +1 -1
- package/man/mend-keys-mode.1 +1 -1
- package/man/mend-keys-share.1 +1 -1
- package/man/mend-keys-show.1 +1 -1
- package/man/mend-land.1 +1 -1
- package/man/mend-login.1 +1 -1
- package/man/mend-logout.1 +1 -1
- package/man/mend-man.1 +1 -1
- package/man/mend-members.1 +1 -1
- package/man/mend-memory-import.1 +1 -1
- package/man/mend-memory-rm.1 +1 -1
- package/man/mend-memory-show.1 +1 -1
- package/man/mend-memory.1 +1 -1
- package/man/mend-models.1 +1 -1
- package/man/mend-operator-exposure.1 +1 -1
- package/man/mend-operator-gate.1 +1 -1
- package/man/mend-operator-grant-owner.1 +1 -1
- package/man/mend-operator-org-create.1 +1 -1
- package/man/mend-operator-org-invite-owner.1 +1 -1
- package/man/mend-operator-org-list.1 +1 -1
- package/man/mend-operator-org-rename.1 +1 -1
- package/man/mend-operator-reset-link.1 +1 -1
- package/man/mend-pair.1 +1 -1
- package/man/mend-projects.1 +1 -1
- package/man/mend-pull.1 +1 -1
- package/man/mend-refresh.1 +1 -1
- package/man/mend-rejoin.1 +1 -1
- package/man/mend-resume.1 +1 -1
- package/man/mend-run.1 +1 -1
- package/man/mend-secrets-add.1 +1 -1
- package/man/mend-secrets-rm.1 +1 -1
- package/man/mend-secrets.1 +1 -1
- package/man/mend-server-logs.1 +1 -1
- package/man/mend-server-restart.1 +1 -1
- package/man/mend-server-setup.1 +1 -1
- package/man/mend-server-start.1 +1 -1
- package/man/mend-server-status.1 +1 -1
- package/man/mend-server-stop.1 +1 -1
- package/man/mend-server-upgrade.1 +1 -1
- package/man/mend-server.1 +1 -1
- package/man/mend-service-add.1 +1 -1
- package/man/mend-service-connect.1 +1 -1
- package/man/mend-service-init.1 +1 -1
- package/man/mend-service-list.1 +1 -1
- package/man/mend-service-logs.1 +1 -1
- package/man/mend-service-restart.1 +1 -1
- package/man/mend-service-run.1 +1 -1
- package/man/mend-service-stop.1 +1 -1
- package/man/mend-session-share.1 +13 -3
- package/man/mend-sessions.1 +3 -1
- package/man/mend-shell.1 +1 -1
- package/man/mend-skills-push.1 +1 -1
- package/man/mend-skills.1 +1 -1
- package/man/mend-snake.1 +1 -1
- package/man/mend-ssh-setup.1 +1 -1
- package/man/mend-ssh.1 +1 -1
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +1 -1
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-workspace-replace.1 +24 -0
- package/man/mend-worktrees-rm.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +5 -2
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -13,14 +13,17 @@ import {
|
|
|
13
13
|
PI_PROFILE_AGENT_PATH,
|
|
14
14
|
PI_PROFILE_LOCAL_PACKAGES,
|
|
15
15
|
PI_PROFILE_MAX_FILE_BYTES,
|
|
16
|
+
REPLACE_WORKSPACE_ACTION,
|
|
16
17
|
WORKTREE_REMOVAL_FORCE_HINT,
|
|
17
18
|
agentIsLive,
|
|
18
19
|
agentMemoryNameOf,
|
|
19
20
|
agentOutcome,
|
|
20
21
|
agentStartingFacts,
|
|
21
22
|
agentStartingLine,
|
|
23
|
+
askYesNo,
|
|
22
24
|
captureLineOf,
|
|
23
25
|
claudeGrantFacts,
|
|
26
|
+
confirmPlan,
|
|
24
27
|
createSseParser,
|
|
25
28
|
csiUKeysOf,
|
|
26
29
|
cwdFacts,
|
|
@@ -29,12 +32,14 @@ import {
|
|
|
29
32
|
gitCurrentBranch,
|
|
30
33
|
gitOriginUrl,
|
|
31
34
|
gitTopLevel,
|
|
35
|
+
hasPersonFacts,
|
|
32
36
|
hostUserNamespacesFix,
|
|
33
37
|
hostUserNamespacesOf,
|
|
34
38
|
isDetachChunk,
|
|
35
39
|
isPasteChunk,
|
|
36
40
|
landingFactLine,
|
|
37
41
|
landingFactsFromWire,
|
|
42
|
+
launcherNameOf,
|
|
38
43
|
matchProjectByCwd,
|
|
39
44
|
narrowCredential,
|
|
40
45
|
noAnswerError,
|
|
@@ -43,16 +48,27 @@ import {
|
|
|
43
48
|
normalizeRemoteUrl,
|
|
44
49
|
parseLaunchArgs,
|
|
45
50
|
pasteBytes,
|
|
51
|
+
replaceConfirmLines,
|
|
46
52
|
repositoryCloneUrlIssue,
|
|
47
53
|
sameGrant,
|
|
48
54
|
servicesHoldOf,
|
|
49
55
|
sessionDisplayName,
|
|
56
|
+
sessionWorkspaceLines,
|
|
57
|
+
sharedControlConfirm,
|
|
58
|
+
sharedControlConfirmLines,
|
|
59
|
+
sharedControlLine,
|
|
60
|
+
sharedControlQuestion,
|
|
50
61
|
spoken,
|
|
51
62
|
terminalReadOnlyLine,
|
|
52
63
|
trackBracketedPaste,
|
|
53
64
|
validatePiProfile,
|
|
54
|
-
validateSecretFilePath
|
|
55
|
-
|
|
65
|
+
validateSecretFilePath,
|
|
66
|
+
workspaceLineList,
|
|
67
|
+
workspaceReadsOf,
|
|
68
|
+
workspaceRetirementLine,
|
|
69
|
+
worktreeJoinLine,
|
|
70
|
+
worktreeListsPeople
|
|
71
|
+
} from "./chunk-ASHAV3UV.js";
|
|
56
72
|
|
|
57
73
|
// src/main.ts
|
|
58
74
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
@@ -1705,7 +1721,8 @@ var COMMANDS = [
|
|
|
1705
1721
|
],
|
|
1706
1722
|
description: [
|
|
1707
1723
|
"Sessions run on the platform, so the platform needs your provider login. It is stored under your own user and used only for your work. Nothing is shared with other users.",
|
|
1708
|
-
"Claude and Codex get a login of Mend's own. Both rotate their refresh token, so two copies of one login fight and the one that refreshes second is signed out, and Mend's server refreshes on a schedule. So this logs in through the provider's own flow and your own login stays as it is: Claude in the browser, Codex with a device code, both sent and not kept here. Run it again whenever Mend says a login needs reconnecting. GitHub sends gh's token."
|
|
1724
|
+
"Claude and Codex get a login of Mend's own. Both rotate their refresh token, so two copies of one login fight and the one that refreshes second is signed out, and Mend's server refreshes on a schedule. So this logs in through the provider's own flow and your own login stays as it is: Claude in the browser, Codex with a device code, both sent and not kept here. Run it again whenever Mend says a login needs reconnecting. GitHub sends gh's token.",
|
|
1725
|
+
"In a per-person workspace each person's processes use their own GitHub login, in their own home: git over HTTPS to GitHub reads it through Mend's credential helper, and no GITHUB_TOKEN is set in the environment. A command that needs the token takes it for itself: GITHUB_TOKEN=$(/run/mend/bin/mend-git-credential token) <command>."
|
|
1709
1726
|
],
|
|
1710
1727
|
options: [
|
|
1711
1728
|
{
|
|
@@ -1774,6 +1791,7 @@ var COMMANDS = [
|
|
|
1774
1791
|
description: [
|
|
1775
1792
|
"mend codex, mend claude, mend opencode and mend pi are the same command with a different harness. The session runs in a workspace on the platform, in its own git worktree, and everything it does is recorded. This terminal attaches to it.",
|
|
1776
1793
|
"The worktree's name is asked first. --name skips the ask; an existing name joins that worktree as a new session. --worktree joins only and fails if the name is unknown. A quoted prompt becomes the first message.",
|
|
1794
|
+
"When another person's session is running in the worktree you join, the launch says so before the session starts: you share its workspace, everything you run runs as you, on your own logins, and either of you can read the other's files, logins included.",
|
|
1777
1795
|
"Detach with Ctrl+] and the session keeps running. Reattach from any terminal with mend attach, or from the phone.",
|
|
1778
1796
|
tunnelText,
|
|
1779
1797
|
"Ctrl+V with an image on this machine's clipboard sends the image to the session and pastes its path; codex and claude read it. Needs wl-paste on Wayland, xclip on X11, nothing extra on macOS.",
|
|
@@ -1998,6 +2016,7 @@ var COMMANDS = [
|
|
|
1998
2016
|
synopsis: ["[--all] [--project <p>] [--json | --json=v2]"],
|
|
1999
2017
|
description: [
|
|
2000
2018
|
"One line per session: harness, worktree, status, and what the review found. Live sessions by default.",
|
|
2019
|
+
"Under a live session, a line each when there is something to say about its workspace: who else is live in it (Shared workspace with Anna), the waiting line while a turn waits for another person's work, and the line of a workspace that started before Mend 0.36 and waits to be replaced. The change's owner also reads how to replace it, and what would stop.",
|
|
2001
2020
|
"The JSON is stable for integrations. --json is the v1 flat list; --json=v2 groups sessions by worktree, the same shape mend worktrees prints."
|
|
2002
2021
|
],
|
|
2003
2022
|
options: [
|
|
@@ -2036,6 +2055,21 @@ var COMMANDS = [
|
|
|
2036
2055
|
],
|
|
2037
2056
|
see: ["worktrees", "land", "stop"]
|
|
2038
2057
|
},
|
|
2058
|
+
{
|
|
2059
|
+
name: "workspace replace",
|
|
2060
|
+
section: "sessions",
|
|
2061
|
+
summary: "replace a workspace that started before Mend 0.36",
|
|
2062
|
+
synopsis: ["<session> [--yes]"],
|
|
2063
|
+
description: [
|
|
2064
|
+
`${REPLACE_WORKSPACE_ACTION}, from a terminal. A workspace that shares one home, as every workspace started before Mend 0.36 does, takes only its launcher's sessions and turns once its worktree runs each person as themselves, until it is replaced. Mend replaces it on its own when nothing would stop; otherwise the change's owner does, here or on the web.`,
|
|
2065
|
+
"It prints what was checked, and when: Mend's records, and the processes in the workspace and its running containers once they have been checked. Then what would stop, one line each: terminal sessions (they end resumable), shells, Services started by hand, processes Mend did not start, running containers and anything that could not be checked. Mend starts the launching session's mend.toml Services again.",
|
|
2066
|
+
"The replacement names what it showed you: when more would stop by the time you answer, the server refuses, nothing is stopped, and its words are printed as they are; run it again to look at the list again. An agent turn in flight is never stopped: the server refuses the same way. The workspace goes once its last save is done.",
|
|
2067
|
+
"A prefix of the session id is enough. mend sessions shows which sessions wait for this."
|
|
2068
|
+
],
|
|
2069
|
+
options: [{ flag: "--yes", text: "replace without asking; required without a terminal" }],
|
|
2070
|
+
examples: [{ command: "mend workspace replace 3f2a", text: "" }],
|
|
2071
|
+
see: ["sessions", "stop"]
|
|
2072
|
+
},
|
|
2039
2073
|
{
|
|
2040
2074
|
name: "projects",
|
|
2041
2075
|
section: "sessions",
|
|
@@ -2572,9 +2606,18 @@ var COMMANDS = [
|
|
|
2572
2606
|
name: "session share",
|
|
2573
2607
|
section: "organization",
|
|
2574
2608
|
summary: "let everyone who can see a session steer it, or stop",
|
|
2575
|
-
synopsis: ["<session> on|off"],
|
|
2609
|
+
synopsis: ["<session> on|off [--yes]"],
|
|
2576
2610
|
description: [
|
|
2577
|
-
"The session's owner turns shared control on or off; an organization owner may turn it off. While it is on, anyone who can see the project sends turns, answers approvals and interrupts
|
|
2611
|
+
"The session's owner turns shared control on or off; an organization owner may turn it off. While it is on, anyone who can see the project sends turns, answers approvals and interrupts. They can read the terminal; only the owner types in it, opens a shell or starts a Service. Every act is recorded with who did it. A prefix of the session id is enough.",
|
|
2612
|
+
"Turning it on asks first, in every worktree, in the words true to it; --yes answers for a script, and is required without a terminal. Turning it off asks nothing.",
|
|
2613
|
+
`In a worktree that runs each person as themselves, it asks: ${sharedControlConfirm(true).body}`,
|
|
2614
|
+
`Otherwise it asks: ${sharedControlConfirm(false).body}`
|
|
2615
|
+
],
|
|
2616
|
+
options: [
|
|
2617
|
+
{
|
|
2618
|
+
flag: "--yes",
|
|
2619
|
+
text: "turn it on without asking; required without a terminal"
|
|
2620
|
+
}
|
|
2578
2621
|
],
|
|
2579
2622
|
examples: [{ command: "mend session share 3f2a on", text: "" }],
|
|
2580
2623
|
see: ["sessions"]
|
|
@@ -3703,7 +3746,7 @@ var TIMEOUT_MS = 3e3;
|
|
|
3703
3746
|
var MARKS = { ok: "\u2713", todo: "\u25CB", failed: "\u2717" };
|
|
3704
3747
|
var LABEL_WIDTH = 11;
|
|
3705
3748
|
var day = (at) => at.toISOString().slice(0, 10);
|
|
3706
|
-
var formatCheck = (check,
|
|
3749
|
+
var formatCheck = (check, paint7 = (_state, mark) => mark) => `${paint7(check.state, MARKS[check.state])} ${check.label.padEnd(LABEL_WIDTH)} ${check.detail}${check.fix === null ? "" : ` \u2192 ${check.fix}`}`;
|
|
3707
3750
|
var getJson = async (config, route) => {
|
|
3708
3751
|
try {
|
|
3709
3752
|
const response = await fetch(`${config.url}/api${route}`, {
|
|
@@ -4103,7 +4146,7 @@ var dotfilesRepositoryFacts = (repository) => [
|
|
|
4103
4146
|
].join(" \xB7 ");
|
|
4104
4147
|
|
|
4105
4148
|
// src/env.ts
|
|
4106
|
-
var formatLoadReport = (report,
|
|
4149
|
+
var formatLoadReport = (report, paint7) => {
|
|
4107
4150
|
const width = Math.max(
|
|
4108
4151
|
0,
|
|
4109
4152
|
...report.loaded.map((entry) => entry.name.length),
|
|
@@ -4112,12 +4155,12 @@ var formatLoadReport = (report, paint6) => {
|
|
|
4112
4155
|
const lines = [];
|
|
4113
4156
|
for (const entry of report.loaded) {
|
|
4114
4157
|
lines.push(
|
|
4115
|
-
` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${
|
|
4158
|
+
` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint7.dim(`\xB7 ${entry.action}${entry.lane === "configuration" ? " \xB7 plaintext" : ""}`)}`
|
|
4116
4159
|
);
|
|
4117
4160
|
}
|
|
4118
4161
|
for (const entry of report.rejected) {
|
|
4119
4162
|
lines.push(
|
|
4120
|
-
` ${entry.name.padEnd(width)} ${
|
|
4163
|
+
` ${entry.name.padEnd(width)} ${paint7.warn("rejected")} ${paint7.dim(`\xB7 ${entry.reason}`)}`
|
|
4121
4164
|
);
|
|
4122
4165
|
}
|
|
4123
4166
|
return lines;
|
|
@@ -4866,7 +4909,7 @@ var contiguousIn = (efforts, scale) => {
|
|
|
4866
4909
|
return from >= 0 && efforts.every((effort, index) => scale[from + index] === effort);
|
|
4867
4910
|
};
|
|
4868
4911
|
var effortRange = (efforts, scale) => efforts.length === 0 ? "none" : efforts.length <= 2 || !contiguousIn(efforts, scale) ? efforts.join(", ") : `${efforts[0]} \u2026 ${efforts.at(-1)}`;
|
|
4869
|
-
var modelCatalogLines = (catalogs,
|
|
4912
|
+
var modelCatalogLines = (catalogs, dim8 = (text) => text) => {
|
|
4870
4913
|
if (catalogs.length === 0)
|
|
4871
4914
|
return ["no models listed \xB7 the server's harness_models table is empty"];
|
|
4872
4915
|
const lines = [];
|
|
@@ -4874,7 +4917,7 @@ var modelCatalogLines = (catalogs, dim7 = (text) => text) => {
|
|
|
4874
4917
|
if (index > 0) lines.push("");
|
|
4875
4918
|
const efforts = effortRange(catalog.efforts, catalog.efforts);
|
|
4876
4919
|
lines.push(
|
|
4877
|
-
`${catalog.harness} ${
|
|
4920
|
+
`${catalog.harness} ${dim8(`effort ${efforts}${catalog.fastCapable ? " \xB7 --fast" : ""}`)}`
|
|
4878
4921
|
);
|
|
4879
4922
|
const idWidth = Math.max(...catalog.models.map((model) => model.id.length));
|
|
4880
4923
|
for (const model of catalog.models) {
|
|
@@ -4884,10 +4927,10 @@ var modelCatalogLines = (catalogs, dim7 = (text) => text) => {
|
|
|
4884
4927
|
facts.push(`effort ${effortRange(model.efforts, catalog.efforts)}`);
|
|
4885
4928
|
}
|
|
4886
4929
|
lines.push(
|
|
4887
|
-
` ${model.id.padEnd(idWidth)} ${model.label}${facts.length === 0 ? "" : ` ${
|
|
4930
|
+
` ${model.id.padEnd(idWidth)} ${model.label}${facts.length === 0 ? "" : ` ${dim8(facts.join(" \xB7 "))}`}`
|
|
4888
4931
|
);
|
|
4889
4932
|
}
|
|
4890
|
-
if (catalog.models.length === 0) lines.push(` ${
|
|
4933
|
+
if (catalog.models.length === 0) lines.push(` ${dim8("no models listed \xB7 the harness picks")}`);
|
|
4891
4934
|
}
|
|
4892
4935
|
return lines;
|
|
4893
4936
|
};
|
|
@@ -5058,10 +5101,13 @@ var folderCommand = async (api2, args) => {
|
|
|
5058
5101
|
return fail4(`unknown folder command "${verb}" \xB7 mend help folder`);
|
|
5059
5102
|
}
|
|
5060
5103
|
};
|
|
5061
|
-
var sessionShareCommand = async (api2, args
|
|
5062
|
-
|
|
5104
|
+
var sessionShareCommand = async (api2, args, io = {
|
|
5105
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
5106
|
+
ask: askYesNo
|
|
5107
|
+
}) => {
|
|
5108
|
+
const [prefix, state] = args.filter((arg) => !arg.startsWith("-"));
|
|
5063
5109
|
if (prefix === void 0 || state !== "on" && state !== "off") {
|
|
5064
|
-
return fail4("usage: mend session share <session> on|off");
|
|
5110
|
+
return fail4("usage: mend session share <session> on|off [--yes]");
|
|
5065
5111
|
}
|
|
5066
5112
|
const sessions = await api2(
|
|
5067
5113
|
"GET",
|
|
@@ -5073,10 +5119,28 @@ var sessionShareCommand = async (api2, args) => {
|
|
|
5073
5119
|
return fail4(`"${prefix}" matches ${matches.length} sessions; type more of the id`);
|
|
5074
5120
|
const [session] = matches;
|
|
5075
5121
|
if (session === void 0) return fail4(`no live session starts with "${prefix}"`);
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5122
|
+
if (state === "off") {
|
|
5123
|
+
await api2("PUT", `/sessions/${session.id}/shared-control`, { enabled: false });
|
|
5124
|
+
say4(`${green4("\u2713")} shared control off \xB7 ${session.harness} ${dim4(session.id.slice(0, 8))}`);
|
|
5125
|
+
return;
|
|
5126
|
+
}
|
|
5127
|
+
const plan = confirmPlan(args, io.interactive);
|
|
5128
|
+
if (plan === "refuse") return fail4("non-interactive \xB7 pass --yes to turn shared control on");
|
|
5129
|
+
const detail = await api2(
|
|
5130
|
+
"GET",
|
|
5131
|
+
`/sessions/${session.id}`
|
|
5079
5132
|
);
|
|
5133
|
+
const turnsOnSendersLogin = detail.control?.turnsOnSendersLogin === true;
|
|
5134
|
+
if (plan === "ask") {
|
|
5135
|
+
for (const line of sharedControlConfirmLines(turnsOnSendersLogin)) say4(line);
|
|
5136
|
+
if (!await io.ask(sharedControlQuestion(turnsOnSendersLogin))) {
|
|
5137
|
+
say4(dim4("shared control stays off"));
|
|
5138
|
+
return;
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
await api2("PUT", `/sessions/${session.id}/shared-control`, { enabled: true });
|
|
5142
|
+
say4(`${green4("\u2713")} shared control on \xB7 ${session.harness} ${dim4(session.id.slice(0, 8))}`);
|
|
5143
|
+
say4(dim4(sharedControlLine(turnsOnSendersLogin)));
|
|
5080
5144
|
};
|
|
5081
5145
|
var renderGate = (items) => {
|
|
5082
5146
|
const width = Math.max(...items.map((item) => item.id.length));
|
|
@@ -9689,6 +9753,65 @@ var upgradeUrl = async (input) => {
|
|
|
9689
9753
|
return url;
|
|
9690
9754
|
};
|
|
9691
9755
|
|
|
9756
|
+
// src/workspace-replace.ts
|
|
9757
|
+
var paint5 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
9758
|
+
var dim6 = paint5("2");
|
|
9759
|
+
var green6 = paint5("32");
|
|
9760
|
+
var say6 = (line) => process.stdout.write(`${line}
|
|
9761
|
+
`);
|
|
9762
|
+
var fail6 = (message) => {
|
|
9763
|
+
process.stderr.write(`mend: ${message}
|
|
9764
|
+
`);
|
|
9765
|
+
process.exit(1);
|
|
9766
|
+
};
|
|
9767
|
+
var WORKSPACE_REPLACE_USAGE = "usage: mend workspace replace <session> [--yes]";
|
|
9768
|
+
var terminalIo = () => ({
|
|
9769
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
9770
|
+
ask: askYesNo
|
|
9771
|
+
});
|
|
9772
|
+
var workspaceCommand = async (api2, tryApi, args, io = terminalIo()) => {
|
|
9773
|
+
const [verb, prefix] = args.filter((arg) => !arg.startsWith("-"));
|
|
9774
|
+
if (verb !== "replace" || prefix === void 0) return fail6(WORKSPACE_REPLACE_USAGE);
|
|
9775
|
+
const sessions = await api2("GET", "/sessions?retained=1");
|
|
9776
|
+
const matches = sessions.filter((session2) => session2.id.startsWith(prefix));
|
|
9777
|
+
if (matches.length > 1) {
|
|
9778
|
+
return fail6(`"${prefix}" matches ${matches.length} sessions; type more of the id`);
|
|
9779
|
+
}
|
|
9780
|
+
const [session] = matches;
|
|
9781
|
+
if (session === void 0) return fail6(`no live session starts with "${prefix}"`);
|
|
9782
|
+
const retirement = await tryApi(
|
|
9783
|
+
"GET",
|
|
9784
|
+
`/sessions/${session.id}/workspace-retirement`
|
|
9785
|
+
).catch(() => null);
|
|
9786
|
+
if (retirement !== null) {
|
|
9787
|
+
const members = await tryApi(
|
|
9788
|
+
"GET",
|
|
9789
|
+
"/organization/members"
|
|
9790
|
+
).catch(() => []);
|
|
9791
|
+
say6(
|
|
9792
|
+
dim6(
|
|
9793
|
+
workspaceRetirementLine(
|
|
9794
|
+
retirement,
|
|
9795
|
+
launcherNameOf(retirement.launcher, session.livePeople ?? [], members)
|
|
9796
|
+
)
|
|
9797
|
+
)
|
|
9798
|
+
);
|
|
9799
|
+
for (const line of replaceConfirmLines(retirement)) say6(line);
|
|
9800
|
+
const plan = confirmPlan(args, io.interactive);
|
|
9801
|
+
if (plan === "refuse") return fail6("non-interactive \xB7 pass --yes to replace the workspace");
|
|
9802
|
+
if (plan === "ask" && !await io.ask("replace it?")) {
|
|
9803
|
+
say6(dim6("nothing replaced"));
|
|
9804
|
+
return;
|
|
9805
|
+
}
|
|
9806
|
+
}
|
|
9807
|
+
await api2("POST", `/sessions/${session.id}/workspace-retirement/replace`, {
|
|
9808
|
+
seen: retirement?.fingerprint ?? ""
|
|
9809
|
+
});
|
|
9810
|
+
say6(
|
|
9811
|
+
`${green6("\u2713")} ${workspaceRetirementLine({ state: "retiring", preRelease: retirement?.preRelease ?? true, reason: null }, "")} ${dim6(`\xB7 ${session.harness} ${session.id.slice(0, 8)}`)}`
|
|
9812
|
+
);
|
|
9813
|
+
};
|
|
9814
|
+
|
|
9692
9815
|
// src/worktree-remove.ts
|
|
9693
9816
|
var parseWorktreesRmArgs = (args) => {
|
|
9694
9817
|
let name = null;
|
|
@@ -9787,7 +9910,7 @@ var loadConfig = () => {
|
|
|
9787
9910
|
try {
|
|
9788
9911
|
fileConfig = JSON.parse(fs20.readFileSync(CONFIG_PATH, "utf8"));
|
|
9789
9912
|
} catch {
|
|
9790
|
-
|
|
9913
|
+
fail7(`could not parse ${CONFIG_PATH}`);
|
|
9791
9914
|
}
|
|
9792
9915
|
}
|
|
9793
9916
|
const configuredUrl = process.env["MEND_URL"] ?? fileConfig.url ?? null;
|
|
@@ -9798,7 +9921,7 @@ var loadConfig = () => {
|
|
|
9798
9921
|
deviceId: fileConfig.deviceId ?? null
|
|
9799
9922
|
};
|
|
9800
9923
|
};
|
|
9801
|
-
var
|
|
9924
|
+
var fail7 = (message) => {
|
|
9802
9925
|
process.stderr.write(`mend: ${message}
|
|
9803
9926
|
`);
|
|
9804
9927
|
process.exit(1);
|
|
@@ -9807,18 +9930,18 @@ var parseMendUrl = (value) => {
|
|
|
9807
9930
|
try {
|
|
9808
9931
|
return new URL(value);
|
|
9809
9932
|
} catch {
|
|
9810
|
-
return
|
|
9933
|
+
return fail7(`"${value}" is not a valid Mend URL`);
|
|
9811
9934
|
}
|
|
9812
9935
|
};
|
|
9813
|
-
var
|
|
9814
|
-
var
|
|
9815
|
-
var
|
|
9816
|
-
var amber2 =
|
|
9817
|
-
var cobalt2 =
|
|
9818
|
-
var
|
|
9936
|
+
var paint6 = (code) => (text) => process.stdout.isTTY ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
9937
|
+
var dim7 = paint6("2");
|
|
9938
|
+
var green7 = paint6("32");
|
|
9939
|
+
var amber2 = paint6("33");
|
|
9940
|
+
var cobalt2 = paint6("34");
|
|
9941
|
+
var say7 = (line) => process.stdout.write(`${line}
|
|
9819
9942
|
`);
|
|
9820
9943
|
var detachKeyEnabled = process.env["MEND_DETACH_KEY"] !== "none";
|
|
9821
|
-
var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${
|
|
9944
|
+
var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${dim7("Ctrl+]")}` : "";
|
|
9822
9945
|
var herdrAgentOf = (harness) => {
|
|
9823
9946
|
if (harness === "codex" || harness === "claude" || harness === "opencode") return harness;
|
|
9824
9947
|
return null;
|
|
@@ -9886,6 +10009,7 @@ var request = async (config, method, route, body) => {
|
|
|
9886
10009
|
}
|
|
9887
10010
|
throw new MendRequestError("http", message, response.status);
|
|
9888
10011
|
}
|
|
10012
|
+
if (text === "") return JSON.parse("null");
|
|
9889
10013
|
try {
|
|
9890
10014
|
return JSON.parse(text);
|
|
9891
10015
|
} catch {
|
|
@@ -9896,7 +10020,7 @@ var api = async (config, method, route, body) => {
|
|
|
9896
10020
|
try {
|
|
9897
10021
|
return await request(config, method, route, body);
|
|
9898
10022
|
} catch (error) {
|
|
9899
|
-
return
|
|
10023
|
+
return fail7(error instanceof Error ? error.message : String(error));
|
|
9900
10024
|
}
|
|
9901
10025
|
};
|
|
9902
10026
|
var download = (config) => async (route) => {
|
|
@@ -9907,10 +10031,10 @@ var download = (config) => async (route) => {
|
|
|
9907
10031
|
try {
|
|
9908
10032
|
response = await fetch(`${config.url}/api${route}`, { headers });
|
|
9909
10033
|
} catch (error) {
|
|
9910
|
-
return
|
|
10034
|
+
return fail7(noAnswerError(error, config.url, `GET ${route}`, Date.now() - started).message);
|
|
9911
10035
|
}
|
|
9912
10036
|
if (response.status === 401) {
|
|
9913
|
-
return
|
|
10037
|
+
return fail7(
|
|
9914
10038
|
config.token === null ? `not signed in to ${config.url} \u2014 run: mend login` : `unauthorized at ${config.url} \u2014 the saved token was rejected; run: mend login`
|
|
9915
10039
|
);
|
|
9916
10040
|
}
|
|
@@ -9961,7 +10085,7 @@ var withSpinner = async (label, work) => {
|
|
|
9961
10085
|
const seconds2 = Math.round((Date.now() - started) / 1e3);
|
|
9962
10086
|
const text = typeof label === "string" ? label : label();
|
|
9963
10087
|
process.stdout.write(
|
|
9964
|
-
`\r\x1B[2K ${frames[frame % frames.length]} ${text} ${
|
|
10088
|
+
`\r\x1B[2K ${frames[frame % frames.length]} ${text} ${dim7(`${seconds2}s`)} `
|
|
9965
10089
|
);
|
|
9966
10090
|
frame += 1;
|
|
9967
10091
|
}, 120);
|
|
@@ -9990,7 +10114,7 @@ var followStarting = async (config, sessionId, label, start) => {
|
|
|
9990
10114
|
read: () => request(config, "GET", `/sessions/${sessionId}`),
|
|
9991
10115
|
onLine: (next) => {
|
|
9992
10116
|
line = next === "starting" ? label : next;
|
|
9993
|
-
if (process.stdout.isTTY !== true)
|
|
10117
|
+
if (process.stdout.isTTY !== true) say7(dim7(` ${next}`));
|
|
9994
10118
|
},
|
|
9995
10119
|
sleep: sleep3,
|
|
9996
10120
|
now: Date.now
|
|
@@ -10006,13 +10130,13 @@ var startedOrExit = (config, sessionId, outcome) => {
|
|
|
10006
10130
|
case "live":
|
|
10007
10131
|
return outcome.session;
|
|
10008
10132
|
case "refused":
|
|
10009
|
-
return
|
|
10133
|
+
return fail7(outcome.message);
|
|
10010
10134
|
case "settled":
|
|
10011
|
-
return
|
|
10135
|
+
return fail7(
|
|
10012
10136
|
`session ${id8} \xB7 ${startingLineOf(outcome.session)} before its agent ran \xB7 ${config.url}/sessions/${sessionId}`
|
|
10013
10137
|
);
|
|
10014
10138
|
case "unreachable":
|
|
10015
|
-
return
|
|
10139
|
+
return fail7(
|
|
10016
10140
|
`${outcome.message} \xB7 the session may still be starting \xB7 follow it: mend attach ${id8}`
|
|
10017
10141
|
);
|
|
10018
10142
|
}
|
|
@@ -10023,7 +10147,7 @@ var adopt = async (config, args) => {
|
|
|
10023
10147
|
const name = nameFlag !== -1 && args[nameFlag + 1] !== void 0 ? String(args[nameFlag + 1]) : null;
|
|
10024
10148
|
const authFlagIndex = args.indexOf("--auth");
|
|
10025
10149
|
if (args.includes("--private") && args.includes("--shared")) {
|
|
10026
|
-
return
|
|
10150
|
+
return fail7("--private and --shared pick one visibility; pass one of them");
|
|
10027
10151
|
}
|
|
10028
10152
|
const visibility = args.includes("--shared") ? "shared" : "private";
|
|
10029
10153
|
const positional = args.filter(
|
|
@@ -10031,16 +10155,16 @@ var adopt = async (config, args) => {
|
|
|
10031
10155
|
);
|
|
10032
10156
|
const source = positional[0] ?? gitOriginUrl(process.cwd());
|
|
10033
10157
|
if (source === null) {
|
|
10034
|
-
return
|
|
10158
|
+
return fail7(
|
|
10035
10159
|
"No Git URL was given and this repository has no origin. Run mend adopt <git-url>."
|
|
10036
10160
|
);
|
|
10037
10161
|
}
|
|
10038
10162
|
const sourceIssue = repositoryCloneUrlIssue(source);
|
|
10039
|
-
if (sourceIssue !== null) return
|
|
10163
|
+
if (sourceIssue !== null) return fail7(sourceIssue);
|
|
10040
10164
|
const projectName = name ?? normalizeProjectName(path21.basename(source, ".git"));
|
|
10041
10165
|
const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
|
|
10042
10166
|
if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
|
|
10043
|
-
return
|
|
10167
|
+
return fail7(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
|
|
10044
10168
|
}
|
|
10045
10169
|
const project2 = await api(config, "POST", "/projects", {
|
|
10046
10170
|
name: projectName,
|
|
@@ -10048,25 +10172,25 @@ var adopt = async (config, args) => {
|
|
|
10048
10172
|
visibility,
|
|
10049
10173
|
...auth === null ? {} : { gitAuthMode: auth }
|
|
10050
10174
|
});
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
`${
|
|
10175
|
+
say7(`${green7("\u2713")} adopted \xB7 ${project2.name} \xB7 ${dim7(project2.storePath)}`);
|
|
10176
|
+
say7(`${dim7(" default branch")} ${project2.defaultBranch}`);
|
|
10177
|
+
say7(
|
|
10178
|
+
`${dim7(" visible to")} ${visibility === "private" ? "only you" : "everyone in the organization"}`
|
|
10055
10179
|
);
|
|
10056
10180
|
if (project2.gitAuthMode === "mend-key") {
|
|
10057
|
-
|
|
10181
|
+
say7(`${dim7(" git auth")} mend key ${dim7("(your Mend key signed this clone)")}`);
|
|
10058
10182
|
} else if (project2.gitAuthMode === "bridge") {
|
|
10059
|
-
|
|
10183
|
+
say7(`${dim7(" git auth")} bridge ${dim7("(signed through the connected `mend keys share`)")}`);
|
|
10060
10184
|
}
|
|
10061
|
-
|
|
10062
|
-
`${
|
|
10185
|
+
say7(
|
|
10186
|
+
`${dim7(" sessions start with:")} mend codex ${dim7("(from anywhere \u2014")} --project ${project2.name}${dim7(")")}`
|
|
10063
10187
|
);
|
|
10064
10188
|
const repoRoot = positional[0] === void 0 ? cwdFacts(process.cwd()).repoRoot : null;
|
|
10065
10189
|
const memory = repoRoot === null ? null : claudeMemoryDirFor(repoRoot);
|
|
10066
10190
|
if (memory !== null) {
|
|
10067
10191
|
const count = scanClaudeMemory(memory).files.length;
|
|
10068
|
-
|
|
10069
|
-
`${
|
|
10192
|
+
say7(
|
|
10193
|
+
`${dim7(" claude memory")} ${count} file${count === 1 ? "" : "s"} on this machine ${dim7("\u2192 mend memory import")}`
|
|
10070
10194
|
);
|
|
10071
10195
|
}
|
|
10072
10196
|
};
|
|
@@ -10074,30 +10198,30 @@ var findProject = async (config, explicit, adoptCwd = false) => {
|
|
|
10074
10198
|
const projects = await api(config, "GET", "/projects");
|
|
10075
10199
|
if (explicit !== null) {
|
|
10076
10200
|
const named = projects.find((p) => p.name === explicit);
|
|
10077
|
-
if (named === void 0) return
|
|
10201
|
+
if (named === void 0) return fail7(`no adopted project named "${explicit}"`);
|
|
10078
10202
|
return named;
|
|
10079
10203
|
}
|
|
10080
10204
|
const cwd = process.cwd();
|
|
10081
10205
|
const project2 = matchProjectByCwd(projects, cwdFacts(cwd));
|
|
10082
10206
|
if (project2 !== void 0) return project2;
|
|
10083
10207
|
if (!adoptCwd) {
|
|
10084
|
-
return
|
|
10208
|
+
return fail7(
|
|
10085
10209
|
`no adopted project matches ${cwd} \u2014 run "mend adopt" here first, or name one with --project`
|
|
10086
10210
|
);
|
|
10087
10211
|
}
|
|
10088
10212
|
const facts = cwdFacts(cwd);
|
|
10089
10213
|
if (facts.repoRoot === null || facts.originUrl === null) {
|
|
10090
|
-
return
|
|
10214
|
+
return fail7(
|
|
10091
10215
|
`${cwd} does not match an adopted project with a Git origin \u2014 run "mend adopt <git-url>" first`
|
|
10092
10216
|
);
|
|
10093
10217
|
}
|
|
10094
10218
|
const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
|
|
10095
|
-
if (sourceIssue !== null) return
|
|
10219
|
+
if (sourceIssue !== null) return fail7(sourceIssue);
|
|
10096
10220
|
const adopted = await api(config, "POST", "/projects", {
|
|
10097
10221
|
name: normalizeProjectName(path21.basename(facts.repoRoot)),
|
|
10098
10222
|
source: facts.originUrl
|
|
10099
10223
|
});
|
|
10100
|
-
|
|
10224
|
+
say7(`${green7("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim7(adopted.storePath)}`);
|
|
10101
10225
|
return adopted;
|
|
10102
10226
|
};
|
|
10103
10227
|
var resolvedBackgroundSessions = async (config, project2) => {
|
|
@@ -10124,48 +10248,96 @@ var askWorktreeName = async () => {
|
|
|
10124
10248
|
const readline = await import("node:readline/promises");
|
|
10125
10249
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10126
10250
|
try {
|
|
10127
|
-
const answer = (await rl.question(` worktree name ${
|
|
10251
|
+
const answer = (await rl.question(` worktree name ${dim7("(enter for auto)")}: `)).trim();
|
|
10128
10252
|
return answer === "" ? null : normalizeProjectName(answer);
|
|
10129
10253
|
} finally {
|
|
10130
10254
|
rl.close();
|
|
10131
10255
|
}
|
|
10132
10256
|
};
|
|
10257
|
+
var viewerIdOf = (config) => request(config, "GET", "/organization").then(
|
|
10258
|
+
(view) => view.userId,
|
|
10259
|
+
() => null
|
|
10260
|
+
);
|
|
10261
|
+
var workspaceLinesOf = async (config, sessions) => {
|
|
10262
|
+
const live = sessions.filter((session) => LIVE_STATUSES.has(session.status));
|
|
10263
|
+
if (!hasPersonFacts(live)) return /* @__PURE__ */ new Map();
|
|
10264
|
+
const viewerRead = live.some((session) => (session.livePeople ?? []).length > 0) ? viewerIdOf(config) : Promise.resolve(null);
|
|
10265
|
+
const facts = await Promise.all(
|
|
10266
|
+
live.map(async (session) => {
|
|
10267
|
+
const reads = workspaceReadsOf(session);
|
|
10268
|
+
const [wait, retirement] = await Promise.all([
|
|
10269
|
+
reads.waiting ? request(
|
|
10270
|
+
config,
|
|
10271
|
+
"GET",
|
|
10272
|
+
`/sessions/${session.id}/waiting`
|
|
10273
|
+
).catch(() => null) : null,
|
|
10274
|
+
reads.retirement ? request(
|
|
10275
|
+
config,
|
|
10276
|
+
"GET",
|
|
10277
|
+
`/sessions/${session.id}/workspace-retirement`
|
|
10278
|
+
).catch(() => null) : null
|
|
10279
|
+
]);
|
|
10280
|
+
return { session, wait, retirement };
|
|
10281
|
+
})
|
|
10282
|
+
);
|
|
10283
|
+
const viewer = await viewerRead;
|
|
10284
|
+
const members = facts.some((fact) => fact.retirement !== null) ? await request(config, "GET", "/organization/members").catch(
|
|
10285
|
+
() => []
|
|
10286
|
+
) : [];
|
|
10287
|
+
return new Map(
|
|
10288
|
+
facts.map(({ session, wait, retirement }) => [
|
|
10289
|
+
session.id,
|
|
10290
|
+
workspaceLineList(
|
|
10291
|
+
sessionWorkspaceLines({
|
|
10292
|
+
sessionId: session.id,
|
|
10293
|
+
livePeople: session.livePeople ?? [],
|
|
10294
|
+
viewer,
|
|
10295
|
+
wait,
|
|
10296
|
+
retirement,
|
|
10297
|
+
members
|
|
10298
|
+
})
|
|
10299
|
+
)
|
|
10300
|
+
])
|
|
10301
|
+
);
|
|
10302
|
+
};
|
|
10133
10303
|
var launch = async (config, harness, args) => {
|
|
10134
10304
|
const parsed = parseLaunchArgs(args);
|
|
10135
|
-
if (parsed.error !== null) return
|
|
10305
|
+
if (parsed.error !== null) return fail7(parsed.error);
|
|
10136
10306
|
const structured = parsed.prompt !== null || parsed.model !== null || parsed.effort !== null || parsed.ask || parsed.fast;
|
|
10137
10307
|
if (harness === "run" && structured) {
|
|
10138
|
-
return
|
|
10308
|
+
return fail7(`mend run takes no prompt or harness flags \xB7 ${usageOf("run")}`);
|
|
10139
10309
|
}
|
|
10140
10310
|
if (harness === "run" && parsed.autoLand !== null) {
|
|
10141
|
-
return
|
|
10311
|
+
return fail7(
|
|
10142
10312
|
"mend run takes no landing flags \u2014 a command has no turns to land after; land it with mend land"
|
|
10143
10313
|
);
|
|
10144
10314
|
}
|
|
10145
10315
|
if (harness === "run" && (parsed.detach || parsed.foreground)) {
|
|
10146
|
-
return
|
|
10316
|
+
return fail7(
|
|
10147
10317
|
"mend run takes no lifecycle flags \u2014 it tails the record; Ctrl+C stops watching, not the command"
|
|
10148
10318
|
);
|
|
10149
10319
|
}
|
|
10150
10320
|
const argv = harness === "run" ? parsed.custom : HARNESS_COMMANDS[harness] ?? [];
|
|
10151
10321
|
if (argv.length === 0) {
|
|
10152
|
-
return
|
|
10322
|
+
return fail7(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
|
|
10153
10323
|
}
|
|
10154
10324
|
const project2 = await findProject(config, parsed.project, true);
|
|
10155
|
-
|
|
10156
|
-
`${
|
|
10325
|
+
say7(
|
|
10326
|
+
`${green7("\u2713")} project ${project2.name} ${dim7(`\xB7 ${project2.defaultBranch}${parsed.project === null ? " \xB7 from cwd" : ""}`)}`
|
|
10157
10327
|
);
|
|
10158
10328
|
let worktreeName;
|
|
10329
|
+
let projectView = null;
|
|
10159
10330
|
if (parsed.worktree !== null) {
|
|
10160
10331
|
const detail = await api(config, "GET", `/projects/${project2.id}`);
|
|
10332
|
+
projectView = detail;
|
|
10161
10333
|
const existing = detail.worktrees ?? [];
|
|
10162
10334
|
if (detail.worktrees === void 0) {
|
|
10163
|
-
return
|
|
10335
|
+
return fail7("this server predates shared worktrees \u2014 use --name instead");
|
|
10164
10336
|
}
|
|
10165
10337
|
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);
|
|
10166
10338
|
if (match === void 0) {
|
|
10167
10339
|
const names = existing.map((worktree) => worktree.name).join(", ");
|
|
10168
|
-
return
|
|
10340
|
+
return fail7(
|
|
10169
10341
|
`no worktree matches "${parsed.worktree}" in ${project2.name}${names === "" ? "" : ` \u2014 have: ${names}`}`
|
|
10170
10342
|
);
|
|
10171
10343
|
}
|
|
@@ -10174,17 +10346,19 @@ var launch = async (config, harness, args) => {
|
|
|
10174
10346
|
worktreeName = harness === "run" ? parsed.name : parsed.name ?? await askWorktreeName();
|
|
10175
10347
|
}
|
|
10176
10348
|
if (worktreeName !== null) {
|
|
10177
|
-
const detail = await api(config, "GET", `/projects/${project2.id}`).catch(
|
|
10178
|
-
() => null
|
|
10179
|
-
);
|
|
10349
|
+
const detail = projectView ?? await api(config, "GET", `/projects/${project2.id}`).catch(() => null);
|
|
10180
10350
|
const joined = detail?.worktrees?.find((worktree) => worktree.name === worktreeName);
|
|
10181
10351
|
if (joined !== void 0) {
|
|
10182
10352
|
const members = (detail?.sessions ?? []).filter(
|
|
10183
10353
|
(candidate) => candidate.worktreeId === joined.id
|
|
10184
10354
|
).length;
|
|
10185
|
-
|
|
10186
|
-
`${
|
|
10355
|
+
say7(
|
|
10356
|
+
`${green7("\u2713")} joins worktree ${joined.name} ${dim7(`\xB7 ${members} session${members === 1 ? "" : "s"} \xB7 branch ${joined.branch}`)}`
|
|
10187
10357
|
);
|
|
10358
|
+
const rows = detail?.sessions ?? [];
|
|
10359
|
+
const viewer = worktreeListsPeople(rows, joined.id) ? await viewerIdOf(config) : null;
|
|
10360
|
+
const joinLine = worktreeJoinLine(rows, joined.id, viewer);
|
|
10361
|
+
if (joinLine !== null) say7(`${amber2("\xB7")} ${joinLine}`);
|
|
10188
10362
|
}
|
|
10189
10363
|
}
|
|
10190
10364
|
const lifecycle = harness === "run" ? "background" : parsed.detach ? "detach" : parsed.foreground ? "foreground" : await resolvedBackgroundSessions(config, project2) ? "background" : "foreground";
|
|
@@ -10209,14 +10383,14 @@ var launch = async (config, harness, args) => {
|
|
|
10209
10383
|
...parsed.autoLand === null ? {} : { autoLand: parsed.autoLand }
|
|
10210
10384
|
});
|
|
10211
10385
|
createdSessionId = session.id;
|
|
10212
|
-
|
|
10386
|
+
say7(`${green7("\u2713")} worktree ${session.worktree} ${dim7(`\xB7 branch ${session.branch}`)}`);
|
|
10213
10387
|
const landing = autoLandLine(parsed.autoLand, project2);
|
|
10214
|
-
if (landing !== null)
|
|
10388
|
+
if (landing !== null) say7(landing);
|
|
10215
10389
|
const baseWord = session.baseRef === null ? "" : `${session.baseRef} `;
|
|
10216
|
-
|
|
10217
|
-
`${
|
|
10390
|
+
say7(
|
|
10391
|
+
`${green7("\u2713")} base ${baseWord}${dim7(session.baseSha.slice(0, 12))} \xB7 session ${dim7(session.id.slice(0, 8))}`
|
|
10218
10392
|
);
|
|
10219
|
-
|
|
10393
|
+
say7(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
10220
10394
|
if (harness === "run") {
|
|
10221
10395
|
return supervisedRun(config, session, argv);
|
|
10222
10396
|
}
|
|
@@ -10234,14 +10408,14 @@ var launch = async (config, harness, args) => {
|
|
|
10234
10408
|
request(config, "POST", `/sessions/${session.id}/launch`, launchBody)
|
|
10235
10409
|
);
|
|
10236
10410
|
if (lifecycle === "detach") {
|
|
10237
|
-
|
|
10238
|
-
|
|
10411
|
+
say7(`${green7("\u2713 recording")} \xB7 running detached`);
|
|
10412
|
+
say7(`${cobalt2(" attach")} \xB7 mend attach ${session.id.slice(0, 8)}`);
|
|
10239
10413
|
return;
|
|
10240
10414
|
}
|
|
10241
|
-
|
|
10415
|
+
say7(`${green7("\u2713 recording")} \xB7 workspace mounts the worktree${detachHint()}`);
|
|
10242
10416
|
const tunnels = attachTunnels(config, parsed.noTunnel);
|
|
10243
10417
|
await tunnels?.start(session.id);
|
|
10244
|
-
|
|
10418
|
+
say7("");
|
|
10245
10419
|
attachOwnsSignals = true;
|
|
10246
10420
|
await attachOrExit(config, session.id, session.harness, lifecycle, tunnels);
|
|
10247
10421
|
exitAfterSessionEnd(config, session.id);
|
|
@@ -10251,7 +10425,7 @@ var autoLandLine = (override, project2) => {
|
|
|
10251
10425
|
if (override && project2.autoLand === "off") {
|
|
10252
10426
|
return `${amber2("\xB7")} automatic landing \xB7 off for ${project2.name} \xB7 the project's setting wins over --land`;
|
|
10253
10427
|
}
|
|
10254
|
-
return `${
|
|
10428
|
+
return `${green7("\u2713")} automatic landing \xB7 ${override ? "on" : "off"} for this session ${dim7("\xB7 from this terminal, land with mend land")}`;
|
|
10255
10429
|
};
|
|
10256
10430
|
var ATTACH_CONNECT_TIMEOUT_MS = (() => {
|
|
10257
10431
|
const configured = Number(process.env["MEND_ATTACH_TIMEOUT_MS"]);
|
|
@@ -10448,8 +10622,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
10448
10622
|
watching = true;
|
|
10449
10623
|
const members = await request(config, "GET", "/organization/members").catch(() => []);
|
|
10450
10624
|
process.stdout.write(
|
|
10451
|
-
`\r\x1B[2K${
|
|
10452
|
-
${
|
|
10625
|
+
`\r\x1B[2K${dim7(watchNotice(ownerNameOf(detail?.session.ownerUserId, members)))}\r
|
|
10626
|
+
${dim7(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}\r
|
|
10453
10627
|
\r
|
|
10454
10628
|
`
|
|
10455
10629
|
);
|
|
@@ -10522,51 +10696,51 @@ var stopSessionQuickly = async (config, sessionId) => {
|
|
|
10522
10696
|
var stopAndExit = async (config, sessionId) => {
|
|
10523
10697
|
const id8 = sessionId.slice(0, 8);
|
|
10524
10698
|
if (await stopSessionQuickly(config, sessionId)) {
|
|
10525
|
-
|
|
10526
|
-
|
|
10699
|
+
say7(`${green7("\u2713")} stopped \xB7 ${id8}`);
|
|
10700
|
+
say7(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
|
|
10527
10701
|
process.exit(0);
|
|
10528
10702
|
}
|
|
10529
|
-
|
|
10703
|
+
say7(`${amber2("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
|
|
10530
10704
|
process.exit(1);
|
|
10531
10705
|
};
|
|
10532
10706
|
var noAnswerLine = (config, id8) => `${amber2("no answer")} \u2014 ${config.url} did not open the terminal within ${spoken(ATTACH_CONNECT_TIMEOUT_MS)}; the session keeps running \xB7 try again: mend attach ${id8} \xB7 check the server: mend doctor`;
|
|
10533
10707
|
var finishAttach = async (config, sessionId, outcome, mode = "background") => {
|
|
10534
10708
|
const id8 = sessionId.slice(0, 8);
|
|
10535
10709
|
if (outcome === "unavailable") {
|
|
10536
|
-
return
|
|
10710
|
+
return fail7(`tty attach unavailable: could not connect to ${config.url}`);
|
|
10537
10711
|
}
|
|
10538
10712
|
if (outcome === "no-answer") {
|
|
10539
|
-
|
|
10540
|
-
|
|
10713
|
+
say7("");
|
|
10714
|
+
say7(noAnswerLine(config, id8));
|
|
10541
10715
|
if (mode === "foreground") return stopAndExit(config, sessionId);
|
|
10542
10716
|
process.exit(1);
|
|
10543
10717
|
}
|
|
10544
10718
|
if (outcome === "detached") {
|
|
10545
|
-
|
|
10546
|
-
|
|
10719
|
+
say7("");
|
|
10720
|
+
say7(`${amber2("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
10547
10721
|
process.exit(0);
|
|
10548
10722
|
}
|
|
10549
10723
|
if (outcome === "interrupted") {
|
|
10550
|
-
|
|
10724
|
+
say7("");
|
|
10551
10725
|
if (mode === "foreground") return stopAndExit(config, sessionId);
|
|
10552
|
-
|
|
10726
|
+
say7(`${amber2("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
10553
10727
|
process.exit(0);
|
|
10554
10728
|
}
|
|
10555
10729
|
if (outcome === "dropped") {
|
|
10556
|
-
|
|
10730
|
+
say7("");
|
|
10557
10731
|
if (mode === "foreground") {
|
|
10558
10732
|
let live;
|
|
10559
10733
|
try {
|
|
10560
10734
|
const detail = await request(config, "GET", `/sessions/${sessionId}`);
|
|
10561
10735
|
live = agentIsLive(detail.session, detail.currentAgent);
|
|
10562
10736
|
} catch {
|
|
10563
|
-
|
|
10737
|
+
say7(`${amber2("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
|
|
10564
10738
|
process.exit(1);
|
|
10565
10739
|
}
|
|
10566
10740
|
if (!live) return;
|
|
10567
10741
|
return stopAndExit(config, sessionId);
|
|
10568
10742
|
}
|
|
10569
|
-
|
|
10743
|
+
say7(`${amber2("disconnected")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
10570
10744
|
process.exit(0);
|
|
10571
10745
|
}
|
|
10572
10746
|
};
|
|
@@ -10581,9 +10755,9 @@ var attachOrExit = async (config, sessionId, harness, mode = "background", tunne
|
|
|
10581
10755
|
);
|
|
10582
10756
|
var attachWithTunnels = (tunnels, work) => tunnels === null ? work() : tunnels.attached(work);
|
|
10583
10757
|
var exitAfterSessionEnd = (config, sessionId) => {
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10758
|
+
say7("");
|
|
10759
|
+
say7(`${green7("\u2713")} session ended`);
|
|
10760
|
+
say7(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${sessionId}`);
|
|
10587
10761
|
process.exit(0);
|
|
10588
10762
|
};
|
|
10589
10763
|
var attach = async (config, args) => {
|
|
@@ -10595,18 +10769,18 @@ var attach = async (config, args) => {
|
|
|
10595
10769
|
}
|
|
10596
10770
|
const sessions = await api(config, "GET", "/sessions");
|
|
10597
10771
|
const match = sessions.find((s) => s.id.startsWith(prefix));
|
|
10598
|
-
if (match === void 0) return
|
|
10772
|
+
if (match === void 0) return fail7(`no active session matches "${prefix}"`);
|
|
10599
10773
|
return attachPicked(config, match, tunnels);
|
|
10600
10774
|
};
|
|
10601
10775
|
var attachPicked = async (config, session, tunnels) => {
|
|
10602
|
-
|
|
10603
|
-
`${
|
|
10776
|
+
say7(
|
|
10777
|
+
`${green7("\u2713")} attaching to ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim7(session.id.slice(0, 8))}${detachHint()}`
|
|
10604
10778
|
);
|
|
10605
10779
|
if (session.status === "starting") {
|
|
10606
10780
|
await startAndFollow(config, session.id, "starting \u2014 attaching once the agent runs\u2026", null);
|
|
10607
10781
|
}
|
|
10608
10782
|
await tunnels?.start(session.id);
|
|
10609
|
-
|
|
10783
|
+
say7("");
|
|
10610
10784
|
const outcome = await attachWithTunnels(
|
|
10611
10785
|
tunnels,
|
|
10612
10786
|
() => attachTty(config, session.id, session.harness, 0n, void 0, {
|
|
@@ -10616,14 +10790,14 @@ var attachPicked = async (config, session, tunnels) => {
|
|
|
10616
10790
|
if (outcome === "unavailable") {
|
|
10617
10791
|
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
10618
10792
|
if (detail.currentAgent?.kind === "agent-protocol" && agentIsLive(detail.session, detail.currentAgent)) {
|
|
10619
|
-
|
|
10793
|
+
say7(`${amber2("taking over")} from the protocol session`);
|
|
10620
10794
|
await startAndFollow(
|
|
10621
10795
|
config,
|
|
10622
10796
|
session.id,
|
|
10623
10797
|
"reopening as a terminal \u2014 same conversation\u2026",
|
|
10624
10798
|
request(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
|
|
10625
10799
|
);
|
|
10626
|
-
|
|
10800
|
+
say7("");
|
|
10627
10801
|
await attachOrExit(config, session.id, session.harness, "background", tunnels);
|
|
10628
10802
|
return exitAfterSessionEnd(config, session.id);
|
|
10629
10803
|
}
|
|
@@ -10648,7 +10822,7 @@ var stopCommand = async (config, args) => {
|
|
|
10648
10822
|
return;
|
|
10649
10823
|
}
|
|
10650
10824
|
if (!all && prefix === void 0) {
|
|
10651
|
-
return
|
|
10825
|
+
return fail7(usageOf("stop"));
|
|
10652
10826
|
}
|
|
10653
10827
|
const [sessions, projects] = await Promise.all([
|
|
10654
10828
|
api(config, "GET", "/sessions"),
|
|
@@ -10657,21 +10831,21 @@ var stopCommand = async (config, args) => {
|
|
|
10657
10831
|
let scoped = sessions;
|
|
10658
10832
|
if (projectName !== null) {
|
|
10659
10833
|
const project2 = projects.find((p) => p.name === projectName);
|
|
10660
|
-
if (project2 === void 0) return
|
|
10834
|
+
if (project2 === void 0) return fail7(`no project named "${projectName}"`);
|
|
10661
10835
|
scoped = sessions.filter((s) => s.projectId === project2.id);
|
|
10662
10836
|
}
|
|
10663
10837
|
let targets;
|
|
10664
10838
|
if (all) {
|
|
10665
10839
|
targets = scoped;
|
|
10666
10840
|
if (targets.length === 0) {
|
|
10667
|
-
|
|
10841
|
+
say7("no active sessions");
|
|
10668
10842
|
return;
|
|
10669
10843
|
}
|
|
10670
10844
|
} else {
|
|
10671
10845
|
const matches = scoped.filter((s) => s.id.startsWith(prefix ?? ""));
|
|
10672
|
-
if (matches.length === 0) return
|
|
10846
|
+
if (matches.length === 0) return fail7(`no active session matches "${prefix}"`);
|
|
10673
10847
|
if (matches.length > 1) {
|
|
10674
|
-
return
|
|
10848
|
+
return fail7(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
|
|
10675
10849
|
}
|
|
10676
10850
|
targets = matches;
|
|
10677
10851
|
}
|
|
@@ -10680,24 +10854,24 @@ var stopCommand = async (config, args) => {
|
|
|
10680
10854
|
var stopSessions = async (config, targets, summarise) => {
|
|
10681
10855
|
for (const session of targets) {
|
|
10682
10856
|
await api(config, "POST", `/sessions/${session.id}/stop`);
|
|
10683
|
-
|
|
10684
|
-
`${
|
|
10857
|
+
say7(
|
|
10858
|
+
`${green7("\u2713")} stopped \xB7 ${session.harness} \xB7 ${dim7(session.id.slice(0, 8))} \xB7 ${session.branch}`
|
|
10685
10859
|
);
|
|
10686
10860
|
const after = await api(config, "GET", `/sessions/${session.id}`).catch(
|
|
10687
10861
|
() => null
|
|
10688
10862
|
);
|
|
10689
10863
|
const hold = after === null ? null : servicesHoldOf(after.session, after.currentAgent, after.liveServices ?? 0);
|
|
10690
10864
|
if (hold !== null) {
|
|
10691
|
-
|
|
10865
|
+
say7(`${amber2(" " + hold)} \xB7 mend stop --services ${session.id.slice(0, 8)}`);
|
|
10692
10866
|
}
|
|
10693
10867
|
const capture = after === null ? null : captureLineOf(after.session);
|
|
10694
10868
|
if (capture !== null) {
|
|
10695
|
-
|
|
10869
|
+
say7(`${amber2(" " + capture)} \xB7 the workspace stops once nothing is pending`);
|
|
10696
10870
|
}
|
|
10697
|
-
|
|
10871
|
+
say7(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
10698
10872
|
}
|
|
10699
10873
|
if (summarise) {
|
|
10700
|
-
|
|
10874
|
+
say7(`${green7("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
|
|
10701
10875
|
}
|
|
10702
10876
|
};
|
|
10703
10877
|
var stopServicesOf = async (config, session) => {
|
|
@@ -10707,18 +10881,18 @@ var stopServicesOf = async (config, session) => {
|
|
|
10707
10881
|
`/sessions/${session.id}/services/stop`
|
|
10708
10882
|
);
|
|
10709
10883
|
if (result.stopped === 0) {
|
|
10710
|
-
|
|
10884
|
+
say7(`no live services \xB7 ${session.harness} \xB7 ${dim7(session.id.slice(0, 8))}`);
|
|
10711
10885
|
return;
|
|
10712
10886
|
}
|
|
10713
|
-
|
|
10714
|
-
`${
|
|
10887
|
+
say7(
|
|
10888
|
+
`${green7("\u2713")} stopped ${result.stopped} service${result.stopped === 1 ? "" : "s"} \xB7 ${session.harness} \xB7 ${dim7(session.id.slice(0, 8))} \xB7 ${session.branch}`
|
|
10715
10889
|
);
|
|
10716
10890
|
};
|
|
10717
10891
|
var pickSessionInteractively = async (rows) => {
|
|
10718
|
-
|
|
10892
|
+
say7(dim7("more than one live session \u2014 pick one:"));
|
|
10719
10893
|
rows.forEach((row, index) => {
|
|
10720
|
-
|
|
10721
|
-
` ${index + 1}. ${row.session.harness.padEnd(8)} ${
|
|
10894
|
+
say7(
|
|
10895
|
+
` ${index + 1}. ${row.session.harness.padEnd(8)} ${dim7(row.session.id.slice(0, 8))} ${row.projectName} ${dim7(row.session.branch)} ${dim7(row.session.status)}`
|
|
10722
10896
|
);
|
|
10723
10897
|
});
|
|
10724
10898
|
const readline = await import("node:readline/promises");
|
|
@@ -10726,17 +10900,17 @@ var pickSessionInteractively = async (rows) => {
|
|
|
10726
10900
|
const answer = await rl.question(" session #: ");
|
|
10727
10901
|
rl.close();
|
|
10728
10902
|
const chosen = rows[Number(answer.trim()) - 1];
|
|
10729
|
-
if (chosen === void 0) return
|
|
10903
|
+
if (chosen === void 0) return fail7(`"${answer.trim()}" is not one of the choices`);
|
|
10730
10904
|
return chosen.session;
|
|
10731
10905
|
};
|
|
10732
10906
|
var resolveLiveSession = async (config, prefix, command) => {
|
|
10733
10907
|
const sessions = await api(config, "GET", "/sessions?retained=1");
|
|
10734
10908
|
if (prefix !== void 0) {
|
|
10735
10909
|
const matches = sessions.filter((s) => s.id.startsWith(prefix));
|
|
10736
|
-
if (matches.length === 0) return
|
|
10910
|
+
if (matches.length === 0) return fail7(`no live or retained session matches "${prefix}"`);
|
|
10737
10911
|
const exact = matches[0];
|
|
10738
10912
|
if (matches.length > 1 || exact === void 0) {
|
|
10739
|
-
return
|
|
10913
|
+
return fail7(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
|
|
10740
10914
|
}
|
|
10741
10915
|
return exact;
|
|
10742
10916
|
}
|
|
@@ -10745,11 +10919,11 @@ var resolveLiveSession = async (config, prefix, command) => {
|
|
|
10745
10919
|
const candidates = project2 === void 0 ? sessions : sessions.filter((s) => s.projectId === project2.id);
|
|
10746
10920
|
const only = candidates[0];
|
|
10747
10921
|
if (only === void 0) {
|
|
10748
|
-
return
|
|
10922
|
+
return fail7("no live or retained session \u2014 start one with mend codex|claude|opencode|pi");
|
|
10749
10923
|
}
|
|
10750
10924
|
if (candidates.length === 1) return only;
|
|
10751
10925
|
if (process.stdin.isTTY !== true) {
|
|
10752
|
-
return
|
|
10926
|
+
return fail7(`several live sessions \u2014 name one: mend ${command} <session-id-prefix>`);
|
|
10753
10927
|
}
|
|
10754
10928
|
const nameById = new Map(projects.map((p) => [p.id, p.name]));
|
|
10755
10929
|
return pickSessionInteractively(
|
|
@@ -10762,30 +10936,30 @@ var resolveLiveSession = async (config, prefix, command) => {
|
|
|
10762
10936
|
var shellCommand = async (config, args) => {
|
|
10763
10937
|
const prefix = args.find((a) => !a.startsWith("--"));
|
|
10764
10938
|
const session = await resolveLiveSession(config, prefix, "shell");
|
|
10765
|
-
|
|
10766
|
-
`${
|
|
10939
|
+
say7(
|
|
10940
|
+
`${green7("\u2713")} shell in ${session.harness} session ${dim7(session.id.slice(0, 8))} \xB7 ${dim7(session.branch)}${detachHint()}`
|
|
10767
10941
|
);
|
|
10768
10942
|
const shellProcess = await withSpinner(
|
|
10769
10943
|
"opening a shell in the session workspace\u2026",
|
|
10770
10944
|
api(config, "POST", `/sessions/${session.id}/shell`)
|
|
10771
10945
|
);
|
|
10772
|
-
|
|
10946
|
+
say7("");
|
|
10773
10947
|
const outcome = await attachTty(config, session.id, "shell", 0n, shellProcess.id, {
|
|
10774
10948
|
handleSignals: true
|
|
10775
10949
|
});
|
|
10776
10950
|
if (outcome === "unavailable") {
|
|
10777
|
-
return
|
|
10951
|
+
return fail7(`tty attach unavailable: could not connect to ${config.url}`);
|
|
10778
10952
|
}
|
|
10779
|
-
|
|
10953
|
+
say7("");
|
|
10780
10954
|
if (outcome === "no-answer") {
|
|
10781
|
-
|
|
10955
|
+
say7(noAnswerLine(config, session.id.slice(0, 8)));
|
|
10782
10956
|
process.exit(1);
|
|
10783
10957
|
}
|
|
10784
10958
|
if (outcome === "detached" || outcome === "interrupted" || outcome === "dropped") {
|
|
10785
|
-
|
|
10959
|
+
say7(`${amber2("detached")} \u2014 the shell keeps running and holds the workspace open`);
|
|
10786
10960
|
process.exit(0);
|
|
10787
10961
|
}
|
|
10788
|
-
|
|
10962
|
+
say7(`${green7("\u2713")} shell ended`);
|
|
10789
10963
|
process.exit(0);
|
|
10790
10964
|
};
|
|
10791
10965
|
var flattenService = (view) => {
|
|
@@ -10827,30 +11001,30 @@ var serviceUrl = (service) => service.browserUrl ?? (service.authority === null
|
|
|
10827
11001
|
var printServiceAccess = (config, service, tunneling = false) => {
|
|
10828
11002
|
const gate = "no Mend sign-in on this port \u2014 network reach is the only gate";
|
|
10829
11003
|
if (serverIsLocal(config)) {
|
|
10830
|
-
if (service.authority !== null)
|
|
11004
|
+
if (service.authority !== null) say7(` ${cobalt2(serviceUrl(service))} ${dim7(gate)}`);
|
|
10831
11005
|
return;
|
|
10832
11006
|
}
|
|
10833
11007
|
if (service.protocol === "udp") {
|
|
10834
11008
|
if (service.authority !== null) {
|
|
10835
|
-
|
|
11009
|
+
say7(` ${cobalt2(serviceUrl(service))} ${dim7(`on the server's network \xB7 ${gate}`)}`);
|
|
10836
11010
|
}
|
|
10837
11011
|
return;
|
|
10838
11012
|
}
|
|
10839
11013
|
if (!tunneling) {
|
|
10840
11014
|
const local = service.hostPort ?? service.workspacePort;
|
|
10841
|
-
|
|
10842
|
-
` ${cobalt2(`mend service connect ${service.label}`)} ${
|
|
11015
|
+
say7(
|
|
11016
|
+
` ${cobalt2(`mend service connect ${service.label}`)} ${dim7(`\u2192 127.0.0.1:${local} on this machine, authenticated as you`)}`
|
|
10843
11017
|
);
|
|
10844
11018
|
}
|
|
10845
11019
|
if (service.authority !== null) {
|
|
10846
|
-
|
|
11020
|
+
say7(` ${dim7(`server-side listener ${service.authority} \xB7 ${gate}`)}`);
|
|
10847
11021
|
}
|
|
10848
11022
|
};
|
|
10849
11023
|
var printWorkspaceTtlFailure = (service) => {
|
|
10850
11024
|
if (service.workspaceTtlRenewalError === null) return;
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
11025
|
+
say7(amber2(` workspace TTL renewal failed \xB7 ${service.workspaceTtlRenewalError}`));
|
|
11026
|
+
say7(
|
|
11027
|
+
dim7(
|
|
10854
11028
|
` last renewed ${service.workspaceTtlRenewedAt ?? "unknown"} \xB7 known expiry ${service.workspaceExpiresAt ?? "unknown"} \xB7 failed ${service.workspaceTtlRenewalFailedAt ?? "unknown"}`
|
|
10855
11029
|
)
|
|
10856
11030
|
);
|
|
@@ -10860,11 +11034,11 @@ var printServiceEndpoint = (config, service, tunneling = false) => {
|
|
|
10860
11034
|
printWorkspaceTtlFailure(service);
|
|
10861
11035
|
};
|
|
10862
11036
|
var printService = (config, service) => {
|
|
10863
|
-
const status = service.status === "reachable" ?
|
|
11037
|
+
const status = service.status === "reachable" ? green7(service.status) : amber2(service.status);
|
|
10864
11038
|
const port = `:${service.workspacePort ?? "?"}${service.protocol === "udp" ? "/udp" : ""}`;
|
|
10865
11039
|
const statusPad = " ".repeat(Math.max(1, 12 - service.status.length));
|
|
10866
|
-
|
|
10867
|
-
`${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${
|
|
11040
|
+
say7(
|
|
11041
|
+
`${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${dim7(port.padEnd(7))} ${dim7(service.id.slice(0, 8))}`
|
|
10868
11042
|
);
|
|
10869
11043
|
printServiceAccess(config, service);
|
|
10870
11044
|
printWorkspaceTtlFailure(service);
|
|
@@ -10875,21 +11049,21 @@ var serviceAdd = async (config, args) => {
|
|
|
10875
11049
|
const protocol = args.includes("--udp") ? "udp" : "tcp";
|
|
10876
11050
|
const http = args.includes("--http");
|
|
10877
11051
|
const https = args.includes("--https");
|
|
10878
|
-
if (http && https) return
|
|
11052
|
+
if (http && https) return fail7("Choose either --http or --https, not both.");
|
|
10879
11053
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
10880
11054
|
if (protocol === "udp" && browserScheme !== null) {
|
|
10881
|
-
return
|
|
11055
|
+
return fail7("UDP Services cannot use --http or --https");
|
|
10882
11056
|
}
|
|
10883
11057
|
const positional = args.filter(
|
|
10884
11058
|
(a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1)
|
|
10885
11059
|
);
|
|
10886
11060
|
const portRaw = positional.find((a) => /^\d+$/.test(a));
|
|
10887
11061
|
if (portRaw === void 0) {
|
|
10888
|
-
return
|
|
11062
|
+
return fail7(usageOf("service add"));
|
|
10889
11063
|
}
|
|
10890
11064
|
const port = Number(portRaw);
|
|
10891
11065
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
10892
|
-
return
|
|
11066
|
+
return fail7(`"${portRaw}" is not a port`);
|
|
10893
11067
|
}
|
|
10894
11068
|
const prefix = positional.find((a) => a !== portRaw);
|
|
10895
11069
|
const session = await resolveLiveSession(config, prefix, "service add");
|
|
@@ -10899,52 +11073,52 @@ var serviceAdd = async (config, args) => {
|
|
|
10899
11073
|
protocol,
|
|
10900
11074
|
browserScheme
|
|
10901
11075
|
});
|
|
10902
|
-
|
|
11076
|
+
say7(`${green7("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
|
|
10903
11077
|
printServiceEndpoint(config, service);
|
|
10904
11078
|
if (protocol === "udp") {
|
|
10905
|
-
|
|
11079
|
+
say7(dim7(` udp \u2014 a reply is the only reachability signal; silence just relays`));
|
|
10906
11080
|
} else if (service.status !== "reachable") {
|
|
10907
|
-
|
|
11081
|
+
say7(dim7(` nothing answered on :${port} yet \u2014 the URL goes live when something listens`));
|
|
10908
11082
|
}
|
|
10909
11083
|
};
|
|
10910
11084
|
var serviceList = async (config) => {
|
|
10911
11085
|
const services = await fetchServices(config);
|
|
10912
11086
|
if (services.length === 0) {
|
|
10913
|
-
|
|
11087
|
+
say7(dim7("no live services \u2014 mend service add <port> adopts a listening one"));
|
|
10914
11088
|
return;
|
|
10915
11089
|
}
|
|
10916
11090
|
for (const service of services) printService(config, service);
|
|
10917
11091
|
};
|
|
10918
11092
|
var serviceStop = async (config, args) => {
|
|
10919
11093
|
const needle = args.find((a) => !a.startsWith("--"));
|
|
10920
|
-
if (needle === void 0) return
|
|
11094
|
+
if (needle === void 0) return fail7(usageOf("service stop"));
|
|
10921
11095
|
const services = await fetchServices(config);
|
|
10922
11096
|
const matches = services.filter(
|
|
10923
11097
|
(service) => service.label === needle || service.id.startsWith(needle)
|
|
10924
11098
|
);
|
|
10925
|
-
if (matches.length === 0) return
|
|
11099
|
+
if (matches.length === 0) return fail7(`no live service matches "${needle}"`);
|
|
10926
11100
|
const match = matches[0];
|
|
10927
11101
|
if (matches.length > 1 || match === void 0) {
|
|
10928
|
-
return
|
|
11102
|
+
return fail7(`"${needle}" is ambiguous \u2014 use more of the id`);
|
|
10929
11103
|
}
|
|
10930
11104
|
const stoppedService = await mutateService(config, "POST", `/services/${match.id}/stop`);
|
|
10931
|
-
|
|
11105
|
+
say7(`${green7("\u2713")} stopped \xB7 ${stoppedService.label ?? stoppedService.id.slice(0, 8)}`);
|
|
10932
11106
|
};
|
|
10933
11107
|
var findLiveService = async (config, needle) => {
|
|
10934
11108
|
const services = await fetchServices(config);
|
|
10935
11109
|
const matches = services.filter(
|
|
10936
11110
|
(service) => service.label === needle || service.id.startsWith(needle)
|
|
10937
11111
|
);
|
|
10938
|
-
if (matches.length === 0) return
|
|
11112
|
+
if (matches.length === 0) return fail7(`no live service matches "${needle}"`);
|
|
10939
11113
|
const match = matches[0];
|
|
10940
11114
|
if (matches.length > 1 || match === void 0) {
|
|
10941
|
-
return
|
|
11115
|
+
return fail7(`"${needle}" is ambiguous \u2014 use more of the id`);
|
|
10942
11116
|
}
|
|
10943
11117
|
return match;
|
|
10944
11118
|
};
|
|
10945
11119
|
var willAutoConnect = (config, service, optOut) => !optOut && !serverIsLocal(config) && service.protocol !== "udp";
|
|
10946
11120
|
var autoConnect = async (config, service) => {
|
|
10947
|
-
|
|
11121
|
+
say7(dim7(" remote server \u2014 tunneling the port here \xB7 Ctrl-C stops the tunnel, not the Service"));
|
|
10948
11122
|
await tunnelServices(config, [service], null);
|
|
10949
11123
|
};
|
|
10950
11124
|
var serviceRun = async (config, args) => {
|
|
@@ -10953,7 +11127,7 @@ var serviceRun = async (config, args) => {
|
|
|
10953
11127
|
if (dashdash === -1) {
|
|
10954
11128
|
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
10955
11129
|
const name2 = positionals.at(-1);
|
|
10956
|
-
if (name2 === void 0) return
|
|
11130
|
+
if (name2 === void 0) return fail7(usage2);
|
|
10957
11131
|
const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
|
|
10958
11132
|
const session2 = await resolveLiveSession(config, prefix2, "service run");
|
|
10959
11133
|
const recipes = await api(
|
|
@@ -10964,7 +11138,7 @@ var serviceRun = async (config, args) => {
|
|
|
10964
11138
|
const recipe = recipes.find((entry) => entry.name === name2);
|
|
10965
11139
|
if (recipe === void 0) {
|
|
10966
11140
|
const known = recipes.map((entry) => entry.name).join(", ");
|
|
10967
|
-
return
|
|
11141
|
+
return fail7(
|
|
10968
11142
|
recipes.length === 0 ? `no mend.toml recipes in this worktree \u2014 declare [service.${name2}] first` : `no recipe named "${name2}" \u2014 declared: ${known}`
|
|
10969
11143
|
);
|
|
10970
11144
|
}
|
|
@@ -10975,26 +11149,26 @@ var serviceRun = async (config, args) => {
|
|
|
10975
11149
|
})
|
|
10976
11150
|
);
|
|
10977
11151
|
const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect"));
|
|
10978
|
-
|
|
11152
|
+
say7(`${green7("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
|
|
10979
11153
|
printServiceEndpoint(config, service2, tunneling2);
|
|
10980
|
-
|
|
11154
|
+
say7(dim7(` logs: mend service logs ${service2.label ?? service2.id.slice(0, 8)}`));
|
|
10981
11155
|
if (tunneling2) await autoConnect(config, service2);
|
|
10982
11156
|
return;
|
|
10983
11157
|
}
|
|
10984
11158
|
const argv = args.slice(dashdash + 1);
|
|
10985
|
-
if (argv.length === 0) return
|
|
11159
|
+
if (argv.length === 0) return fail7(usage2);
|
|
10986
11160
|
const head = args.slice(0, dashdash);
|
|
10987
11161
|
const portFlag = head.indexOf("--port");
|
|
10988
11162
|
const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
|
|
10989
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) return
|
|
11163
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return fail7(usage2);
|
|
10990
11164
|
const nameFlag = head.indexOf("--name");
|
|
10991
11165
|
const name = nameFlag !== -1 && head[nameFlag + 1] !== void 0 ? String(head[nameFlag + 1]) : null;
|
|
10992
11166
|
const protocol = head.includes("--udp") ? "udp" : "tcp";
|
|
10993
11167
|
const http = head.includes("--http");
|
|
10994
11168
|
const https = head.includes("--https");
|
|
10995
|
-
if (http && https) return
|
|
11169
|
+
if (http && https) return fail7(usage2);
|
|
10996
11170
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
10997
|
-
if (protocol === "udp" && browserScheme !== null) return
|
|
11171
|
+
if (protocol === "udp" && browserScheme !== null) return fail7(usage2);
|
|
10998
11172
|
const prefix = head.find(
|
|
10999
11173
|
(a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
|
|
11000
11174
|
);
|
|
@@ -11010,9 +11184,9 @@ var serviceRun = async (config, args) => {
|
|
|
11010
11184
|
})
|
|
11011
11185
|
);
|
|
11012
11186
|
const tunneling = willAutoConnect(config, service, head.includes("--no-connect"));
|
|
11013
|
-
|
|
11187
|
+
say7(`${green7("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
|
|
11014
11188
|
printServiceEndpoint(config, service, tunneling);
|
|
11015
|
-
|
|
11189
|
+
say7(dim7(` logs: mend service logs ${service.label ?? service.id.slice(0, 8)}`));
|
|
11016
11190
|
if (tunneling) await autoConnect(config, service);
|
|
11017
11191
|
};
|
|
11018
11192
|
var serviceLogs = async (config, args) => {
|
|
@@ -11020,32 +11194,32 @@ var serviceLogs = async (config, args) => {
|
|
|
11020
11194
|
const from = fromFlag === -1 ? "0" : args[fromFlag + 1];
|
|
11021
11195
|
const needle = firstPositional(args, ["--from"]);
|
|
11022
11196
|
if (needle === void 0 || from === void 0 || !/^(0|[1-9]\d*)$/.test(from)) {
|
|
11023
|
-
return
|
|
11197
|
+
return fail7(usageOf("service logs"));
|
|
11024
11198
|
}
|
|
11025
11199
|
const everything = await fetchServiceViews(config, true);
|
|
11026
11200
|
const matches = everything.filter(
|
|
11027
11201
|
(view2) => view2.service.name === needle || view2.service.id.startsWith(needle) || view2.attempts.some((attempt3) => attempt3.id.startsWith(needle))
|
|
11028
11202
|
);
|
|
11029
|
-
if (matches.length === 0) return
|
|
11203
|
+
if (matches.length === 0) return fail7(`no service matches "${needle}"`);
|
|
11030
11204
|
const view = matches.find((candidate) => {
|
|
11031
11205
|
const current = candidate.service.currentAttemptId === null ? null : candidate.attempts.find((attempt3) => attempt3.id === candidate.service.currentAttemptId);
|
|
11032
11206
|
return current !== null && current !== void 0 && current.exitedAt === null;
|
|
11033
11207
|
}) ?? matches[0];
|
|
11034
|
-
if (view === void 0) return
|
|
11208
|
+
if (view === void 0) return fail7(`no service matches "${needle}"`);
|
|
11035
11209
|
const currentAttempt = view.service.currentAttemptId === null ? null : view.attempts.find((candidate) => candidate.id === view.service.currentAttemptId) ?? null;
|
|
11036
11210
|
const attempt2 = view.attempts.find((candidate) => candidate.id.startsWith(needle)) ?? currentAttempt ?? view.attempts.findLast((candidate) => candidate.sealantSessionId !== null) ?? null;
|
|
11037
11211
|
if (attempt2?.sealantSessionId === null || attempt2 === null) {
|
|
11038
|
-
return
|
|
11212
|
+
return fail7(
|
|
11039
11213
|
`"${needle}" is an adopted port \u2014 no process of Mend's, no logs. mend service run supervises.`
|
|
11040
11214
|
);
|
|
11041
11215
|
}
|
|
11042
11216
|
const label = view.service.name;
|
|
11043
|
-
|
|
11044
|
-
|
|
11217
|
+
say7(
|
|
11218
|
+
dim7(
|
|
11045
11219
|
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}`
|
|
11046
11220
|
)
|
|
11047
11221
|
);
|
|
11048
|
-
|
|
11222
|
+
say7("");
|
|
11049
11223
|
let cursor = from;
|
|
11050
11224
|
let telemetryReported = false;
|
|
11051
11225
|
for (; ; ) {
|
|
@@ -11055,7 +11229,7 @@ var serviceLogs = async (config, args) => {
|
|
|
11055
11229
|
`/processes/${attempt2.id}/logs?from=${encodeURIComponent(cursor)}&limit=256`
|
|
11056
11230
|
);
|
|
11057
11231
|
if (!telemetryReported) {
|
|
11058
|
-
|
|
11232
|
+
say7(dim7(`telemetry loss: ${page.telemetryLoss} \xB7 ${page.telemetryNote}`));
|
|
11059
11233
|
telemetryReported = true;
|
|
11060
11234
|
}
|
|
11061
11235
|
for (const chunk of page.chunks) {
|
|
@@ -11067,25 +11241,25 @@ var serviceLogs = async (config, args) => {
|
|
|
11067
11241
|
if (ended && !advanced) break;
|
|
11068
11242
|
if (!advanced) await new Promise((resolve8) => setTimeout(resolve8, 250));
|
|
11069
11243
|
}
|
|
11070
|
-
|
|
11071
|
-
|
|
11244
|
+
say7("");
|
|
11245
|
+
say7(dim7(`stream ended \xB7 next sequence ${cursor}`));
|
|
11072
11246
|
};
|
|
11073
11247
|
var serviceRestart = async (config, args) => {
|
|
11074
11248
|
const needle = args.find((a) => !a.startsWith("--"));
|
|
11075
|
-
if (needle === void 0) return
|
|
11249
|
+
if (needle === void 0) return fail7(usageOf("service restart"));
|
|
11076
11250
|
const service = await findLiveService(config, needle);
|
|
11077
11251
|
const restarted = await withSpinner(
|
|
11078
11252
|
`restarting ${service.label ?? service.id.slice(0, 8)}\u2026`,
|
|
11079
11253
|
mutateService(config, "POST", `/services/${service.id}/restart`)
|
|
11080
11254
|
);
|
|
11081
|
-
|
|
11255
|
+
say7(`${green7("\u2713")} restarted \xB7 ${restarted.status}`);
|
|
11082
11256
|
printServiceEndpoint(config, restarted);
|
|
11083
11257
|
};
|
|
11084
11258
|
var serviceInit = async (args) => {
|
|
11085
11259
|
const root = gitTopLevel(process.cwd()) ?? process.cwd();
|
|
11086
11260
|
const target = path21.join(root, "mend.toml");
|
|
11087
11261
|
if (fs20.existsSync(target)) {
|
|
11088
|
-
return
|
|
11262
|
+
return fail7(`${target} already exists \u2014 edit it directly (init never merges)`);
|
|
11089
11263
|
}
|
|
11090
11264
|
const rootFiles = fs20.readdirSync(root);
|
|
11091
11265
|
const proposals = [];
|
|
@@ -11127,30 +11301,30 @@ var serviceInit = async (args) => {
|
|
|
11127
11301
|
}
|
|
11128
11302
|
}
|
|
11129
11303
|
if (proposals.length === 0) {
|
|
11130
|
-
return
|
|
11304
|
+
return fail7(
|
|
11131
11305
|
"nothing to propose \u2014 no server-ish package.json script with a nameable port, no compose ports"
|
|
11132
11306
|
);
|
|
11133
11307
|
}
|
|
11134
11308
|
const toml = renderMendToml(proposals);
|
|
11135
|
-
|
|
11136
|
-
|
|
11309
|
+
say7(dim7(`proposed ${target}:`));
|
|
11310
|
+
say7("");
|
|
11137
11311
|
process.stdout.write(toml);
|
|
11138
|
-
|
|
11312
|
+
say7("");
|
|
11139
11313
|
if (!args.includes("--yes")) {
|
|
11140
11314
|
if (process.stdin.isTTY !== true) {
|
|
11141
|
-
return
|
|
11315
|
+
return fail7("non-interactive \u2014 pass --yes to write the file");
|
|
11142
11316
|
}
|
|
11143
11317
|
const readline = await import("node:readline/promises");
|
|
11144
11318
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11145
11319
|
const answer = await rl.question("write it? [y/N] ");
|
|
11146
11320
|
rl.close();
|
|
11147
11321
|
if (answer.trim().toLowerCase() !== "y") {
|
|
11148
|
-
|
|
11322
|
+
say7(dim7("nothing written"));
|
|
11149
11323
|
return;
|
|
11150
11324
|
}
|
|
11151
11325
|
}
|
|
11152
11326
|
fs20.writeFileSync(target, toml);
|
|
11153
|
-
|
|
11327
|
+
say7(`${green7("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
|
|
11154
11328
|
};
|
|
11155
11329
|
var tunnelServices = async (config, services, portOverride) => {
|
|
11156
11330
|
for (const service of services) {
|
|
@@ -11159,15 +11333,15 @@ var tunnelServices = async (config, services, portOverride) => {
|
|
|
11159
11333
|
(socket) => pumpConnection(socket, tunnelUrlFor(config, service.id))
|
|
11160
11334
|
);
|
|
11161
11335
|
await listenLocal(server, port, false).catch(
|
|
11162
|
-
(error) =>
|
|
11336
|
+
(error) => fail7(
|
|
11163
11337
|
error.code === "EADDRINUSE" ? `127.0.0.1:${port} is already in use here \u2014 pick one with: mend service connect ${service.label} --port <n>` : error.message
|
|
11164
11338
|
)
|
|
11165
11339
|
);
|
|
11166
|
-
|
|
11167
|
-
`${
|
|
11340
|
+
say7(
|
|
11341
|
+
`${green7("\u25CF")} ${service.label ?? service.id.slice(0, 8)} \u2192 127.0.0.1:${port} ${dim7(`(tunnel to ${config.url})`)}`
|
|
11168
11342
|
);
|
|
11169
11343
|
}
|
|
11170
|
-
|
|
11344
|
+
say7(dim7(" connections are authenticated as you \xB7 Ctrl-C stops"));
|
|
11171
11345
|
await new Promise(() => {
|
|
11172
11346
|
});
|
|
11173
11347
|
};
|
|
@@ -11177,7 +11351,7 @@ var attachTunnels = (config, optOut) => {
|
|
|
11177
11351
|
let attached = false;
|
|
11178
11352
|
const tell = (line) => {
|
|
11179
11353
|
if (!attached) {
|
|
11180
|
-
|
|
11354
|
+
say7(line);
|
|
11181
11355
|
return;
|
|
11182
11356
|
}
|
|
11183
11357
|
const rows = process.stdout.rows ?? 24;
|
|
@@ -11187,9 +11361,9 @@ var attachTunnels = (config, optOut) => {
|
|
|
11187
11361
|
listServices: () => fetchServices(config),
|
|
11188
11362
|
tunnelUrl: (serviceId) => tunnelUrlFor(config, serviceId),
|
|
11189
11363
|
events: serverEvents(config),
|
|
11190
|
-
onOpen: (tunnel) => tell(`${
|
|
11364
|
+
onOpen: (tunnel) => tell(`${green7("\u25CF")} ${tunnel.line} ${dim7("\xB7 tunnel, closes on detach")}`),
|
|
11191
11365
|
onClose: (tunnel, reason) => {
|
|
11192
|
-
if (reason === "stopped") tell(
|
|
11366
|
+
if (reason === "stopped") tell(dim7(`\u25CB ${tunnel.service.label} stopped \xB7 tunnel closed`));
|
|
11193
11367
|
},
|
|
11194
11368
|
onError: (service, message) => tell(amber2(`${service.label} not tunneled \xB7 ${message}`))
|
|
11195
11369
|
});
|
|
@@ -11213,18 +11387,18 @@ var serviceConnect = async (config, args) => {
|
|
|
11213
11387
|
const portFlag = args.indexOf("--port");
|
|
11214
11388
|
const portOverride = portFlag === -1 ? null : Number(args[portFlag + 1]);
|
|
11215
11389
|
if (portOverride !== null && !Number.isInteger(portOverride)) {
|
|
11216
|
-
return
|
|
11390
|
+
return fail7("--port takes a port number");
|
|
11217
11391
|
}
|
|
11218
11392
|
const names = args.filter((a, i) => !a.startsWith("--") && i !== portFlag + 1);
|
|
11219
11393
|
const live = (await fetchServices(config)).filter((s) => s.protocol === "tcp");
|
|
11220
11394
|
const picked = names.length === 0 ? live : live.filter((s) => names.some((n) => s.label === n || s.id.startsWith(n)));
|
|
11221
11395
|
if (picked.length === 0) {
|
|
11222
|
-
return
|
|
11396
|
+
return fail7(
|
|
11223
11397
|
names.length === 0 ? "no live TCP services \u2014 mend service run starts one" : `no live TCP service matches "${names.join('", "')}"`
|
|
11224
11398
|
);
|
|
11225
11399
|
}
|
|
11226
11400
|
if (portOverride !== null && picked.length !== 1) {
|
|
11227
|
-
return
|
|
11401
|
+
return fail7("--port applies to exactly one service \u2014 name it");
|
|
11228
11402
|
}
|
|
11229
11403
|
await tunnelServices(config, picked, portOverride);
|
|
11230
11404
|
};
|
|
@@ -11265,23 +11439,23 @@ var memoryPathOf = (name) => {
|
|
|
11265
11439
|
};
|
|
11266
11440
|
var memoryImport = async (config, args) => {
|
|
11267
11441
|
const repoRoot = cwdFacts(process.cwd()).repoRoot;
|
|
11268
|
-
if (repoRoot === null) return
|
|
11442
|
+
if (repoRoot === null) return fail7("run mend memory import inside the repository's checkout");
|
|
11269
11443
|
const claudeDir = claudeMemoryDirFor(repoRoot);
|
|
11270
11444
|
const claude = claudeDir === null ? null : scanClaudeMemory(claudeDir);
|
|
11271
11445
|
const codex = await scanCodexMemory(repoRoot);
|
|
11272
|
-
for (const note of [...claude?.notes ?? [], ...codex.notes])
|
|
11446
|
+
for (const note of [...claude?.notes ?? [], ...codex.notes]) say7(dim7(` ${note}`));
|
|
11273
11447
|
const files = [...claude?.files ?? [], ...codex.files];
|
|
11274
11448
|
if (files.length === 0) {
|
|
11275
|
-
return
|
|
11449
|
+
return fail7(`neither claude nor codex keeps memory for ${repoRoot} on this machine`);
|
|
11276
11450
|
}
|
|
11277
11451
|
const project2 = await findProject(config, takeFlagValue4(args, "--project"));
|
|
11278
11452
|
if (claude !== null && claude.files.length > 0) {
|
|
11279
|
-
|
|
11453
|
+
say7(
|
|
11280
11454
|
`claude memory \xB7 ${claude.dir} \xB7 ${claude.files.length} file${claude.files.length === 1 ? "" : "s"}`
|
|
11281
11455
|
);
|
|
11282
11456
|
}
|
|
11283
11457
|
if (codex.files.length > 0) {
|
|
11284
|
-
|
|
11458
|
+
say7(
|
|
11285
11459
|
`codex memory \xB7 ${codex.home} \xB7 ${codex.summaries} conversation summar${codex.summaries === 1 ? "y" : "ies"} of this repository`
|
|
11286
11460
|
);
|
|
11287
11461
|
}
|
|
@@ -11296,12 +11470,12 @@ var memoryImport = async (config, args) => {
|
|
|
11296
11470
|
source === null ? { files } : { files, source }
|
|
11297
11471
|
)
|
|
11298
11472
|
);
|
|
11299
|
-
|
|
11300
|
-
`${dryRun ? "would import" :
|
|
11473
|
+
say7(
|
|
11474
|
+
`${dryRun ? "would import" : green7("imported")} into ${project2.name} \xB7 ${importReportCounts(report)}`
|
|
11301
11475
|
);
|
|
11302
|
-
for (const line of importReportLines(report))
|
|
11303
|
-
|
|
11304
|
-
|
|
11476
|
+
for (const line of importReportLines(report)) say7(dim7(line));
|
|
11477
|
+
say7(
|
|
11478
|
+
dim7(
|
|
11305
11479
|
dryRun ? "--dry-run: nothing written" : "every version replaced is kept \xB7 sessions in the project receive it from the next launch"
|
|
11306
11480
|
)
|
|
11307
11481
|
);
|
|
@@ -11316,7 +11490,7 @@ var memoryCommand = async (config, args) => {
|
|
|
11316
11490
|
const name = rest.find(
|
|
11317
11491
|
(arg, index) => !arg.startsWith("--") && rest[index - 1] !== "--project"
|
|
11318
11492
|
);
|
|
11319
|
-
if (name === void 0) return
|
|
11493
|
+
if (name === void 0) return fail7(usageOf(`memory ${verb}`));
|
|
11320
11494
|
const query = `?path=${encodeURIComponent(memoryPathOf(name))}`;
|
|
11321
11495
|
if (verb === "rm") {
|
|
11322
11496
|
const removed = await api(
|
|
@@ -11324,7 +11498,7 @@ var memoryCommand = async (config, args) => {
|
|
|
11324
11498
|
"DELETE",
|
|
11325
11499
|
`${base}/file${query}`
|
|
11326
11500
|
);
|
|
11327
|
-
return
|
|
11501
|
+
return say7(
|
|
11328
11502
|
removed.removed ? `removed ${name} \xB7 kept as a version` : `${name}: not in memory`
|
|
11329
11503
|
);
|
|
11330
11504
|
}
|
|
@@ -11344,15 +11518,15 @@ var memoryCommand = async (config, args) => {
|
|
|
11344
11518
|
base
|
|
11345
11519
|
);
|
|
11346
11520
|
if (view.files.length === 0) {
|
|
11347
|
-
|
|
11348
|
-
`${project2.name}: no agent memory yet ${
|
|
11521
|
+
say7(
|
|
11522
|
+
`${project2.name}: no agent memory yet ${dim7("\xB7 mend memory import brings this machine's")}`
|
|
11349
11523
|
);
|
|
11350
11524
|
return;
|
|
11351
11525
|
}
|
|
11352
11526
|
const width = Math.max(...view.files.map((file) => file.name.length));
|
|
11353
11527
|
for (const file of view.files) {
|
|
11354
|
-
|
|
11355
|
-
`${file.harness.padEnd(7)} ${file.name.padEnd(width)} ${kilobytes(file.bytes).padStart(8)} ${
|
|
11528
|
+
say7(
|
|
11529
|
+
`${file.harness.padEnd(7)} ${file.name.padEnd(width)} ${kilobytes(file.bytes).padStart(8)} ${dim7(
|
|
11356
11530
|
`${file.updatedAt.slice(0, 16).replace("T", " ")} \xB7 ${file.updatedBySession === null ? "imported" : `session ${file.updatedBySession.slice(0, 8)}`}`
|
|
11357
11531
|
)}`
|
|
11358
11532
|
);
|
|
@@ -11362,17 +11536,17 @@ var secretsCommand = async (config, args) => {
|
|
|
11362
11536
|
const [verb, ...rest] = args;
|
|
11363
11537
|
if (verb === "add" || verb === "rm") {
|
|
11364
11538
|
const given = rest.find((arg, index) => !arg.startsWith("--") && rest[index - 1] !== "--from");
|
|
11365
|
-
if (given === void 0) return
|
|
11539
|
+
if (given === void 0) return fail7(usageOf(`secrets ${verb}`));
|
|
11366
11540
|
const resolved = secretFilePathOf(given, os13.homedir());
|
|
11367
|
-
if (resolved.issue !== null) return
|
|
11541
|
+
if (resolved.issue !== null) return fail7(resolved.issue);
|
|
11368
11542
|
if (verb === "rm") {
|
|
11369
11543
|
const removed = await api(
|
|
11370
11544
|
config,
|
|
11371
11545
|
"DELETE",
|
|
11372
11546
|
`/me/secret-files?path=${encodeURIComponent(resolved.path)}`
|
|
11373
11547
|
);
|
|
11374
|
-
return
|
|
11375
|
-
removed.removed ? `removed ~/${resolved.path} ${
|
|
11548
|
+
return say7(
|
|
11549
|
+
removed.removed ? `removed ~/${resolved.path} ${dim7("\xB7 sessions launched from now on do not receive it")}` : `~/${resolved.path}: not kept`
|
|
11376
11550
|
);
|
|
11377
11551
|
}
|
|
11378
11552
|
const from = takeFlagValue4(rest, "--from");
|
|
@@ -11380,31 +11554,31 @@ var secretsCommand = async (config, args) => {
|
|
|
11380
11554
|
try {
|
|
11381
11555
|
bytes = fs20.readFileSync(from ?? 0);
|
|
11382
11556
|
} catch (error) {
|
|
11383
|
-
return
|
|
11557
|
+
return fail7(
|
|
11384
11558
|
from === null ? `nothing on stdin: pass --from <file> or pipe the content in` : `${from}: ${error instanceof Error ? error.message : String(error)}`
|
|
11385
11559
|
);
|
|
11386
11560
|
}
|
|
11387
11561
|
if (bytes.byteLength === 0)
|
|
11388
|
-
return
|
|
11562
|
+
return fail7(from === null ? "nothing on stdin" : `${from} is empty`);
|
|
11389
11563
|
const saved = await api(config, "PUT", "/me/secret-files", secretFileUploadOf(resolved.path, bytes));
|
|
11390
|
-
|
|
11391
|
-
`${
|
|
11564
|
+
say7(
|
|
11565
|
+
`${green7(saved.action)} ~/${saved.file.path} \xB7 ${saved.file.bytes} B ${dim7(
|
|
11392
11566
|
"\xB7 sessions launched from now on receive it"
|
|
11393
11567
|
)}`
|
|
11394
11568
|
);
|
|
11395
11569
|
return;
|
|
11396
11570
|
}
|
|
11397
|
-
if (verb !== void 0 && verb !== "list") return
|
|
11571
|
+
if (verb !== void 0 && verb !== "list") return fail7(usageOf("secrets"));
|
|
11398
11572
|
const view = await api(
|
|
11399
11573
|
config,
|
|
11400
11574
|
"GET",
|
|
11401
11575
|
"/me/secret-files"
|
|
11402
11576
|
);
|
|
11403
11577
|
if (view.files.length === 0) {
|
|
11404
|
-
|
|
11578
|
+
say7(`no secret files ${dim7("\xB7 mend secrets add <path> --from <file> keeps one")}`);
|
|
11405
11579
|
return;
|
|
11406
11580
|
}
|
|
11407
|
-
for (const line of secretFileLines(view.files))
|
|
11581
|
+
for (const line of secretFileLines(view.files)) say7(line);
|
|
11408
11582
|
};
|
|
11409
11583
|
var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
|
|
11410
11584
|
var readIfExists = (file) => fs20.existsSync(file) ? fs20.readFileSync(file, "utf8") : null;
|
|
@@ -11458,7 +11632,7 @@ var accountsCommand = async (config) => {
|
|
|
11458
11632
|
try {
|
|
11459
11633
|
lines = await accountsLines(config);
|
|
11460
11634
|
} catch (error) {
|
|
11461
|
-
return
|
|
11635
|
+
return fail7(error instanceof Error ? error.message : String(error));
|
|
11462
11636
|
}
|
|
11463
11637
|
for (const line of lines) process.stdout.write(`${line}
|
|
11464
11638
|
`);
|
|
@@ -11470,7 +11644,7 @@ var claudeGrantSecret = () => {
|
|
|
11470
11644
|
var doctorBundle = (config, args) => doctorBundleCommand(args, {
|
|
11471
11645
|
defaultDir: path21.join(mendCliHome(), "bundles"),
|
|
11472
11646
|
now: () => /* @__PURE__ */ new Date(),
|
|
11473
|
-
say:
|
|
11647
|
+
say: say7,
|
|
11474
11648
|
warn: (line) => process.stderr.write(`${line}
|
|
11475
11649
|
`),
|
|
11476
11650
|
collectors: (tail) => bundleCollectors({
|
|
@@ -11515,24 +11689,24 @@ var claudeGrant = async () => {
|
|
|
11515
11689
|
const dir = throwawayLoginDir(home, "claude-login-");
|
|
11516
11690
|
try {
|
|
11517
11691
|
if (grantStatus(cli, dir) === null) {
|
|
11518
|
-
|
|
11692
|
+
fail7(
|
|
11519
11693
|
"claude: could not run `claude auth status` \u2014 install Claude Code, set MEND_CLAUDE_BIN, or paste a credential: mend connect claude --from-stdin"
|
|
11520
11694
|
);
|
|
11521
11695
|
return null;
|
|
11522
11696
|
}
|
|
11523
|
-
|
|
11524
|
-
|
|
11697
|
+
say7(" Mend needs its own Claude login; it is sent to your server and not kept here");
|
|
11698
|
+
say7(dim7(" your own Claude login stays as it is"));
|
|
11525
11699
|
if (!runClaudeLogin(cli, dir)) {
|
|
11526
|
-
|
|
11700
|
+
fail7("claude: the login did not complete");
|
|
11527
11701
|
return null;
|
|
11528
11702
|
}
|
|
11529
11703
|
const status = grantStatus(cli, dir);
|
|
11530
11704
|
if (status === null || !status.loggedIn) {
|
|
11531
|
-
|
|
11705
|
+
fail7("claude: the login completed but Claude still reports no grant in Mend's directory");
|
|
11532
11706
|
return null;
|
|
11533
11707
|
}
|
|
11534
11708
|
if (status.configDirectory !== null && path21.resolve(status.configDirectory) !== path21.resolve(dir)) {
|
|
11535
|
-
|
|
11709
|
+
fail7(
|
|
11536
11710
|
`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`
|
|
11537
11711
|
);
|
|
11538
11712
|
return null;
|
|
@@ -11540,33 +11714,33 @@ var claudeGrant = async () => {
|
|
|
11540
11714
|
const read = readGrant(dir);
|
|
11541
11715
|
if (read.kind === "missing") {
|
|
11542
11716
|
const where = read.triedService === null ? read.triedPath : `${read.triedPath} or the Keychain item ${read.triedService}`;
|
|
11543
|
-
|
|
11717
|
+
fail7(`claude: logged in, but no credential to read \u2014 looked in ${where}`);
|
|
11544
11718
|
return null;
|
|
11545
11719
|
}
|
|
11546
11720
|
const why = staleGrantReason(read.secret);
|
|
11547
11721
|
if (why !== null) {
|
|
11548
|
-
|
|
11722
|
+
fail7(`claude: the new login is already ${why}`);
|
|
11549
11723
|
return null;
|
|
11550
11724
|
}
|
|
11551
11725
|
const mine = localCredential("claude");
|
|
11552
11726
|
if (mine !== null && sameGrant(read.secret, mine)) {
|
|
11553
|
-
|
|
11727
|
+
fail7(
|
|
11554
11728
|
"claude: that is the same grant this machine's Claude holds, so both sides would race on refresh \u2014 connect it deliberately with --use-my-login"
|
|
11555
11729
|
);
|
|
11556
11730
|
return null;
|
|
11557
11731
|
}
|
|
11558
11732
|
const personalAfter = grantStatus(cli, personal);
|
|
11559
11733
|
if (personalBefore?.loggedIn === true && personalAfter?.loggedIn === false) {
|
|
11560
|
-
|
|
11734
|
+
say7(
|
|
11561
11735
|
` 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.`
|
|
11562
11736
|
);
|
|
11563
11737
|
} else if (personalAfter?.loggedIn === true) {
|
|
11564
|
-
|
|
11738
|
+
say7(dim7(" your own Claude login still works \xB7 verified"));
|
|
11565
11739
|
}
|
|
11566
11740
|
const kept = claudeGrantDir(home);
|
|
11567
11741
|
if (fs20.existsSync(kept)) {
|
|
11568
11742
|
forgetGrant(kept);
|
|
11569
|
-
|
|
11743
|
+
say7(dim7(` removed the copy an older mend kept in ${kept} \xB7 the server refreshes the login`));
|
|
11570
11744
|
}
|
|
11571
11745
|
return read.secret;
|
|
11572
11746
|
} finally {
|
|
@@ -11580,12 +11754,12 @@ var connectPi = async (config, flags) => {
|
|
|
11580
11754
|
return;
|
|
11581
11755
|
}
|
|
11582
11756
|
const scan = scanPiProfile(takeFlagValue4(flags, "--dir") ?? piAgentDir());
|
|
11583
|
-
if ("error" in scan) return
|
|
11584
|
-
for (const line of piProfileLines(scan))
|
|
11757
|
+
if ("error" in scan) return fail7(scan.error);
|
|
11758
|
+
for (const line of piProfileLines(scan)) say7(line);
|
|
11585
11759
|
const issue = validatePiProfile(scan.files);
|
|
11586
|
-
if (issue !== null) return
|
|
11760
|
+
if (issue !== null) return fail7(`pi: ${issue}`);
|
|
11587
11761
|
if (flags.includes("--dry-run")) {
|
|
11588
|
-
|
|
11762
|
+
say7(dim7(" --dry-run: nothing sent"));
|
|
11589
11763
|
return;
|
|
11590
11764
|
}
|
|
11591
11765
|
const saved = await withSpinner(
|
|
@@ -11606,13 +11780,13 @@ var connectCommand = async (config, args) => {
|
|
|
11606
11780
|
const [providerArg, ...flags] = args;
|
|
11607
11781
|
if (providerArg === "pi") return connectPi(config, flags);
|
|
11608
11782
|
if (!isProvider(providerArg)) {
|
|
11609
|
-
return
|
|
11783
|
+
return fail7(usageOf("connect"));
|
|
11610
11784
|
}
|
|
11611
11785
|
const provider = providerArg;
|
|
11612
11786
|
if (flags.includes("--remove")) {
|
|
11613
11787
|
const identity = await api(config, "GET", "/me/sealant");
|
|
11614
11788
|
const account2 = identity.accounts.find((row) => row.provider === provider);
|
|
11615
|
-
if (account2 === void 0) return
|
|
11789
|
+
if (account2 === void 0) return fail7(`${provider}: nothing connected`);
|
|
11616
11790
|
await api(config, "DELETE", `/me/sealant/accounts/${account2.id}`);
|
|
11617
11791
|
process.stdout.write(`${provider}: disconnected
|
|
11618
11792
|
`);
|
|
@@ -11621,15 +11795,15 @@ var connectCommand = async (config, args) => {
|
|
|
11621
11795
|
let secret;
|
|
11622
11796
|
if (flags.includes("--from-stdin")) {
|
|
11623
11797
|
secret = fs20.readFileSync(0, "utf8").trim();
|
|
11624
|
-
if (secret === "") return
|
|
11798
|
+
if (secret === "") return fail7("nothing on stdin");
|
|
11625
11799
|
} else if (provider === "codex" && !flags.includes("--use-my-login")) {
|
|
11626
11800
|
const grant = codexGrant({
|
|
11627
11801
|
cli: codexCli(),
|
|
11628
11802
|
personalAuthJson: readIfExists(path21.join(personalCodexHome(), "auth.json")),
|
|
11629
|
-
say:
|
|
11803
|
+
say: say7,
|
|
11630
11804
|
parent: mendCliHome()
|
|
11631
11805
|
});
|
|
11632
|
-
if (grant.kind === "failed") return
|
|
11806
|
+
if (grant.kind === "failed") return fail7(grant.reason);
|
|
11633
11807
|
secret = grant.secret;
|
|
11634
11808
|
} else if (provider === "claude" && !flags.includes("--use-my-login")) {
|
|
11635
11809
|
secret = await claudeGrant();
|
|
@@ -11638,11 +11812,11 @@ var connectCommand = async (config, args) => {
|
|
|
11638
11812
|
secret = localCredential(provider);
|
|
11639
11813
|
if (secret === null) {
|
|
11640
11814
|
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";
|
|
11641
|
-
return
|
|
11815
|
+
return fail7(`${provider}: no credential on this machine \u2014 ${where}`);
|
|
11642
11816
|
}
|
|
11643
11817
|
if (provider === "claude" || provider === "codex") {
|
|
11644
|
-
|
|
11645
|
-
|
|
11818
|
+
say7(
|
|
11819
|
+
dim7(
|
|
11646
11820
|
" --use-my-login: Mend and this machine will share one login, and whichever refreshes second is signed out"
|
|
11647
11821
|
)
|
|
11648
11822
|
);
|
|
@@ -11650,7 +11824,7 @@ var connectCommand = async (config, args) => {
|
|
|
11650
11824
|
}
|
|
11651
11825
|
const narrowed = narrowCredential(provider, secret);
|
|
11652
11826
|
if (narrowed.kind === "narrowed" && narrowed.dropped.length > 0) {
|
|
11653
|
-
|
|
11827
|
+
say7(dim7(` keeping ${narrowed.dropped.join(", ")} on this machine`));
|
|
11654
11828
|
}
|
|
11655
11829
|
const account = await withSpinner(
|
|
11656
11830
|
`connecting ${provider}`,
|
|
@@ -11663,8 +11837,8 @@ var connectCommand = async (config, args) => {
|
|
|
11663
11837
|
`);
|
|
11664
11838
|
const facts = claudeGrantFacts(narrowed.secret);
|
|
11665
11839
|
if (facts !== null) {
|
|
11666
|
-
|
|
11667
|
-
|
|
11840
|
+
say7(
|
|
11841
|
+
dim7(
|
|
11668
11842
|
` access expires ${minuteOrUnknown(facts.accessExpiresAt)} \xB7 grant expires ${minuteOrUnknown(facts.refreshExpiresAt)}`
|
|
11669
11843
|
)
|
|
11670
11844
|
);
|
|
@@ -11690,25 +11864,25 @@ var login = async (config, args) => {
|
|
|
11690
11864
|
defaultUrl: config.url,
|
|
11691
11865
|
save: (next) => {
|
|
11692
11866
|
saveCliConfig({ url: next.url, token: next.token, deviceId: next.deviceId });
|
|
11693
|
-
|
|
11867
|
+
say7(dim7(` token saved to ${CONFIG_PATH} (0600)`));
|
|
11694
11868
|
}
|
|
11695
11869
|
});
|
|
11696
11870
|
};
|
|
11697
11871
|
var logout = async (config) => {
|
|
11698
11872
|
if (!fs20.existsSync(CONFIG_PATH) && config.token === null) {
|
|
11699
|
-
|
|
11873
|
+
say7(dim7("nothing saved \u2014 already signed out"));
|
|
11700
11874
|
return;
|
|
11701
11875
|
}
|
|
11702
11876
|
if (config.token !== null && config.deviceId !== null) {
|
|
11703
11877
|
try {
|
|
11704
11878
|
await request(config, "DELETE", `/me/devices/${config.deviceId}`);
|
|
11705
|
-
|
|
11879
|
+
say7(`${green7("\u2713")} device revoked on ${config.url}`);
|
|
11706
11880
|
} catch {
|
|
11707
|
-
|
|
11881
|
+
say7(dim7(" could not revoke on the server \u2014 end it under Settings \u2192 Devices"));
|
|
11708
11882
|
}
|
|
11709
11883
|
}
|
|
11710
11884
|
saveCliConfig({ ...config, token: null, deviceId: null });
|
|
11711
|
-
|
|
11885
|
+
say7(`${green7("\u2713")} signed out \xB7 ${dim7(`token removed from ${CONFIG_PATH}`)}`);
|
|
11712
11886
|
};
|
|
11713
11887
|
var UNINSTALL_CHOICES = [
|
|
11714
11888
|
{ scope: "all", text: "everything \xB7 the server on this machine and this machine's Mend files" },
|
|
@@ -11720,20 +11894,20 @@ var UNINSTALL_CHOICES = [
|
|
|
11720
11894
|
];
|
|
11721
11895
|
var askUninstallScope = async () => {
|
|
11722
11896
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
11723
|
-
return
|
|
11897
|
+
return fail7(`${UNINSTALL_USAGE} \xB7 no terminal to ask on`);
|
|
11724
11898
|
}
|
|
11725
|
-
|
|
11726
|
-
UNINSTALL_CHOICES.forEach((choice, index) =>
|
|
11899
|
+
say7("what should go?");
|
|
11900
|
+
UNINSTALL_CHOICES.forEach((choice, index) => say7(` ${index + 1}. ${choice.text}`));
|
|
11727
11901
|
const readline = await import("node:readline/promises");
|
|
11728
11902
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11729
11903
|
const answer = (await rl.question(" 1, 2 or 3: ")).trim();
|
|
11730
11904
|
rl.close();
|
|
11731
11905
|
const chosen = UNINSTALL_CHOICES[Number(answer) - 1];
|
|
11732
|
-
if (chosen === void 0) return
|
|
11906
|
+
if (chosen === void 0) return fail7(`"${answer}" is not one of the choices`);
|
|
11733
11907
|
return chosen.scope;
|
|
11734
11908
|
};
|
|
11735
11909
|
var confirmUninstall = async (word) => {
|
|
11736
|
-
if (process.stdin.isTTY !== true) return
|
|
11910
|
+
if (process.stdin.isTTY !== true) return fail7("non-interactive \u2014 pass --yes to remove");
|
|
11737
11911
|
const readline = await import("node:readline/promises");
|
|
11738
11912
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11739
11913
|
const answer = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
|
|
@@ -11742,7 +11916,7 @@ var confirmUninstall = async (word) => {
|
|
|
11742
11916
|
};
|
|
11743
11917
|
var uninstallCommand = async (config, args) => {
|
|
11744
11918
|
const parsed = parseUninstallArgs(args);
|
|
11745
|
-
if ("error" in parsed) return
|
|
11919
|
+
if ("error" in parsed) return fail7(parsed.error);
|
|
11746
11920
|
const scope = parsed.scope ?? await askUninstallScope();
|
|
11747
11921
|
const server = nodeServerRuntime();
|
|
11748
11922
|
const runtime = {
|
|
@@ -11761,42 +11935,42 @@ var uninstallCommand = async (config, args) => {
|
|
|
11761
11935
|
}
|
|
11762
11936
|
};
|
|
11763
11937
|
const plan = await describeUninstall(runtime, scope);
|
|
11764
|
-
|
|
11765
|
-
for (const line of planLines(plan, server.configDir))
|
|
11938
|
+
say7(dim7(`mend uninstall \xB7 ${scope === "all" ? "everything" : scope}`));
|
|
11939
|
+
for (const line of planLines(plan, server.configDir)) say7(` ${line}`);
|
|
11766
11940
|
const nothing = (plan.server === null || plan.server === "none") && (plan.home === null || plan.home.cliConfig === null && plan.home.sshDirectory === null && plan.home.managedSshBlocks === 0);
|
|
11767
11941
|
if (nothing) {
|
|
11768
|
-
|
|
11942
|
+
say7(dim7("nothing to remove"));
|
|
11769
11943
|
return;
|
|
11770
11944
|
}
|
|
11771
11945
|
if (planDeletesData(plan)) {
|
|
11772
|
-
|
|
11946
|
+
say7("repositories, worktrees, the database and its backups are deleted with the server");
|
|
11773
11947
|
}
|
|
11774
11948
|
if (!parsed.yes && !await confirmUninstall(planDeletesData(plan) ? "delete" : "y")) {
|
|
11775
|
-
|
|
11949
|
+
say7(dim7("nothing removed"));
|
|
11776
11950
|
return;
|
|
11777
11951
|
}
|
|
11778
11952
|
const outcome = await executeUninstall(runtime, plan);
|
|
11779
|
-
for (const line of outcome.leftovers)
|
|
11953
|
+
for (const line of outcome.leftovers) say7(dim7(` kept \xB7 ${line}`));
|
|
11780
11954
|
if (outcome.failures.length > 0) {
|
|
11781
|
-
return
|
|
11955
|
+
return fail7(outcome.failures.join("\n"));
|
|
11782
11956
|
}
|
|
11783
|
-
|
|
11784
|
-
`${
|
|
11957
|
+
say7(
|
|
11958
|
+
`${green7("\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"}`
|
|
11785
11959
|
);
|
|
11786
11960
|
};
|
|
11787
11961
|
var printGitKey = (key) => {
|
|
11788
11962
|
if (key.publicKey === null) return;
|
|
11789
|
-
|
|
11790
|
-
if (key.fingerprint !== null)
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
|
|
11963
|
+
say7(key.publicKey);
|
|
11964
|
+
if (key.fingerprint !== null) say7(dim7(` ${key.fingerprint}`));
|
|
11965
|
+
say7(dim7(" add this to your git account's SSH keys (GitHub: settings \u2192 SSH keys) so"));
|
|
11966
|
+
say7(dim7(" every repository you can reach works; for one repository only, add it"));
|
|
11967
|
+
say7(dim7(" as that repository's deploy key instead (grant write if sessions push)"));
|
|
11794
11968
|
};
|
|
11795
11969
|
var keysShow = async (config) => {
|
|
11796
11970
|
const key = await api(config, "GET", "/keys/git");
|
|
11797
11971
|
if (!key.exists) {
|
|
11798
|
-
|
|
11799
|
-
`no Mend key yet ${
|
|
11972
|
+
say7(
|
|
11973
|
+
`no Mend key yet ${dim7("\u2014 mend keys init generates one (ed25519, stays on the server host)")}`
|
|
11800
11974
|
);
|
|
11801
11975
|
return;
|
|
11802
11976
|
}
|
|
@@ -11804,13 +11978,13 @@ var keysShow = async (config) => {
|
|
|
11804
11978
|
};
|
|
11805
11979
|
var keysInit = async (config) => {
|
|
11806
11980
|
const key = await api(config, "POST", "/keys/git");
|
|
11807
|
-
|
|
11981
|
+
say7(`${green7("\u2713")} Mend key ready ${dim7("(private half stays on the server host)")}`);
|
|
11808
11982
|
printGitKey(key);
|
|
11809
11983
|
};
|
|
11810
11984
|
var keysShare = async (config) => {
|
|
11811
11985
|
const agentSock = process.env["SSH_AUTH_SOCK"];
|
|
11812
11986
|
if (agentSock === void 0 || agentSock === "") {
|
|
11813
|
-
return
|
|
11987
|
+
return fail7("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
|
|
11814
11988
|
}
|
|
11815
11989
|
await shareAgent({
|
|
11816
11990
|
url: () => socketUrl(config, "keys-bridge", { host: os13.hostname() }),
|
|
@@ -11819,26 +11993,26 @@ var keysShare = async (config) => {
|
|
|
11819
11993
|
onEvent: (event) => {
|
|
11820
11994
|
switch (event.kind) {
|
|
11821
11995
|
case "connected":
|
|
11822
|
-
|
|
11823
|
-
|
|
11996
|
+
say7(`${green7("\u25CF")} sharing this machine's ssh-agent with ${config.url}`);
|
|
11997
|
+
say7(dim7(` agent: ${agentSock} \xB7 signature requests print here \xB7 Ctrl-C stops sharing`));
|
|
11824
11998
|
return;
|
|
11825
11999
|
case "disconnected":
|
|
11826
|
-
|
|
11827
|
-
|
|
12000
|
+
say7(
|
|
12001
|
+
dim7(`not connected \u2014 retrying in ${Math.round(event.retryMs / 1e3)}s (Ctrl-C stops)`)
|
|
11828
12002
|
);
|
|
11829
12003
|
return;
|
|
11830
12004
|
case "sign-requested":
|
|
11831
|
-
|
|
11832
|
-
|
|
12005
|
+
say7(`${amber2("\u270E")} signature requested by mend ${dim7(`(${event.context})`)}`);
|
|
12006
|
+
say7(dim7(" waiting \u2014 touch your key if it blinks (up to 60s)\u2026"));
|
|
11833
12007
|
return;
|
|
11834
12008
|
case "identities-requested":
|
|
11835
|
-
|
|
12009
|
+
say7(dim7(` identities requested (${event.context})`));
|
|
11836
12010
|
return;
|
|
11837
12011
|
case "signed":
|
|
11838
|
-
|
|
12012
|
+
say7(`${green7("\u2713")} signed ${dim7(`(${event.seconds.toFixed(1)}s)`)}`);
|
|
11839
12013
|
return;
|
|
11840
12014
|
case "not-signed":
|
|
11841
|
-
|
|
12015
|
+
say7(`${amber2("\u2717")} not signed \u2014 ${event.message}`);
|
|
11842
12016
|
return;
|
|
11843
12017
|
}
|
|
11844
12018
|
}
|
|
@@ -11848,15 +12022,15 @@ var gitAuthorCommand = async (config, args) => {
|
|
|
11848
12022
|
const asked = parseGitAuthorArgs(args);
|
|
11849
12023
|
switch (asked.kind) {
|
|
11850
12024
|
case "usage":
|
|
11851
|
-
return
|
|
12025
|
+
return fail7(usageOf("git-author"));
|
|
11852
12026
|
case "invalid":
|
|
11853
|
-
return
|
|
12027
|
+
return fail7(asked.message);
|
|
11854
12028
|
case "show":
|
|
11855
|
-
|
|
12029
|
+
say7(gitAuthorLine(await api(config, "GET", "/me/git-author")));
|
|
11856
12030
|
return;
|
|
11857
12031
|
case "clear": {
|
|
11858
12032
|
const author = await api(config, "DELETE", "/me/git-author");
|
|
11859
|
-
|
|
12033
|
+
say7(`${green7("\u2713")} git author \xB7 ${gitAuthorLine(author)}`);
|
|
11860
12034
|
return;
|
|
11861
12035
|
}
|
|
11862
12036
|
case "set": {
|
|
@@ -11864,9 +12038,9 @@ var gitAuthorCommand = async (config, args) => {
|
|
|
11864
12038
|
name: asked.name,
|
|
11865
12039
|
email: asked.email
|
|
11866
12040
|
});
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
12041
|
+
say7(`${green7("\u2713")} git author \xB7 ${gitAuthorLine(author)}`);
|
|
12042
|
+
say7(
|
|
12043
|
+
dim7(" sessions launched from now on commit as this, unless their dotfiles say otherwise")
|
|
11870
12044
|
);
|
|
11871
12045
|
return;
|
|
11872
12046
|
}
|
|
@@ -11875,19 +12049,19 @@ var gitAuthorCommand = async (config, args) => {
|
|
|
11875
12049
|
var keysMode = async (config, args) => {
|
|
11876
12050
|
const [value] = args;
|
|
11877
12051
|
if (value !== void 0 && value !== "mend-key" && value !== "bridge") {
|
|
11878
|
-
return
|
|
12052
|
+
return fail7(`keys mode takes mend-key or bridge, not "${value}"`);
|
|
11879
12053
|
}
|
|
11880
12054
|
const access = value === void 0 ? await api(config, "GET", "/me/git-access") : await api(config, "PUT", "/me/git-access", { mode: value });
|
|
11881
|
-
if (value !== void 0)
|
|
12055
|
+
if (value !== void 0) say7(`${green7("\u2713")} git access \xB7 ${value}`);
|
|
11882
12056
|
if (access.mode === "mend-key") {
|
|
11883
|
-
|
|
12057
|
+
say7(`mend-key ${dim7("\u2014 your Mend key on the server signs; works whenever the server is up")}`);
|
|
11884
12058
|
if (access.key.exists) printGitKey(access.key);
|
|
11885
|
-
else
|
|
12059
|
+
else say7(dim7(" no key yet \u2014 mend keys init creates it"));
|
|
11886
12060
|
return;
|
|
11887
12061
|
}
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
access.bridge.connected ? `${
|
|
12062
|
+
say7(`bridge ${dim7("\u2014 this machine's ssh-agent signs; only while a mend command runs here")}`);
|
|
12063
|
+
say7(
|
|
12064
|
+
access.bridge.connected ? `${green7("\u25CF")} signer connected \xB7 ${access.bridge.clientName ?? "unknown machine"}` : dim7(" no signer connected \u2014 mend shares the agent whenever it runs (or: mend keys share)")
|
|
11891
12065
|
);
|
|
11892
12066
|
};
|
|
11893
12067
|
var keysCommand = async (config, args) => {
|
|
@@ -11903,42 +12077,42 @@ var keysCommand = async (config, args) => {
|
|
|
11903
12077
|
case void 0:
|
|
11904
12078
|
return keysShow(config);
|
|
11905
12079
|
default:
|
|
11906
|
-
return
|
|
12080
|
+
return fail7(`unknown keys command "${verb}" \xB7 mend help keys lists them`);
|
|
11907
12081
|
}
|
|
11908
12082
|
};
|
|
11909
12083
|
var formatDotfileBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
11910
|
-
var repositoryLine = (repository) => repository === null ? `repo ${
|
|
12084
|
+
var repositoryLine = (repository) => repository === null ? `repo ${dim7("none")}` : `repo ${repository.url} ${dim7(`(${dotfilesRepositoryFacts(repository)})`)}`;
|
|
11911
12085
|
var dotfilesShow = async (config) => {
|
|
11912
12086
|
const dotfiles = await api(config, "GET", "/dotfiles");
|
|
11913
|
-
|
|
12087
|
+
say7(repositoryLine(dotfiles.repository));
|
|
11914
12088
|
if (dotfiles.snapshot === null) {
|
|
11915
|
-
|
|
12089
|
+
say7(`snapshot ${dim7("none \u2014 sync from this machine: mend dotfiles sync --all")}`);
|
|
11916
12090
|
return;
|
|
11917
12091
|
}
|
|
11918
12092
|
const snapshot = dotfiles.snapshot;
|
|
11919
|
-
|
|
11920
|
-
`snapshot ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} \xB7 from ${snapshot.source} \xB7 ${
|
|
12093
|
+
say7(
|
|
12094
|
+
`snapshot ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} \xB7 from ${snapshot.source} \xB7 ${dim7(snapshot.sha.slice(0, 7))}`
|
|
11921
12095
|
);
|
|
11922
12096
|
for (const file of snapshot.files) {
|
|
11923
|
-
|
|
12097
|
+
say7(` ${file.path.padEnd(36)} ${dim7(formatDotfileBytes(file.bytes))}`);
|
|
11924
12098
|
}
|
|
11925
12099
|
};
|
|
11926
12100
|
var dotfilesRepo = async (config, args) => {
|
|
11927
12101
|
const parsed = parseDotfilesRepoArgs(args);
|
|
11928
|
-
if (parsed.kind === "error") return
|
|
12102
|
+
if (parsed.kind === "error") return fail7(`${parsed.error}
|
|
11929
12103
|
${usageOf("dotfiles repo")}`);
|
|
11930
12104
|
const repository = parsed.kind === "clear" ? null : parsed.repository;
|
|
11931
|
-
if (repository !== null)
|
|
12105
|
+
if (repository !== null) say7(dim7(`cloning ${repository.url} on the server to check it\u2026`));
|
|
11932
12106
|
const result = await api(config, "PUT", "/dotfiles/repository", { repository });
|
|
11933
12107
|
if (result.repository === null) {
|
|
11934
|
-
|
|
11935
|
-
`${
|
|
12108
|
+
say7(
|
|
12109
|
+
`${green7("\u2713")} cleared the dotfiles repository ${dim7("\u2014 applies from the next session launch")}`
|
|
11936
12110
|
);
|
|
11937
12111
|
return;
|
|
11938
12112
|
}
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
12113
|
+
say7(`${green7("\u2713")} saved \xB7 the server cloned it once to check`);
|
|
12114
|
+
say7(repositoryLine(result.repository));
|
|
12115
|
+
say7(dim7("applies from the next session launch"));
|
|
11942
12116
|
};
|
|
11943
12117
|
var dotfilesSync = async (config, args) => {
|
|
11944
12118
|
const all = args.includes("--all");
|
|
@@ -11947,36 +12121,36 @@ var dotfilesSync = async (config, args) => {
|
|
|
11947
12121
|
if (!all && requestedPaths.length === 0) {
|
|
11948
12122
|
const found = scanDotfileCandidates(home);
|
|
11949
12123
|
if (found.length === 0) {
|
|
11950
|
-
|
|
12124
|
+
say7(dim7("no known config files found under ~"));
|
|
11951
12125
|
return;
|
|
11952
12126
|
}
|
|
11953
12127
|
let group = "";
|
|
11954
12128
|
for (const entry of found) {
|
|
11955
12129
|
if (entry.group !== group) {
|
|
11956
12130
|
group = entry.group;
|
|
11957
|
-
|
|
12131
|
+
say7(dim7(group));
|
|
11958
12132
|
}
|
|
11959
|
-
|
|
12133
|
+
say7(` ${entry.path.padEnd(36)} ${dim7(formatDotfileBytes(entry.bytes))}`);
|
|
11960
12134
|
}
|
|
11961
|
-
|
|
11962
|
-
|
|
12135
|
+
say7("");
|
|
12136
|
+
say7(
|
|
11963
12137
|
`sync everything with ${cobalt2("mend dotfiles sync --all")}, or pick: ${cobalt2("mend dotfiles sync .zshrc .gitconfig")}`
|
|
11964
12138
|
);
|
|
11965
12139
|
return;
|
|
11966
12140
|
}
|
|
11967
12141
|
const selected = all ? scanDotfileCandidates(home).map((entry) => entry.path) : requestedPaths;
|
|
11968
|
-
if (selected.length === 0) return
|
|
12142
|
+
if (selected.length === 0) return fail7("nothing to sync \u2014 no known config files found under ~");
|
|
11969
12143
|
const read = readSyncFiles(home, selected);
|
|
11970
|
-
if ("error" in read) return
|
|
12144
|
+
if ("error" in read) return fail7(read.error);
|
|
11971
12145
|
const result = await api(config, "POST", "/dotfiles/snapshot", {
|
|
11972
12146
|
files: read.files,
|
|
11973
12147
|
source: os13.hostname(),
|
|
11974
12148
|
merge: false
|
|
11975
12149
|
});
|
|
11976
12150
|
const snapshot = result.snapshot;
|
|
11977
|
-
if (snapshot === null) return
|
|
11978
|
-
|
|
11979
|
-
`${
|
|
12151
|
+
if (snapshot === null) return fail7("the server accepted the sync but reports no snapshot");
|
|
12152
|
+
say7(
|
|
12153
|
+
`${green7("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os13.hostname()} \xB7 ${dim7(snapshot.sha.slice(0, 7))} ${dim7("\u2014 applies from the next session launch")}`
|
|
11980
12154
|
);
|
|
11981
12155
|
};
|
|
11982
12156
|
var envLoad = async (config, args) => {
|
|
@@ -11995,10 +12169,10 @@ var envLoad = async (config, args) => {
|
|
|
11995
12169
|
try {
|
|
11996
12170
|
contents = fs20.readFileSync(file, "utf8");
|
|
11997
12171
|
} catch {
|
|
11998
|
-
return
|
|
12172
|
+
return fail7(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
|
|
11999
12173
|
}
|
|
12000
12174
|
if (contents.trim() === "") {
|
|
12001
|
-
|
|
12175
|
+
say7(dim7(`${file} is empty`));
|
|
12002
12176
|
return;
|
|
12003
12177
|
}
|
|
12004
12178
|
const project2 = await findProject(config, explicitProject);
|
|
@@ -12009,30 +12183,30 @@ var envLoad = async (config, args) => {
|
|
|
12009
12183
|
{ contents, allSecret, secretNames }
|
|
12010
12184
|
);
|
|
12011
12185
|
if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
|
|
12012
|
-
|
|
12186
|
+
say7(dim7(`${path21.basename(file)} has no variables`));
|
|
12013
12187
|
return;
|
|
12014
12188
|
}
|
|
12015
|
-
|
|
12016
|
-
for (const line of formatLoadReport(report, { dim:
|
|
12189
|
+
say7(`${green7("\u2713")} loaded ${path21.basename(file)} into ${project2.name}`);
|
|
12190
|
+
for (const line of formatLoadReport(report, { dim: dim7, warn: amber2 })) say7(line);
|
|
12017
12191
|
const plaintextUrls = report.loaded.filter(
|
|
12018
12192
|
(entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
|
|
12019
12193
|
);
|
|
12020
12194
|
if (plaintextUrls.length > 0) {
|
|
12021
|
-
|
|
12195
|
+
say7(
|
|
12022
12196
|
amber2(
|
|
12023
12197
|
` ${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(",")}`
|
|
12024
12198
|
)
|
|
12025
12199
|
);
|
|
12026
12200
|
}
|
|
12027
12201
|
if (report.malformedLines.length > 0) {
|
|
12028
|
-
|
|
12202
|
+
say7(
|
|
12029
12203
|
amber2(
|
|
12030
12204
|
` skipped ${report.malformedLines.length} malformed line${report.malformedLines.length === 1 ? "" : "s"}: ${report.malformedLines.join(", ")}`
|
|
12031
12205
|
)
|
|
12032
12206
|
);
|
|
12033
12207
|
}
|
|
12034
|
-
|
|
12035
|
-
|
|
12208
|
+
say7(
|
|
12209
|
+
dim7(
|
|
12036
12210
|
` 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`
|
|
12037
12211
|
)
|
|
12038
12212
|
);
|
|
@@ -12044,11 +12218,11 @@ var envShow = async (config, args) => {
|
|
|
12044
12218
|
api(config, "GET", `/projects/${project2.id}/secrets`),
|
|
12045
12219
|
api(config, "GET", `/projects/${project2.id}/cluster-bindings`)
|
|
12046
12220
|
]);
|
|
12047
|
-
|
|
12048
|
-
`${project2.name} ${
|
|
12221
|
+
say7(
|
|
12222
|
+
`${project2.name} ${dim7(`\xB7 configuration r${environment.revision} \xB7 secrets r${secrets.revision} \xB7 cluster r${cluster.revision}`)}`
|
|
12049
12223
|
);
|
|
12050
12224
|
if (environment.variables.length === 0 && secrets.secrets.length === 0 && cluster.bindings.length === 0 && cluster.serviceAccount === null) {
|
|
12051
|
-
|
|
12225
|
+
say7(dim7(` nothing stored \u2014 load a file: ${cobalt2("mend env load")}`));
|
|
12052
12226
|
return;
|
|
12053
12227
|
}
|
|
12054
12228
|
const bindingNames = cluster.bindings.map((b) => `${b.kind}/${b.objectName}`);
|
|
@@ -12059,23 +12233,23 @@ var envShow = async (config, args) => {
|
|
|
12059
12233
|
...bindingNames.map((name) => name.length)
|
|
12060
12234
|
);
|
|
12061
12235
|
for (const variable of environment.variables) {
|
|
12062
|
-
|
|
12236
|
+
say7(` ${variable.name.padEnd(width)} configuration ${dim7("\xB7 plaintext")}`);
|
|
12063
12237
|
}
|
|
12064
12238
|
for (const secret of secrets.secrets) {
|
|
12065
|
-
|
|
12239
|
+
say7(` ${secret.name.padEnd(width)} secret ${dim7("\xB7 value set, never shown")}`);
|
|
12066
12240
|
}
|
|
12067
12241
|
for (const name of bindingNames) {
|
|
12068
|
-
|
|
12069
|
-
` ${name.padEnd(width)} cluster binding ${
|
|
12242
|
+
say7(
|
|
12243
|
+
` ${name.padEnd(width)} cluster binding ${dim7("\xB7 resolved by the platform at launch \xB7 contents unknown to Mend")}`
|
|
12070
12244
|
);
|
|
12071
12245
|
}
|
|
12072
12246
|
if (cluster.serviceAccount !== null) {
|
|
12073
|
-
|
|
12074
|
-
` ${cluster.serviceAccount.padEnd(width)} service account ${
|
|
12247
|
+
say7(
|
|
12248
|
+
` ${cluster.serviceAccount.padEnd(width)} service account ${dim7("\xB7 workspace pod identity \xB7 allowlisted by the operator")}`
|
|
12075
12249
|
);
|
|
12076
12250
|
}
|
|
12077
12251
|
if (!cluster.clusterCapable && (cluster.bindings.length > 0 || cluster.serviceAccount !== null)) {
|
|
12078
|
-
|
|
12252
|
+
say7(
|
|
12079
12253
|
amber2(
|
|
12080
12254
|
` ${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`
|
|
12081
12255
|
)
|
|
@@ -12089,48 +12263,48 @@ var envCluster = async (config, args) => {
|
|
|
12089
12263
|
const usage2 = usageOf("env cluster");
|
|
12090
12264
|
const project2 = await findProject(config, explicitProject);
|
|
12091
12265
|
const route = `/projects/${project2.id}/cluster-bindings`;
|
|
12092
|
-
const appliesLine = () =>
|
|
12093
|
-
|
|
12266
|
+
const appliesLine = () => say7(
|
|
12267
|
+
dim7(
|
|
12094
12268
|
" applies from the next workspace launch; running workspaces keep what they started with"
|
|
12095
12269
|
)
|
|
12096
12270
|
);
|
|
12097
12271
|
if (verb === "add") {
|
|
12098
12272
|
const [kind, objectName] = rest;
|
|
12099
12273
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
12100
|
-
return
|
|
12274
|
+
return fail7(`env cluster add takes a kind and an object name
|
|
12101
12275
|
${usage2}`);
|
|
12102
12276
|
}
|
|
12103
12277
|
const result = await api(config, "POST", route, {
|
|
12104
12278
|
kind,
|
|
12105
12279
|
objectName
|
|
12106
12280
|
});
|
|
12107
|
-
|
|
12108
|
-
|
|
12281
|
+
say7(`${green7("\u2713")} bound ${kind}/${objectName} ${dim7(`\xB7 cluster r${result.revision}`)}`);
|
|
12282
|
+
say7(dim7(" resolved by the platform at launch \xB7 contents unknown to Mend"));
|
|
12109
12283
|
return appliesLine();
|
|
12110
12284
|
}
|
|
12111
12285
|
if (verb === "remove") {
|
|
12112
12286
|
const [ref] = rest;
|
|
12113
12287
|
const [kind, objectName] = ref?.split("/", 2) ?? [];
|
|
12114
12288
|
if (kind !== "secret" && kind !== "configmap" || objectName === void 0) {
|
|
12115
|
-
return
|
|
12289
|
+
return fail7(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
|
|
12116
12290
|
${usage2}`);
|
|
12117
12291
|
}
|
|
12118
12292
|
const snapshot = await api(config, "GET", route);
|
|
12119
12293
|
const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
|
|
12120
|
-
if (binding === void 0) return
|
|
12294
|
+
if (binding === void 0) return fail7(`${kind}/${objectName} is not bound on ${project2.name}`);
|
|
12121
12295
|
const result = await api(
|
|
12122
12296
|
config,
|
|
12123
12297
|
"DELETE",
|
|
12124
12298
|
`${route}/${binding.id}`
|
|
12125
12299
|
);
|
|
12126
|
-
|
|
12300
|
+
say7(`${green7("\u2713")} removed ${kind}/${objectName} ${dim7(`\xB7 cluster r${result.revision}`)}`);
|
|
12127
12301
|
return appliesLine();
|
|
12128
12302
|
}
|
|
12129
12303
|
if (verb === "sa") {
|
|
12130
12304
|
const clear = args.includes("--clear");
|
|
12131
12305
|
const [name] = rest;
|
|
12132
12306
|
if (!clear && name === void 0)
|
|
12133
|
-
return
|
|
12307
|
+
return fail7(`env cluster sa takes a name or --clear
|
|
12134
12308
|
${usage2}`);
|
|
12135
12309
|
const result = await api(
|
|
12136
12310
|
config,
|
|
@@ -12138,11 +12312,11 @@ ${usage2}`);
|
|
|
12138
12312
|
`${route}/service-account`,
|
|
12139
12313
|
{ serviceAccount: clear ? null : name }
|
|
12140
12314
|
);
|
|
12141
|
-
|
|
12142
|
-
result.serviceAccount === null ? `${
|
|
12315
|
+
say7(
|
|
12316
|
+
result.serviceAccount === null ? `${green7("\u2713")} cleared the workspace service account ${dim7(`\xB7 cluster r${result.revision}`)}` : `${green7("\u2713")} service account ${result.serviceAccount} ${dim7(`\xB7 cluster r${result.revision}`)}`
|
|
12143
12317
|
);
|
|
12144
12318
|
if (result.serviceAccount !== null) {
|
|
12145
|
-
|
|
12319
|
+
say7(
|
|
12146
12320
|
amber2(
|
|
12147
12321
|
" 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"
|
|
12148
12322
|
)
|
|
@@ -12150,7 +12324,7 @@ ${usage2}`);
|
|
|
12150
12324
|
}
|
|
12151
12325
|
return appliesLine();
|
|
12152
12326
|
}
|
|
12153
|
-
return
|
|
12327
|
+
return fail7(`unknown env cluster command "${verb ?? ""}"
|
|
12154
12328
|
${usage2}`);
|
|
12155
12329
|
};
|
|
12156
12330
|
var envCommand = async (config, args) => {
|
|
@@ -12164,7 +12338,7 @@ var envCommand = async (config, args) => {
|
|
|
12164
12338
|
case void 0:
|
|
12165
12339
|
return envShow(config, rest);
|
|
12166
12340
|
default:
|
|
12167
|
-
return
|
|
12341
|
+
return fail7(`unknown env command "${verb}" \xB7 mend help env lists them`);
|
|
12168
12342
|
}
|
|
12169
12343
|
};
|
|
12170
12344
|
var dotfilesCommand = async (config, args) => {
|
|
@@ -12178,7 +12352,7 @@ var dotfilesCommand = async (config, args) => {
|
|
|
12178
12352
|
case void 0:
|
|
12179
12353
|
return dotfilesShow(config);
|
|
12180
12354
|
default:
|
|
12181
|
-
return
|
|
12355
|
+
return fail7(`unknown dotfiles command "${verb}" \xB7 mend help dotfiles lists them`);
|
|
12182
12356
|
}
|
|
12183
12357
|
};
|
|
12184
12358
|
var skillsList = async (config, args) => {
|
|
@@ -12191,23 +12365,23 @@ var skillsList = async (config, args) => {
|
|
|
12191
12365
|
);
|
|
12192
12366
|
const library = project2 === null ? "your library" : project2.name;
|
|
12193
12367
|
if (skills.length === 0) {
|
|
12194
|
-
|
|
12368
|
+
say7(dim7(`no skills in ${library} \u2014 push a local one: mend skills push`));
|
|
12195
12369
|
return;
|
|
12196
12370
|
}
|
|
12197
12371
|
for (const skill of skills) {
|
|
12198
|
-
|
|
12199
|
-
` ${skill.name.padEnd(28)} ${
|
|
12372
|
+
say7(
|
|
12373
|
+
` ${skill.name.padEnd(28)} ${dim7(`${skill.fileCount} file${skill.fileCount === 1 ? "" : "s"} \xB7 ${formatDotfileBytes(skill.bytes)}`)}`
|
|
12200
12374
|
);
|
|
12201
|
-
if (skill.description !== "")
|
|
12375
|
+
if (skill.description !== "") say7(` ${dim7(skill.description)}`);
|
|
12202
12376
|
}
|
|
12203
12377
|
};
|
|
12204
12378
|
var skillsPush = async (config, args) => {
|
|
12205
12379
|
const dirFlag = takeFlagValue4(args, "--dir");
|
|
12206
12380
|
const root = dirFlag === null ? path21.join(os13.homedir(), DEFAULT_SKILLS_DIR) : path21.resolve(dirFlag);
|
|
12207
12381
|
const scanned = scanSkillLibrary(root);
|
|
12208
|
-
if ("error" in scanned) return
|
|
12209
|
-
for (const note of scanned.notes)
|
|
12210
|
-
if (scanned.skills.length === 0) return
|
|
12382
|
+
if ("error" in scanned) return fail7(scanned.error);
|
|
12383
|
+
for (const note of scanned.notes) say7(dim7(` ${note.skill}: ${note.message}`));
|
|
12384
|
+
if (scanned.skills.length === 0) return fail7(`no skills found under ${root}`);
|
|
12211
12385
|
const forProject = args.includes("--project");
|
|
12212
12386
|
const project2 = forProject ? await findProject(config, takeFlagValue4(args, "--project")) : null;
|
|
12213
12387
|
const report = await api(config, "POST", "/skills/sync", {
|
|
@@ -12227,10 +12401,10 @@ var skillsPush = async (config, args) => {
|
|
|
12227
12401
|
`${report.unchanged.length} unchanged`,
|
|
12228
12402
|
...report.removed.length > 0 ? [`${report.removed.length} removed`] : []
|
|
12229
12403
|
].join(" \xB7 ");
|
|
12230
|
-
|
|
12231
|
-
`${
|
|
12404
|
+
say7(
|
|
12405
|
+
`${green7("pushed")} ${scanned.skills.length} skill${scanned.skills.length === 1 ? "" : "s"} from ${root} to ${library} \xB7 ${dim7(counts)}`
|
|
12232
12406
|
);
|
|
12233
|
-
|
|
12407
|
+
say7(dim7("sessions receive them from the next launch"));
|
|
12234
12408
|
};
|
|
12235
12409
|
var skillsCommand = async (config, args) => {
|
|
12236
12410
|
const [verb, ...rest] = args;
|
|
@@ -12243,7 +12417,7 @@ var skillsCommand = async (config, args) => {
|
|
|
12243
12417
|
return skillsList(config, args);
|
|
12244
12418
|
default:
|
|
12245
12419
|
if (verb.startsWith("--")) return skillsList(config, args);
|
|
12246
|
-
return
|
|
12420
|
+
return fail7(`unknown skills command "${verb}" \xB7 mend help skills lists them`);
|
|
12247
12421
|
}
|
|
12248
12422
|
};
|
|
12249
12423
|
var completeCommand = async (config, args) => {
|
|
@@ -12351,7 +12525,7 @@ var completionsCommand = (args) => {
|
|
|
12351
12525
|
process.stdout.write(BASH_COMPLETIONS);
|
|
12352
12526
|
return;
|
|
12353
12527
|
default:
|
|
12354
|
-
return
|
|
12528
|
+
return fail7(`${usageOf("completions")} \xB7 e.g. mend completions zsh > "$fpath[1]/_mend"`);
|
|
12355
12529
|
}
|
|
12356
12530
|
};
|
|
12357
12531
|
var supervisedRun = async (config, session, argv) => {
|
|
@@ -12361,15 +12535,15 @@ var supervisedRun = async (config, session, argv) => {
|
|
|
12361
12535
|
"provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
|
|
12362
12536
|
request(config, "POST", `/sessions/${session.id}/launch`, { argv })
|
|
12363
12537
|
);
|
|
12364
|
-
|
|
12365
|
-
`${
|
|
12538
|
+
say7(
|
|
12539
|
+
`${green7("\u2713 recording")} \xB7 run ${dim7(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
|
|
12366
12540
|
);
|
|
12367
|
-
|
|
12368
|
-
|
|
12541
|
+
say7(dim7(` ${argv.join(" ")} \u2014 live record:`));
|
|
12542
|
+
say7("");
|
|
12369
12543
|
const headers = {};
|
|
12370
12544
|
if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
|
|
12371
12545
|
const response = await fetch(`${config.url}/api/events`, { headers });
|
|
12372
|
-
if (response.body === null) return
|
|
12546
|
+
if (response.body === null) return fail7("event stream unavailable");
|
|
12373
12547
|
const reader = response.body.getReader();
|
|
12374
12548
|
const decoder = new TextDecoder();
|
|
12375
12549
|
let buffer = "";
|
|
@@ -12391,7 +12565,7 @@ var supervisedRun = async (config, session, argv) => {
|
|
|
12391
12565
|
}
|
|
12392
12566
|
if (event.sessionId !== session.id) continue;
|
|
12393
12567
|
if (event.type === "session-progress" && event.line !== void 0) {
|
|
12394
|
-
|
|
12568
|
+
say7(dim7(` ${event.line}`));
|
|
12395
12569
|
}
|
|
12396
12570
|
if (event.type === "session") {
|
|
12397
12571
|
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
@@ -12401,9 +12575,9 @@ var supervisedRun = async (config, session, argv) => {
|
|
|
12401
12575
|
}
|
|
12402
12576
|
}
|
|
12403
12577
|
await reader.cancel().catch(() => void 0);
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12578
|
+
say7("");
|
|
12579
|
+
say7(`${green7("\u2713")} session ${settled ?? "settled"} \xB7 recorded \xB7 checkpoint taken`);
|
|
12580
|
+
say7(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
12407
12581
|
process.exit(settled === "failed" ? 1 : 0);
|
|
12408
12582
|
};
|
|
12409
12583
|
var continueSession = async (config, args) => {
|
|
@@ -12426,20 +12600,20 @@ var continueSession = async (config, args) => {
|
|
|
12426
12600
|
}
|
|
12427
12601
|
}
|
|
12428
12602
|
if (sessionId === null || followUp === null) {
|
|
12429
|
-
return
|
|
12603
|
+
return fail7("no session with a pending follow-up \u2014 send one from the review first");
|
|
12430
12604
|
}
|
|
12431
12605
|
} else {
|
|
12432
12606
|
followUp = await api(config, "GET", `/sessions/${sessionId}/follow-up`);
|
|
12433
|
-
if (followUp === null) return
|
|
12607
|
+
if (followUp === null) return fail7(`session ${sessionId} has no pending follow-up`);
|
|
12434
12608
|
}
|
|
12435
12609
|
const detail = await api(config, "GET", `/sessions/${sessionId}`);
|
|
12436
12610
|
const session = detail.session;
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
for (const line of followUp.instruction.split("\n").slice(0, 6))
|
|
12440
|
-
if (followUp.instruction.split("\n").length > 6)
|
|
12611
|
+
say7(`${green7("\u2713")} follow-up for session ${dim7(session.id.slice(0, 8))} \xB7 ${session.branch}`);
|
|
12612
|
+
say7(dim7(" instruction:"));
|
|
12613
|
+
for (const line of followUp.instruction.split("\n").slice(0, 6)) say7(dim7(` \u2502 ${line}`));
|
|
12614
|
+
if (followUp.instruction.split("\n").length > 6) say7(dim7(" \u2502 \u2026"));
|
|
12441
12615
|
if (followUp.reviewSliceId === null || followUp.checkpointAId === null || followUp.checkpointBId === null || followUp.diffDigest === null || followUp.idempotencyKey === null) {
|
|
12442
|
-
return
|
|
12616
|
+
return fail7("legacy follow-up \u2014 recreate it from a pinned Review before delivery");
|
|
12443
12617
|
}
|
|
12444
12618
|
const delivered = await withSpinner(
|
|
12445
12619
|
"delivering persisted Review bundle \u2014 retries reconcile one process\u2026",
|
|
@@ -12454,16 +12628,16 @@ var continueSession = async (config, args) => {
|
|
|
12454
12628
|
})
|
|
12455
12629
|
);
|
|
12456
12630
|
if (delivered.status === "pending") {
|
|
12457
|
-
return
|
|
12631
|
+
return fail7("the session is active \u2014 the follow-up remains pending");
|
|
12458
12632
|
}
|
|
12459
12633
|
if (delivered.status === "delivery_failed") {
|
|
12460
|
-
return
|
|
12634
|
+
return fail7(delivered.deliveryError ?? "delivery failed before process membership finalized");
|
|
12461
12635
|
}
|
|
12462
|
-
|
|
12463
|
-
`${
|
|
12636
|
+
say7(
|
|
12637
|
+
`${green7("\u2713 recording")} \xB7 delivered to run ${delivered.deliverySealantRunId ?? "unknown"}${detachHint()}`
|
|
12464
12638
|
);
|
|
12465
|
-
|
|
12466
|
-
|
|
12639
|
+
say7(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
12640
|
+
say7("");
|
|
12467
12641
|
await attachOrExit(config, session.id, session.harness);
|
|
12468
12642
|
exitAfterSessionEnd(config, session.id);
|
|
12469
12643
|
};
|
|
@@ -12480,19 +12654,19 @@ var resumeCommand = async (config, args) => {
|
|
|
12480
12654
|
const detail = await api(config, "GET", `/projects/${project2.id}`);
|
|
12481
12655
|
const match = prefix === void 0 ? detail.sessions.find((s) => !agentLiveIn(detail, s)) : detail.sessions.find((s) => s.id.startsWith(prefix));
|
|
12482
12656
|
if (match === void 0) {
|
|
12483
|
-
return
|
|
12657
|
+
return fail7(
|
|
12484
12658
|
prefix === void 0 ? "no settled session to resume \u2014 mend status lists sessions" : `no session matches "${prefix}"`
|
|
12485
12659
|
);
|
|
12486
12660
|
}
|
|
12487
12661
|
if (agentLiveIn(detail, match)) {
|
|
12488
|
-
return
|
|
12662
|
+
return fail7(
|
|
12489
12663
|
`session ${match.id.slice(0, 8)} is live \u2014 attach: mend attach ${match.id.slice(0, 8)}`
|
|
12490
12664
|
);
|
|
12491
12665
|
}
|
|
12492
|
-
|
|
12493
|
-
`${
|
|
12666
|
+
say7(
|
|
12667
|
+
`${green7("\u2713")} resuming ${match.harness} \xB7 ${dim7(match.id.slice(0, 8))}${withHarness === null ? "" : ` ${dim7("as")} ${withHarness}`}`
|
|
12494
12668
|
);
|
|
12495
|
-
|
|
12669
|
+
say7(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
|
|
12496
12670
|
const priorAgent = detail.annotations.find((annotation) => annotation.sessionId === match.id)?.currentAgent ?? null;
|
|
12497
12671
|
const protocolPrior = priorAgent?.kind === "agent-protocol" && withHarness === null;
|
|
12498
12672
|
await startAndFollow(
|
|
@@ -12503,8 +12677,8 @@ var resumeCommand = async (config, args) => {
|
|
|
12503
12677
|
harness: withHarness
|
|
12504
12678
|
})
|
|
12505
12679
|
);
|
|
12506
|
-
|
|
12507
|
-
|
|
12680
|
+
say7(`${green7("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
|
|
12681
|
+
say7("");
|
|
12508
12682
|
await attachOrExit(config, match.id, withHarness ?? match.harness);
|
|
12509
12683
|
exitAfterSessionEnd(config, match.id);
|
|
12510
12684
|
};
|
|
@@ -12534,20 +12708,20 @@ var rejoinCommand = async (config, args) => {
|
|
|
12534
12708
|
const matches = prefix === void 0 ? newest === void 0 ? [] : [newest] : eligible.filter((session2) => session2.id.startsWith(prefix));
|
|
12535
12709
|
if (matches.length === 0) {
|
|
12536
12710
|
const harnessDescription = harness === null ? "" : ` ${harness}`;
|
|
12537
|
-
return
|
|
12711
|
+
return fail7(
|
|
12538
12712
|
prefix === void 0 ? `no${harnessDescription} session to rejoin in project ${project2.name}` : `no${harnessDescription} session matches "${prefix}" in project ${project2.name}`
|
|
12539
12713
|
);
|
|
12540
12714
|
}
|
|
12541
12715
|
if (matches.length > 1) {
|
|
12542
|
-
return
|
|
12716
|
+
return fail7(`session prefix "${prefix}" is ambiguous \u2014 use more of the id`);
|
|
12543
12717
|
}
|
|
12544
12718
|
const session = matches[0];
|
|
12545
|
-
if (session === void 0) return
|
|
12719
|
+
if (session === void 0) return fail7("session selection failed");
|
|
12546
12720
|
const alreadyLive = agentLiveIn(detail, session);
|
|
12547
|
-
|
|
12548
|
-
`${
|
|
12721
|
+
say7(
|
|
12722
|
+
`${green7("\u2713")} rejoining ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim7(session.id.slice(0, 8))} \xB7 ${alreadyLive ? "already live" : "restoring"}`
|
|
12549
12723
|
);
|
|
12550
|
-
|
|
12724
|
+
say7(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
12551
12725
|
let restored = false;
|
|
12552
12726
|
const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ?? null;
|
|
12553
12727
|
if (currentAgent?.kind === "agent-protocol") {
|
|
@@ -12565,11 +12739,11 @@ var rejoinCommand = async (config, args) => {
|
|
|
12565
12739
|
"resuming \u2014 a fresh workspace restores the saved session state\u2026"
|
|
12566
12740
|
);
|
|
12567
12741
|
}
|
|
12568
|
-
|
|
12569
|
-
restored ? `${
|
|
12742
|
+
say7(
|
|
12743
|
+
restored ? `${green7("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}` : `${green7("\u2713 recording")} \xB7 attached to the live session${detachHint()}`
|
|
12570
12744
|
);
|
|
12571
12745
|
await tunnels?.start(session.id);
|
|
12572
|
-
|
|
12746
|
+
say7("");
|
|
12573
12747
|
let outcome = await attachWithTunnels(
|
|
12574
12748
|
tunnels,
|
|
12575
12749
|
() => attachTty(config, session.id, session.harness, 0n, void 0, {
|
|
@@ -12601,7 +12775,7 @@ var projectsCommand = async (config) => {
|
|
|
12601
12775
|
api(config, "GET", "/sessions")
|
|
12602
12776
|
]);
|
|
12603
12777
|
if (projects.length === 0) {
|
|
12604
|
-
|
|
12778
|
+
say7(dim7("no adopted projects \u2014 mend adopt brings one in"));
|
|
12605
12779
|
return;
|
|
12606
12780
|
}
|
|
12607
12781
|
const liveByProject = /* @__PURE__ */ new Map();
|
|
@@ -12613,13 +12787,13 @@ var projectsCommand = async (config) => {
|
|
|
12613
12787
|
const here = matchProjectByCwd(projects, cwdFacts(process.cwd()));
|
|
12614
12788
|
for (const project2 of projects) {
|
|
12615
12789
|
const live = liveByProject.get(project2.id) ?? 0;
|
|
12616
|
-
const liveLabel = live > 0 ?
|
|
12790
|
+
const liveLabel = live > 0 ? green7(`${live} live`) : dim7("\u2014");
|
|
12617
12791
|
const marker = project2.id === here?.id ? cobalt2("\u25B8 ") : " ";
|
|
12618
|
-
|
|
12619
|
-
`${marker}${project2.name.padEnd(nameWidth)} ${
|
|
12792
|
+
say7(
|
|
12793
|
+
`${marker}${project2.name.padEnd(nameWidth)} ${dim7(project2.defaultBranch.padEnd(branchWidth))} ${liveLabel} ${dim7(project2.storePath)}`
|
|
12620
12794
|
);
|
|
12621
12795
|
}
|
|
12622
|
-
if (here !== void 0)
|
|
12796
|
+
if (here !== void 0) say7(dim7(` \u25B8 ${here.name} is the cwd's project`));
|
|
12623
12797
|
};
|
|
12624
12798
|
var refreshCommand = async (config, args) => {
|
|
12625
12799
|
const explicit = args.find((a) => !a.startsWith("--")) ?? takeFlagValue4(args, "--project");
|
|
@@ -12629,20 +12803,20 @@ var refreshCommand = async (config, args) => {
|
|
|
12629
12803
|
"POST",
|
|
12630
12804
|
`/projects/${project2.id}/refresh`
|
|
12631
12805
|
);
|
|
12632
|
-
|
|
12806
|
+
say7(`${green7("\u2713")} refreshed ${project2.name} ${dim7(`\xB7 ${branches.length} branches`)}`);
|
|
12633
12807
|
for (const branch of branches.slice(0, 12)) {
|
|
12634
12808
|
const marker = branch.isDefault ? cobalt2("\u25B8 ") : " ";
|
|
12635
|
-
|
|
12636
|
-
`${marker}${branch.name} ${
|
|
12809
|
+
say7(
|
|
12810
|
+
`${marker}${branch.name} ${dim7(branch.sha.slice(0, 12))} ${dim7(branch.committedAt.slice(0, 10))}`
|
|
12637
12811
|
);
|
|
12638
12812
|
}
|
|
12639
|
-
if (branches.length > 12)
|
|
12813
|
+
if (branches.length > 12) say7(dim7(` \u2026 ${branches.length - 12} more`));
|
|
12640
12814
|
};
|
|
12641
12815
|
var rowHold = (row) => (
|
|
12642
12816
|
// A workspace a stop is still saving (docs/adr/0002) reads what is left, first.
|
|
12643
12817
|
captureLineOf(row.session) ?? (row.annotation === void 0 ? null : servicesHoldOf(row.session, row.annotation.currentAgent, row.annotation.liveServices ?? 0))
|
|
12644
12818
|
);
|
|
12645
|
-
var printSessionRow = (row) => {
|
|
12819
|
+
var printSessionRow = (row, workspaceLines = []) => {
|
|
12646
12820
|
const { session, annotation } = row;
|
|
12647
12821
|
const live = ACTIVE_STATUSES.has(session.status);
|
|
12648
12822
|
const status = session.status.padEnd(9);
|
|
@@ -12657,9 +12831,10 @@ var printSessionRow = (row) => {
|
|
|
12657
12831
|
facts.push(amber2("follow-up pending"));
|
|
12658
12832
|
const base = session.baseRef === null ? session.baseSha.slice(0, 12) : session.baseRef;
|
|
12659
12833
|
const model = session.model === null || session.model === void 0 ? "" : ` \xB7 ${session.model}${session.effort === null || session.effort === void 0 ? "" : ` \xB7 ${session.effort}`}`;
|
|
12660
|
-
|
|
12661
|
-
`${session.harness.padEnd(8)} ${
|
|
12834
|
+
say7(
|
|
12835
|
+
`${session.harness.padEnd(8)} ${dim7(session.id.slice(0, 8))} ${live ? green7(status) : dim7(status)} ${row.projectName} ${dim7(`${session.branch} \xB7 base ${base}${model}`)}${facts.length > 0 ? ` ${facts.join(dim7(" \xB7 "))}` : ""}`
|
|
12662
12836
|
);
|
|
12837
|
+
for (const line of workspaceLines) say7(` ${dim7(line)}`);
|
|
12663
12838
|
};
|
|
12664
12839
|
var modelsCommand = async (config, args) => {
|
|
12665
12840
|
const catalogs = await api(
|
|
@@ -12667,15 +12842,15 @@ var modelsCommand = async (config, args) => {
|
|
|
12667
12842
|
"GET",
|
|
12668
12843
|
"/harnesses/models"
|
|
12669
12844
|
);
|
|
12670
|
-
if (args.includes("--json")) return
|
|
12671
|
-
for (const line of modelCatalogLines(catalogs,
|
|
12845
|
+
if (args.includes("--json")) return say7(modelCatalogJson(catalogs));
|
|
12846
|
+
for (const line of modelCatalogLines(catalogs, dim7)) say7(line);
|
|
12672
12847
|
};
|
|
12673
12848
|
var sessionsCommand = async (config, args) => {
|
|
12674
12849
|
const all = args.includes("--all");
|
|
12675
12850
|
if (args.includes("--json=v2")) {
|
|
12676
12851
|
const projectFlag2 = args.indexOf("--project");
|
|
12677
12852
|
const projectName2 = projectFlag2 !== -1 && args[projectFlag2 + 1] !== void 0 ? String(args[projectFlag2 + 1]) : null;
|
|
12678
|
-
|
|
12853
|
+
say7(JSON.stringify(await buildWorktreesJson(config, projectName2), null, 2));
|
|
12679
12854
|
return;
|
|
12680
12855
|
}
|
|
12681
12856
|
const json2 = args.includes("--json");
|
|
@@ -12684,7 +12859,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
12684
12859
|
const projects = await api(config, "GET", "/projects");
|
|
12685
12860
|
const scope = projectName === null ? projects : projects.filter((p) => p.name === projectName);
|
|
12686
12861
|
if (projectName !== null && scope.length === 0) {
|
|
12687
|
-
return
|
|
12862
|
+
return fail7(`no adopted project named "${projectName}"`);
|
|
12688
12863
|
}
|
|
12689
12864
|
let rows;
|
|
12690
12865
|
if (all || projectName !== null || !json2) {
|
|
@@ -12714,11 +12889,11 @@ var sessionsCommand = async (config, args) => {
|
|
|
12714
12889
|
}
|
|
12715
12890
|
if (rows.length === 0) {
|
|
12716
12891
|
if (json2) {
|
|
12717
|
-
|
|
12892
|
+
say7(JSON.stringify({ version: 1, sessions: [] }, null, 2));
|
|
12718
12893
|
return;
|
|
12719
12894
|
}
|
|
12720
|
-
|
|
12721
|
-
|
|
12895
|
+
say7(
|
|
12896
|
+
dim7(all ? "no sessions" : "no active sessions \u2014 mend sessions --all includes settled ones")
|
|
12722
12897
|
);
|
|
12723
12898
|
return;
|
|
12724
12899
|
}
|
|
@@ -12760,10 +12935,14 @@ var sessionsCommand = async (config, args) => {
|
|
|
12760
12935
|
}
|
|
12761
12936
|
}))
|
|
12762
12937
|
};
|
|
12763
|
-
|
|
12938
|
+
say7(JSON.stringify(payload, null, 2));
|
|
12764
12939
|
return;
|
|
12765
12940
|
}
|
|
12766
|
-
|
|
12941
|
+
const workspaceLines = await workspaceLinesOf(
|
|
12942
|
+
config,
|
|
12943
|
+
rows.map((row) => row.session)
|
|
12944
|
+
);
|
|
12945
|
+
for (const row of rows) printSessionRow(row, workspaceLines.get(row.session.id));
|
|
12767
12946
|
};
|
|
12768
12947
|
var toWorktreeJsonSession = (session) => ({
|
|
12769
12948
|
id: session.id,
|
|
@@ -12841,8 +13020,8 @@ var worktreesCommand = async (config, args) => {
|
|
|
12841
13020
|
request: (method, route, body) => request(config, method, route, body),
|
|
12842
13021
|
listWorktrees: async (project2) => (await buildWorktreesJson(config, project2)).worktrees,
|
|
12843
13022
|
liveStatuses: ACTIVE_STATUSES,
|
|
12844
|
-
say:
|
|
12845
|
-
fail:
|
|
13023
|
+
say: say7,
|
|
13024
|
+
fail: fail7
|
|
12846
13025
|
},
|
|
12847
13026
|
args.slice(1)
|
|
12848
13027
|
);
|
|
@@ -12852,11 +13031,11 @@ var worktreesCommand = async (config, args) => {
|
|
|
12852
13031
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
|
|
12853
13032
|
const payload = await buildWorktreesJson(config, projectName);
|
|
12854
13033
|
if (json2) {
|
|
12855
|
-
|
|
13034
|
+
say7(JSON.stringify(payload, null, 2));
|
|
12856
13035
|
return;
|
|
12857
13036
|
}
|
|
12858
13037
|
if (payload.worktrees.length === 0) {
|
|
12859
|
-
|
|
13038
|
+
say7(dim7("no worktrees yet \u2014 mend claude --name <worktree> starts one"));
|
|
12860
13039
|
return;
|
|
12861
13040
|
}
|
|
12862
13041
|
for (const worktree of payload.worktrees) {
|
|
@@ -12864,19 +13043,19 @@ var worktreesCommand = async (config, args) => {
|
|
|
12864
13043
|
const facts = [
|
|
12865
13044
|
`${worktree.sessions.length} session${worktree.sessions.length === 1 ? "" : "s"}`
|
|
12866
13045
|
];
|
|
12867
|
-
if (live > 0) facts.push(
|
|
13046
|
+
if (live > 0) facts.push(green7(`${live} live`));
|
|
12868
13047
|
if (worktree.review !== null && worktree.review.openComments > 0) {
|
|
12869
13048
|
facts.push(amber2(`${worktree.review.openComments} open`));
|
|
12870
13049
|
}
|
|
12871
13050
|
const base = worktree.baseRef ?? worktree.baseSha.slice(0, 12);
|
|
12872
|
-
|
|
12873
|
-
`${worktree.name.padEnd(24)} ${worktree.projectName} ${
|
|
13051
|
+
say7(
|
|
13052
|
+
`${worktree.name.padEnd(24)} ${worktree.projectName} ${dim7(`${worktree.branch} \xB7 base ${base}`)} ${facts.join(dim7(" \xB7 "))}`
|
|
12874
13053
|
);
|
|
12875
13054
|
for (const session of worktree.sessions) {
|
|
12876
13055
|
const status = session.status.padEnd(9);
|
|
12877
13056
|
const name = session.label ?? `session ${session.id.slice(0, 8)}`;
|
|
12878
|
-
|
|
12879
|
-
` \u2514 ${session.harness.padEnd(8)} ${
|
|
13057
|
+
say7(
|
|
13058
|
+
` \u2514 ${session.harness.padEnd(8)} ${dim7(session.id.slice(0, 8))} ${ACTIVE_STATUSES.has(session.status) ? green7(status) : dim7(status)} ${name}`
|
|
12880
13059
|
);
|
|
12881
13060
|
}
|
|
12882
13061
|
}
|
|
@@ -12891,13 +13070,13 @@ var hasNodeFfi = () => {
|
|
|
12891
13070
|
};
|
|
12892
13071
|
var dashboard = async (config, options = {}) => {
|
|
12893
13072
|
if (process.stdout.isTTY !== true) {
|
|
12894
|
-
|
|
13073
|
+
say7(renderIndex());
|
|
12895
13074
|
return;
|
|
12896
13075
|
}
|
|
12897
13076
|
if (!hasNodeFfi()) {
|
|
12898
13077
|
const major = Number(process.versions.node.split(".")[0]);
|
|
12899
13078
|
if (Number.isNaN(major) || major < 26) {
|
|
12900
|
-
return
|
|
13079
|
+
return fail7(
|
|
12901
13080
|
`the dashboard needs Node >= 26 (node:ffi) \u2014 this is ${process.version}; every other command still works`
|
|
12902
13081
|
);
|
|
12903
13082
|
}
|
|
@@ -12908,7 +13087,7 @@ var dashboard = async (config, options = {}) => {
|
|
|
12908
13087
|
);
|
|
12909
13088
|
process.exit(rerun.status ?? 0);
|
|
12910
13089
|
}
|
|
12911
|
-
const { runDashboard } = await import("./dashboard-
|
|
13090
|
+
const { runDashboard } = await import("./dashboard-4J2WMT3G.js");
|
|
12912
13091
|
const agentShare = await startShareIfBridge(config);
|
|
12913
13092
|
const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
|
|
12914
13093
|
listServices: () => fetchServices(config),
|
|
@@ -12956,29 +13135,29 @@ var withAgentShare = async (config, work) => {
|
|
|
12956
13135
|
};
|
|
12957
13136
|
var helpCommand = (words) => {
|
|
12958
13137
|
if (words.length === 0) {
|
|
12959
|
-
|
|
13138
|
+
say7(renderIndex());
|
|
12960
13139
|
return;
|
|
12961
13140
|
}
|
|
12962
13141
|
const doc = findCommand(words);
|
|
12963
13142
|
if (doc !== null && (doc.name.split(" ").length > 1 || words.length === 1)) {
|
|
12964
|
-
|
|
13143
|
+
say7(renderCommand(doc));
|
|
12965
13144
|
return;
|
|
12966
13145
|
}
|
|
12967
13146
|
const group = renderGroup(words[0] ?? "");
|
|
12968
13147
|
if (doc !== null) {
|
|
12969
|
-
|
|
13148
|
+
say7(renderCommand(doc));
|
|
12970
13149
|
return;
|
|
12971
13150
|
}
|
|
12972
13151
|
if (group !== null) {
|
|
12973
|
-
|
|
13152
|
+
say7(group);
|
|
12974
13153
|
return;
|
|
12975
13154
|
}
|
|
12976
|
-
return
|
|
13155
|
+
return fail7(`no command "${words.join(" ")}" \xB7 mend help lists them`);
|
|
12977
13156
|
};
|
|
12978
13157
|
var manCommand = (words) => {
|
|
12979
13158
|
const doc = words.length === 0 ? null : findCommand(words);
|
|
12980
13159
|
if (words.length > 0 && doc === null) {
|
|
12981
|
-
return
|
|
13160
|
+
return fail7(`no command "${words.join(" ")}" \xB7 mend help lists them`);
|
|
12982
13161
|
}
|
|
12983
13162
|
const version = cliVersion();
|
|
12984
13163
|
const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
|
|
@@ -12987,7 +13166,7 @@ var manCommand = (words) => {
|
|
|
12987
13166
|
const result = spawnSync6("man", ["-l", file], { stdio: "inherit" });
|
|
12988
13167
|
fs20.rmSync(file, { force: true });
|
|
12989
13168
|
if (result.error !== void 0 || result.status !== 0) {
|
|
12990
|
-
|
|
13169
|
+
say7(doc === null ? renderIndex() : renderCommand(doc));
|
|
12991
13170
|
}
|
|
12992
13171
|
};
|
|
12993
13172
|
var main = async () => {
|
|
@@ -12999,7 +13178,7 @@ var main = async () => {
|
|
|
12999
13178
|
}
|
|
13000
13179
|
if (command === "server") {
|
|
13001
13180
|
const result = await serverCommand(rest);
|
|
13002
|
-
if (result._tag === "error") return
|
|
13181
|
+
if (result._tag === "error") return fail7(result.message);
|
|
13003
13182
|
return;
|
|
13004
13183
|
}
|
|
13005
13184
|
const config = loadConfig();
|
|
@@ -13059,7 +13238,13 @@ var main = async () => {
|
|
|
13059
13238
|
);
|
|
13060
13239
|
case "session":
|
|
13061
13240
|
if (rest[0] === "share") return sessionShareCommand(boundApi(config), rest.slice(1));
|
|
13062
|
-
return
|
|
13241
|
+
return fail7(`unknown session command "${rest[0] ?? ""}" \xB7 mend help session share`);
|
|
13242
|
+
case "workspace":
|
|
13243
|
+
return workspaceCommand(
|
|
13244
|
+
boundApi(config),
|
|
13245
|
+
(method, route, body) => request(config, method, route, body),
|
|
13246
|
+
rest
|
|
13247
|
+
);
|
|
13063
13248
|
// Hidden in the catalog: the installer renders its own QR through this.
|
|
13064
13249
|
case "qr":
|
|
13065
13250
|
return qrCommand(rest);
|
|
@@ -13110,10 +13295,10 @@ var main = async () => {
|
|
|
13110
13295
|
case "--version":
|
|
13111
13296
|
case "-v":
|
|
13112
13297
|
for (const line of versionLines(cliVersion(), await fetchServerVersion(config.url)))
|
|
13113
|
-
|
|
13298
|
+
say7(line);
|
|
13114
13299
|
return;
|
|
13115
13300
|
default:
|
|
13116
|
-
return
|
|
13301
|
+
return fail7(`unknown command "${command}" \xB7 mend help lists them`);
|
|
13117
13302
|
}
|
|
13118
13303
|
};
|
|
13119
13304
|
await main();
|