@sealant/mend 0.36.0-next.658 → 0.36.0-next.665
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-GKRTZRV7.js → chunk-K5WNRH4V.js} +32 -0
- package/dist/{dashboard-IBHSVQ5S.js → dashboard-DQB5RNXJ.js} +1 -1
- package/dist/main.js +878 -139
- package/man/mend-accounts.1 +1 -1
- package/man/mend-adopt.1 +1 -1
- package/man/mend-attach.1 +1 -1
- package/man/mend-codex.1 +1 -1
- package/man/mend-completions.1 +1 -1
- package/man/mend-connect-pi.1 +1 -1
- package/man/mend-connect.1 +1 -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-logs.1 +26 -0
- 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 +2 -2
- 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 +10 -2
- 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 +35 -4
- 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 +12 -2
- package/man/mend-server-start.1 +1 -1
- package/man/mend-server-status.1 +1 -1
- package/man/mend-server-stop.1 +1 -1
- package/man/mend-server-upgrade.1 +1 -1
- package/man/mend-server.1 +1 -1
- package/man/mend-service-add.1 +1 -1
- package/man/mend-service-connect.1 +1 -1
- package/man/mend-service-init.1 +1 -1
- package/man/mend-service-list.1 +1 -1
- package/man/mend-service-logs.1 +3 -1
- package/man/mend-service-restart.1 +1 -1
- package/man/mend-service-run.1 +8 -3
- package/man/mend-service-stop.1 +1 -1
- package/man/mend-session-share.1 +1 -1
- package/man/mend-sessions.1 +1 -1
- package/man/mend-shell.1 +1 -1
- package/man/mend-skills-push.1 +1 -1
- package/man/mend-skills.1 +1 -1
- package/man/mend-snake.1 +1 -1
- package/man/mend-ssh-setup.1 +1 -1
- package/man/mend-ssh.1 +1 -1
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +1 -1
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-wait.1 +28 -0
- package/man/mend-workspace-replace.1 +1 -1
- package/man/mend-worktrees-rm.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +8 -2
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
WORKTREE_REMOVAL_FORCE_HINT,
|
|
18
18
|
agentIsLive,
|
|
19
19
|
agentMemoryNameOf,
|
|
20
|
-
agentOutcome,
|
|
21
20
|
agentStartingFacts,
|
|
22
21
|
agentStartingLine,
|
|
23
22
|
askYesNo,
|
|
@@ -48,6 +47,7 @@ import {
|
|
|
48
47
|
normalizeRemoteUrl,
|
|
49
48
|
parseLaunchArgs,
|
|
50
49
|
pasteBytes,
|
|
50
|
+
redactCredentials,
|
|
51
51
|
replaceConfirmLines,
|
|
52
52
|
repositoryCloneUrlIssue,
|
|
53
53
|
sameGrant,
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
workspaceRetirementLine,
|
|
69
69
|
worktreeJoinLine,
|
|
70
70
|
worktreeListsPeople
|
|
71
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-K5WNRH4V.js";
|
|
72
72
|
|
|
73
73
|
// src/main.ts
|
|
74
74
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
@@ -861,6 +861,9 @@ var isTenancy = (value) => TENANCIES.some((tenancy) => tenancy === value);
|
|
|
861
861
|
var EDGE_IMAGE = "caddy:2.10-alpine";
|
|
862
862
|
var EDGE_COMPOSE_FILE = "compose.edge.yaml";
|
|
863
863
|
var POSTURE_COMPOSE_FILE = "compose.posture.yaml";
|
|
864
|
+
var DECLARABLE_ITEMS = ["core-private", "edge-tls", "workspace-ssh"];
|
|
865
|
+
var isDeclarableItem = (value) => DECLARABLE_ITEMS.some((item) => item === value);
|
|
866
|
+
var publishedAddress = (address, port) => `${net2.isIP(address) === 6 ? `[${address}]` : address}:${port}`;
|
|
864
867
|
var parseEdgeHost = (value) => {
|
|
865
868
|
const host = value.trim().toLowerCase().replace(/\.$/, "");
|
|
866
869
|
const labels = host.split(".");
|
|
@@ -876,7 +879,11 @@ var postureEnvironment = (posture) => {
|
|
|
876
879
|
...posture.exposure === void 0 ? [] : [["MEND_EXPOSURE", posture.exposure]],
|
|
877
880
|
...posture.tenancy === void 0 ? [] : [["MEND_TENANCY", posture.tenancy]],
|
|
878
881
|
...gate ? [["MEND_SOURCE_POLICY", "tenant"], ["MEND_CAPTURE_REQUIRE_SIZES", "true"]] : [],
|
|
879
|
-
...posture.exposure === "public" ? [["MEND_URL_BEARERS", "refuse"]] : []
|
|
882
|
+
...posture.exposure === "public" ? [["MEND_URL_BEARERS", "refuse"]] : [],
|
|
883
|
+
// The gate's workspace-ssh item reads where SSH is published apart from the web port; the
|
|
884
|
+
// container cannot see what its host publishes.
|
|
885
|
+
...posture.sshBind === void 0 || posture.sshPort === void 0 ? [] : [["MEND_SSH_PUBLISHED", publishedAddress(posture.sshBind, posture.sshPort)]],
|
|
886
|
+
...posture.declared === void 0 || posture.declared.length === 0 ? [] : [["MEND_EXPOSURE_DECLARED", posture.declared.join(",")]]
|
|
880
887
|
];
|
|
881
888
|
};
|
|
882
889
|
var postureEnvLines = (posture) => [
|
|
@@ -927,7 +934,11 @@ var healthPosture = (body) => {
|
|
|
927
934
|
var declaredPostureLines = (posture) => [
|
|
928
935
|
...posture.edgeHost === void 0 ? [] : [`edge \xB7 ${posture.edgeHost} \xB7 ${EDGE_IMAGE} on 80 and 443 \xB7 Mend's own port on loopback`],
|
|
929
936
|
`exposure \xB7 declared ${posture.exposure ?? "private"}${posture.exposure === void 0 ? " \xB7 the default, not set on this install" : ""}`,
|
|
930
|
-
`tenancy \xB7 declared ${posture.tenancy ?? "single"}${posture.tenancy === void 0 ? " \xB7 the default, not set on this install" : ""}
|
|
937
|
+
`tenancy \xB7 declared ${posture.tenancy ?? "single"}${posture.tenancy === void 0 ? " \xB7 the default, not set on this install" : ""}`,
|
|
938
|
+
...posture.sshBind === void 0 || posture.sshPort === void 0 ? [] : [
|
|
939
|
+
`workspace ssh \xB7 published on ${publishedAddress(posture.sshBind, posture.sshPort)} apart from the web port`
|
|
940
|
+
],
|
|
941
|
+
...posture.declared === void 0 || posture.declared.length === 0 ? [] : [`stated verified from outside \xB7 ${posture.declared.join(", ")}`]
|
|
931
942
|
];
|
|
932
943
|
var observedEdgeLine = (host, observed) => [
|
|
933
944
|
`edge \xB7 ${host}`,
|
|
@@ -1962,13 +1973,71 @@ var COMMANDS = [
|
|
|
1962
1973
|
name: "run",
|
|
1963
1974
|
section: "sessions",
|
|
1964
1975
|
summary: "run any command as a session",
|
|
1965
|
-
synopsis: [
|
|
1976
|
+
synopsis: [
|
|
1977
|
+
"[--project <p>] [--name <n> | --worktree <n>] [--base <ref>] [--detach] [--json] -- <command...>"
|
|
1978
|
+
],
|
|
1966
1979
|
description: [
|
|
1967
|
-
"The same worktree, record, and review as mend codex, with a command of your own in place of a harness. Everything after -- is the command."
|
|
1980
|
+
"The same worktree, record, and review as mend codex, with a command of your own in place of a harness. Everything after -- is the command.",
|
|
1981
|
+
"The command's output is printed as the record has it, and mend run exits with the command's exit code. It runs in a terminal, so stdout and stderr arrive together, on stdout. What Mend says itself goes to stderr, so out=$(mend run -- git log -1) holds the command's output and nothing else. Ctrl+C stops watching and puts the terminal back; the command keeps running, and mend logs and mend wait pick it up again. A signal exits 128 + its number: 130 for SIGINT (Ctrl+C), 129 for SIGHUP, 143 for SIGTERM. A second signal exits at once, and an exit waits at most 5 seconds for a reader that takes nothing, then says the output may be incomplete.",
|
|
1982
|
+
"Output this terminal could not be given in full (a read the server refused, a reader that went away) fails mend run with exit 1 even when the command succeeded, and the command's own code is said on stderr: a script never takes cut output for the whole of it.",
|
|
1983
|
+
"The platform takes at most 64 words, none empty and none starting or ending with whitespace (a script that starts with a newline, for example). Such a command is refused before anything is created; trim the word and run it again.",
|
|
1984
|
+
"With --detach, mend run returns once the command runs. With --json, stdout carries one JSON object in place of the output: the session id, the process id, the worktree, the branch, and the process's status and exit code as last observed, which is how it ended without --detach (and with it, when the command ended first).",
|
|
1985
|
+
"Workspaces set PAGER=cat, so git log and friends print instead of waiting for a pager the image does not have. A project variable of the same name, or the pager in your own git config, wins."
|
|
1968
1986
|
],
|
|
1969
|
-
options: [
|
|
1970
|
-
|
|
1971
|
-
|
|
1987
|
+
options: [
|
|
1988
|
+
project(),
|
|
1989
|
+
{ flag: "--name <n>", text: "name the worktree; an existing name joins it" },
|
|
1990
|
+
{ flag: "--worktree <n>", text: "join an existing worktree only" },
|
|
1991
|
+
{ flag: "--base <ref>", text: "create the worktree from another git base" },
|
|
1992
|
+
{ flag: "--detach, -d", text: "return once the command runs" },
|
|
1993
|
+
{ flag: "--json", text: "print the session and process ids, and the end, as JSON" }
|
|
1994
|
+
],
|
|
1995
|
+
examples: [
|
|
1996
|
+
{ command: "mend run -- pnpm test", text: "the test output here, exit code included" },
|
|
1997
|
+
{
|
|
1998
|
+
command: "id=$(mend run --detach --json -- make build | jq -r .sessionId)",
|
|
1999
|
+
text: "start it, then mend wait $id"
|
|
2000
|
+
}
|
|
2001
|
+
],
|
|
2002
|
+
see: ["logs", "wait", "codex"]
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
name: "logs",
|
|
2006
|
+
section: "sessions",
|
|
2007
|
+
summary: "print a session's recorded terminal output",
|
|
2008
|
+
synopsis: ["[session] [--follow] [--from <sequence>] [--process <id>]"],
|
|
2009
|
+
description: [
|
|
2010
|
+
"Prints what the session's command (or agent) wrote to its terminal, as the record holds it, on stdout. Settled sessions count: the record outlives the process and the workspace. --follow keeps printing until the process ends. The record is printed as it is, nothing taken out: anyone who can read the project can read it, so what a command printed, a password included, reaches them too. A signal stops it and puts the terminal back, exiting 128 + its number: 130 for SIGINT (Ctrl+C), 129 for SIGHUP, 143 for SIGTERM.",
|
|
2011
|
+
"<session> is the session id, a prefix of it, or the worktree's name. With none, the one live session is taken. --process reads another process of the session, a shell or a Service attempt, by a prefix of its id."
|
|
2012
|
+
],
|
|
2013
|
+
options: [
|
|
2014
|
+
{ flag: "--follow, -f", text: "keep printing until the process ends" },
|
|
2015
|
+
{ flag: "--from <sequence>", text: "start at a record sequence. Default: 0" },
|
|
2016
|
+
{ flag: "--process <id>", text: "another process of the session, by a prefix of its id" }
|
|
2017
|
+
],
|
|
2018
|
+
examples: [{ command: "mend logs 3f2a --follow", text: "" }],
|
|
2019
|
+
see: ["run", "wait", "service logs"]
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
name: "wait",
|
|
2023
|
+
section: "sessions",
|
|
2024
|
+
summary: "wait for a session's command to end",
|
|
2025
|
+
synopsis: ["[session] [--timeout <seconds>] [--process <id>] [--json]"],
|
|
2026
|
+
description: [
|
|
2027
|
+
"Returns once the session's command (or agent) has ended, with its exit code: the code the platform reported, or 1 when it reported none. A command that already ended answers at once. While a launch or a resume is starting, the previous process's end does not count. --process waits for one process by its id, the processId mend run --json prints.",
|
|
2028
|
+
"When --timeout passes first, mend wait exits 124, as timeout(1) does, and the command keeps running. Stopped by a signal, it exits 128 + its number (130 for Ctrl+C), and the command keeps running. The timeout covers everything: finding the session, every read and every retry. --json then prints the last state read, and nothing more is asked of the server.",
|
|
2029
|
+
"<session> is the session id, a prefix of it, or the worktree's name. With none, the one live session is taken."
|
|
2030
|
+
],
|
|
2031
|
+
options: [
|
|
2032
|
+
{ flag: "--timeout <seconds>", text: "give up after this long; exit 124" },
|
|
2033
|
+
{
|
|
2034
|
+
flag: "--process <id>",
|
|
2035
|
+
text: "wait for this process of the session, by a prefix of its id"
|
|
2036
|
+
},
|
|
2037
|
+
{ flag: "--json", text: "print the session, the process, the status and the exit code" }
|
|
2038
|
+
],
|
|
2039
|
+
examples: [{ command: "mend wait 3f2a --timeout 600", text: "" }],
|
|
2040
|
+
see: ["run", "logs"]
|
|
1972
2041
|
},
|
|
1973
2042
|
{
|
|
1974
2043
|
name: "continue",
|
|
@@ -2074,8 +2143,13 @@ var COMMANDS = [
|
|
|
2074
2143
|
name: "projects",
|
|
2075
2144
|
section: "sessions",
|
|
2076
2145
|
summary: "adopted projects and their live sessions",
|
|
2077
|
-
synopsis: [],
|
|
2078
|
-
description: [
|
|
2146
|
+
synopsis: ["[--json]"],
|
|
2147
|
+
description: [
|
|
2148
|
+
"One line per project, with the sessions running in it.",
|
|
2149
|
+
'--json prints them for scripts, stable like mend sessions --json: {"version": 1, "projects": [...]}, an empty list when there are none. Each project has id, name, originUrl (a string, or null), defaultBranch, storePath, liveSessions (a number) and current (true when the current directory is inside it).',
|
|
2150
|
+
"A repository URL is printed without its credentials: https://oauth2:TOKEN@github.com/acme/repo.git reads as https://github.com/acme/repo.git, here and in every line, message and JSON the CLI prints itself; an ssh URL keeps its user and loses only the password. A command's recorded output (mend run, mend logs, mend service logs, mend attach) and a file's contents (mend memory show) are printed as they are, and a session's record is readable by anyone who can read the project."
|
|
2151
|
+
],
|
|
2152
|
+
options: [{ flag: "--json", text: "the projects as JSON" }],
|
|
2079
2153
|
see: ["adopt", "sessions"]
|
|
2080
2154
|
},
|
|
2081
2155
|
{
|
|
@@ -2156,14 +2230,19 @@ var COMMANDS = [
|
|
|
2156
2230
|
section: "services",
|
|
2157
2231
|
summary: "start and supervise a server in the session's workspace",
|
|
2158
2232
|
synopsis: [
|
|
2159
|
-
"[session] --port <port> [--name <n>] [--udp] [--http|--https] [--no-connect] -- <command...>",
|
|
2160
|
-
"[session] <name> [--no-connect]"
|
|
2233
|
+
"[session] --port <port> [--name <n>] [--udp] [--http|--https] [--wait] [--no-connect] -- <command...>",
|
|
2234
|
+
"[session] <name> [--wait] [--no-connect]"
|
|
2161
2235
|
],
|
|
2162
2236
|
description: [
|
|
2163
2237
|
"With --, the command after it is started in the workspace and supervised: its output is recorded, and mend service restart re-runs it. Without --, the name is a Service declared in the worktree's mend.toml. mend service <name> is the shorthand for that.",
|
|
2238
|
+
"Mend waits up to a minute for the port to answer before it returns. --wait makes the exit status say how that ended: 0 once the port answered, 1 when it did not, and 124 when the server gave no answer within 90 seconds. The Service keeps running in every case. A waited start returns and opens no tunnel; mend service connect reaches the port. UDP has no probe, and a recipe that declares only a port is adopted with one probe, so --wait takes neither.",
|
|
2164
2239
|
"The port is tunnelled to this machine's loopback as soon as it listens, unless --no-connect."
|
|
2165
2240
|
],
|
|
2166
2241
|
options: [
|
|
2242
|
+
{
|
|
2243
|
+
flag: "--wait",
|
|
2244
|
+
text: "exit 1 when the port did not answer, 124 with no answer in 90 s; no tunnel"
|
|
2245
|
+
},
|
|
2167
2246
|
{ flag: "--port <port>", text: "the port the command listens on inside the workspace" },
|
|
2168
2247
|
{ flag: "--name <n>", text: "the service's name. Default: the command" },
|
|
2169
2248
|
{ flag: "--udp", text: "a UDP port" },
|
|
@@ -2216,7 +2295,10 @@ var COMMANDS = [
|
|
|
2216
2295
|
section: "services",
|
|
2217
2296
|
summary: "follow a supervised service's output",
|
|
2218
2297
|
synopsis: ["<name-or-id> [--from <sequence>]"],
|
|
2219
|
-
description: [
|
|
2298
|
+
description: [
|
|
2299
|
+
"Replays the recorded output, then follows it live. Ctrl-C stops following.",
|
|
2300
|
+
"The record is printed as it is, nothing taken out, and anyone who can read the project can read it: what the Service printed, a password included, reaches them too."
|
|
2301
|
+
],
|
|
2220
2302
|
options: [{ flag: "--from <sequence>", text: "start the replay at a record sequence" }],
|
|
2221
2303
|
see: ["service run"]
|
|
2222
2304
|
},
|
|
@@ -2639,7 +2721,7 @@ var COMMANDS = [
|
|
|
2639
2721
|
summary: "how this instance is exposed, as declared and as observed",
|
|
2640
2722
|
synopsis: [],
|
|
2641
2723
|
description: [
|
|
2642
|
-
"Operator only. States MEND_EXPOSURE as declared (loopback, private or public) and one line per item of the public exposure gate, with how it was established: observed (this server read it), carried (this build contains it, and the server cannot see it in effect), declared (you stated it and the server cannot check it), or open. MEND_EXPOSURE=public refuses to start while an item the server can observe
|
|
2724
|
+
"Operator only. States MEND_EXPOSURE as declared (loopback, private or public) and one line per item of the public exposure gate, with how it was established: observed (this server read it), carried (this build contains it, and the server cannot see it in effect), declared (you stated it and the server cannot check it), or open. MEND_EXPOSURE=public refuses to start while an item that blocks a start is open: every item the server can observe, and workspace-ssh until you declare it. Items it cannot observe say what would verify them; once you have verified core-private, edge-tls or workspace-ssh (SSH published apart from the web port) from outside, name it in MEND_EXPOSURE_DECLARED, or with mend server setup --declare. The report is what was observed; it is not a statement that the instance is fit to expose."
|
|
2643
2725
|
],
|
|
2644
2726
|
see: ["operator gate", "doctor"]
|
|
2645
2727
|
},
|
|
@@ -2725,7 +2807,7 @@ var COMMANDS = [
|
|
|
2725
2807
|
section: "this machine",
|
|
2726
2808
|
summary: "install or repair the local Mend server",
|
|
2727
2809
|
synopsis: [
|
|
2728
|
-
"[--context <name>] [--version <version|latest>] [--bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--edge <host> | --no-edge] [--exposure <loopback|private|public>] [--tenancy <single|multi>] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
|
|
2810
|
+
"[--context <name>] [--version <version|latest>] [--bind <ip>] [--ssh-bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--edge <host> | --no-edge] [--exposure <loopback|private|public>] [--tenancy <single|multi>] [--declare <item>...] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
|
|
2729
2811
|
],
|
|
2730
2812
|
description: [
|
|
2731
2813
|
"Checks a local Unix-socket Docker context and the Compose plugin, downloads the compose and Postgres initialization assets for one Mend release, preserves existing data and secrets, and starts the server. Re-running repairs the same pinned version. A changed --version is refused; use mend server upgrade. Updating this CLI never updates an existing server pin.",
|
|
@@ -2745,6 +2827,10 @@ var COMMANDS = [
|
|
|
2745
2827
|
text: "exact Mend server version, or latest; omitted keeps the current pin"
|
|
2746
2828
|
},
|
|
2747
2829
|
{ flag: "--bind <ip>", text: "published listen address. Default: 127.0.0.1" },
|
|
2830
|
+
{
|
|
2831
|
+
flag: "--ssh-bind <ip>",
|
|
2832
|
+
text: "where workspace SSH is published, when not on --bind; with --edge, Remote-SSH from another machine needs it. Kept across reruns; the --bind address takes it away"
|
|
2833
|
+
},
|
|
2748
2834
|
{ flag: "--url <origin>", text: "advertised browser URL, required with a non-loopback bind" },
|
|
2749
2835
|
{
|
|
2750
2836
|
flag: "--origin <origin>",
|
|
@@ -2768,6 +2854,10 @@ var COMMANDS = [
|
|
|
2768
2854
|
flag: "--tenancy <v>",
|
|
2769
2855
|
text: "declare single or multi; kept across reruns and upgrades. multi also sets the multi mode gate's variables"
|
|
2770
2856
|
},
|
|
2857
|
+
{
|
|
2858
|
+
flag: "--declare <item>",
|
|
2859
|
+
text: "state a gate item you verified from outside: core-private, edge-tls or workspace-ssh; repeat for more, none clears. Kept across reruns. public with --ssh-bind beyond loopback needs workspace-ssh"
|
|
2860
|
+
},
|
|
2771
2861
|
{
|
|
2772
2862
|
flag: "--assets-dir <dir>",
|
|
2773
2863
|
text: "copy compose.v2.yaml and postgres-init.sh from a release directory; fresh setup requires --version"
|
|
@@ -2788,6 +2878,10 @@ var COMMANDS = [
|
|
|
2788
2878
|
command: "mend server setup --version 0.25.0 --assets-dir ./release-assets --offline",
|
|
2789
2879
|
text: "install from local release assets and preloaded images"
|
|
2790
2880
|
},
|
|
2881
|
+
{
|
|
2882
|
+
command: "mend server setup --edge mend.example.com --ssh-bind 0.0.0.0 --exposure public --declare workspace-ssh",
|
|
2883
|
+
text: "the TLS edge for the browser and the API, and workspace SSH for Remote-SSH from other machines"
|
|
2884
|
+
},
|
|
2791
2885
|
{
|
|
2792
2886
|
command: "mend server setup --edge mend.example.com",
|
|
2793
2887
|
text: "a TLS edge for mend.example.com; its DNS points at this machine and 80 and 443 reach it"
|
|
@@ -3998,8 +4092,10 @@ var doctorCommand = async (config, localCredential2, claudeGrant2) => {
|
|
|
3998
4092
|
onPath,
|
|
3999
4093
|
dockerShutdown: observeHostShutdownTimeout
|
|
4000
4094
|
});
|
|
4001
|
-
for (const check of checks)
|
|
4095
|
+
for (const check of checks) {
|
|
4096
|
+
process.stdout.write(`${redactCredentials(formatCheck(check, paintMark))}
|
|
4002
4097
|
`);
|
|
4098
|
+
}
|
|
4003
4099
|
if (checks.some((check) => check.state === "failed")) process.exitCode = 1;
|
|
4004
4100
|
};
|
|
4005
4101
|
|
|
@@ -4183,7 +4279,7 @@ var gitAuthorLine = (author) => `${author.name} <${author.email}> \xB7 ${author.
|
|
|
4183
4279
|
|
|
4184
4280
|
// src/http-client.ts
|
|
4185
4281
|
import { Agent, setGlobalDispatcher } from "undici";
|
|
4186
|
-
var cliDispatcher = (
|
|
4282
|
+
var cliDispatcher = (connect3) => new Agent({ allowH2: false, ...connect3 === void 0 ? {} : { connect: connect3 } });
|
|
4187
4283
|
var useHttp1 = () => {
|
|
4188
4284
|
setGlobalDispatcher(cliDispatcher());
|
|
4189
4285
|
};
|
|
@@ -4197,10 +4293,10 @@ var paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text
|
|
|
4197
4293
|
var dim = paint("2");
|
|
4198
4294
|
var green = paint("32");
|
|
4199
4295
|
var amber = paint("33");
|
|
4200
|
-
var say = (line) => process.stdout.write(`${line}
|
|
4296
|
+
var say = (line) => process.stdout.write(`${redactCredentials(line)}
|
|
4201
4297
|
`);
|
|
4202
4298
|
var fail = (message) => {
|
|
4203
|
-
process.stderr.write(`mend: ${message}
|
|
4299
|
+
process.stderr.write(`mend: ${redactCredentials(message)}
|
|
4204
4300
|
`);
|
|
4205
4301
|
process.exit(1);
|
|
4206
4302
|
};
|
|
@@ -4667,10 +4763,10 @@ import * as os7 from "node:os";
|
|
|
4667
4763
|
var paint2 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
4668
4764
|
var dim2 = paint2("2");
|
|
4669
4765
|
var green2 = paint2("32");
|
|
4670
|
-
var say2 = (line) => process.stdout.write(`${line}
|
|
4766
|
+
var say2 = (line) => process.stdout.write(`${redactCredentials(line)}
|
|
4671
4767
|
`);
|
|
4672
4768
|
var fail2 = (message) => {
|
|
4673
|
-
process.stderr.write(`mend: ${message}
|
|
4769
|
+
process.stderr.write(`mend: ${redactCredentials(message)}
|
|
4674
4770
|
`);
|
|
4675
4771
|
process.exit(1);
|
|
4676
4772
|
};
|
|
@@ -4761,10 +4857,10 @@ var paint3 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m$
|
|
|
4761
4857
|
var dim3 = paint3("2");
|
|
4762
4858
|
var green3 = paint3("32");
|
|
4763
4859
|
var cobalt = paint3("34");
|
|
4764
|
-
var say3 = (line) => process.stdout.write(`${line}
|
|
4860
|
+
var say3 = (line) => process.stdout.write(`${redactCredentials(line)}
|
|
4765
4861
|
`);
|
|
4766
4862
|
var fail3 = (message) => {
|
|
4767
|
-
process.stderr.write(`mend: ${message}
|
|
4863
|
+
process.stderr.write(`mend: ${redactCredentials(message)}
|
|
4768
4864
|
`);
|
|
4769
4865
|
process.exit(1);
|
|
4770
4866
|
};
|
|
@@ -4942,10 +5038,10 @@ import * as path12 from "node:path";
|
|
|
4942
5038
|
var paint4 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
|
|
4943
5039
|
var dim4 = paint4("2");
|
|
4944
5040
|
var green4 = paint4("32");
|
|
4945
|
-
var say4 = (line) => process.stdout.write(`${line}
|
|
5041
|
+
var say4 = (line) => process.stdout.write(`${redactCredentials(line)}
|
|
4946
5042
|
`);
|
|
4947
5043
|
var fail4 = (message) => {
|
|
4948
|
-
process.stderr.write(`mend: ${message}
|
|
5044
|
+
process.stderr.write(`mend: ${redactCredentials(message)}
|
|
4949
5045
|
`);
|
|
4950
5046
|
process.exit(1);
|
|
4951
5047
|
};
|
|
@@ -5168,13 +5264,13 @@ var renderExposure = (report) => {
|
|
|
5168
5264
|
const mark = (item) => marks[item.established];
|
|
5169
5265
|
const open = report.items.filter((item) => item.established === "open");
|
|
5170
5266
|
const blocking = open.filter((item) => item.blocksStart).length;
|
|
5171
|
-
const
|
|
5267
|
+
const notBlocking = open.length - blocking;
|
|
5172
5268
|
return [
|
|
5173
5269
|
`exposure \xB7 declared ${report.declared} \xB7 ${EXPOSURE_WORDS[report.declared]}`,
|
|
5174
5270
|
...report.items.map(
|
|
5175
5271
|
(item) => `${mark(item)} ${item.id.padEnd(width)} ${item.established.padEnd(8)} ${item.detail}${item.fix === null ? "" : dim4(` \xB7 ${item.fix}`)}`
|
|
5176
5272
|
),
|
|
5177
|
-
open.length === 0 ? "nothing open \xB7 every item was observed here, is carried by this build, or was declared by the operator" : `${open.length} of ${report.items.length} items open \xB7 ${blocking}
|
|
5273
|
+
open.length === 0 ? "nothing open \xB7 every item was observed here, is carried by this build, or was declared by the operator" : `${open.length} of ${report.items.length} items open \xB7 ${blocking} refuse a public start${blocking === 0 ? "" : "; MEND_EXPOSURE=public refuses to start"} \xB7 ${notBlocking} do not`
|
|
5178
5274
|
];
|
|
5179
5275
|
};
|
|
5180
5276
|
var renderOrganizations = (rows) => {
|
|
@@ -5569,6 +5665,208 @@ var piProfileLines = (scan) => {
|
|
|
5569
5665
|
];
|
|
5570
5666
|
};
|
|
5571
5667
|
|
|
5668
|
+
// src/run-scripts.ts
|
|
5669
|
+
var RUN_ARGV_MAX = 64;
|
|
5670
|
+
var whitespaceName = (character) => character === "\n" || character === "\r" ? "a newline" : character === " " ? "a tab" : character === " " ? "a space" : "whitespace";
|
|
5671
|
+
var runArgvIssue = (argv) => {
|
|
5672
|
+
if (argv.length > RUN_ARGV_MAX) {
|
|
5673
|
+
return `the command has ${argv.length} words and the platform takes at most ${RUN_ARGV_MAX} \xB7 put them in a script and run that`;
|
|
5674
|
+
}
|
|
5675
|
+
for (const [index, word] of argv.entries()) {
|
|
5676
|
+
const which = index === 0 ? "the program" : `argument ${index}`;
|
|
5677
|
+
if (word === "") {
|
|
5678
|
+
return `${which} is empty \xB7 the platform refuses empty arguments`;
|
|
5679
|
+
}
|
|
5680
|
+
if (word.trim() !== word) {
|
|
5681
|
+
const leading = word.trimStart() !== word;
|
|
5682
|
+
const edge = leading ? word.charAt(0) : word.charAt(word.length - 1);
|
|
5683
|
+
return `${which} ${leading ? "starts" : "ends"} with ${whitespaceName(edge)} \xB7 the platform refuses arguments with leading or trailing whitespace \xB7 trim it and run again`;
|
|
5684
|
+
}
|
|
5685
|
+
}
|
|
5686
|
+
return null;
|
|
5687
|
+
};
|
|
5688
|
+
var pickProcess = (detail, prefix) => {
|
|
5689
|
+
const processes = detail.processes ?? [];
|
|
5690
|
+
if (prefix !== null) {
|
|
5691
|
+
const matches = processes.filter((candidate) => candidate.id?.startsWith(prefix) === true);
|
|
5692
|
+
const [only] = matches;
|
|
5693
|
+
if (matches.length > 1) {
|
|
5694
|
+
return { error: `"${prefix}" matches ${matches.length} processes; type more of the id` };
|
|
5695
|
+
}
|
|
5696
|
+
if (only === void 0) return { error: `no process of this session matches "${prefix}"` };
|
|
5697
|
+
return { process: only };
|
|
5698
|
+
}
|
|
5699
|
+
const chosen = detail.currentAgent ?? processes.at(-1);
|
|
5700
|
+
if (chosen === void 0) return { error: "this session has run nothing yet" };
|
|
5701
|
+
return { process: chosen };
|
|
5702
|
+
};
|
|
5703
|
+
var SETTLED2 = /* @__PURE__ */ new Set(["completed", "failed", "stopped"]);
|
|
5704
|
+
var processRowOf = (detail, processId) => {
|
|
5705
|
+
const rows = [
|
|
5706
|
+
...detail.currentAgent === null ? [] : [detail.currentAgent],
|
|
5707
|
+
...detail.processes ?? []
|
|
5708
|
+
].filter((row) => row.id === processId);
|
|
5709
|
+
return rows.find((row) => row.exitedAt !== null) ?? rows[0];
|
|
5710
|
+
};
|
|
5711
|
+
var commandEndOf = (detail, processId = null) => {
|
|
5712
|
+
const settled = SETTLED2.has(detail.session.status);
|
|
5713
|
+
if (processId !== null) {
|
|
5714
|
+
const target = processRowOf(detail, processId);
|
|
5715
|
+
if (target !== void 0 && target.exitedAt !== null) {
|
|
5716
|
+
return { processId, status: target.status, exitCode: target.exitCode };
|
|
5717
|
+
}
|
|
5718
|
+
if (settled) {
|
|
5719
|
+
return { processId, status: detail.session.status, exitCode: target?.exitCode ?? null };
|
|
5720
|
+
}
|
|
5721
|
+
return null;
|
|
5722
|
+
}
|
|
5723
|
+
if (detail.session.status === "starting") return null;
|
|
5724
|
+
const agent = detail.currentAgent;
|
|
5725
|
+
if (agent !== null && agent.exitedAt !== null) {
|
|
5726
|
+
return { processId: agent.id ?? null, status: agent.status, exitCode: agent.exitCode };
|
|
5727
|
+
}
|
|
5728
|
+
if (settled) {
|
|
5729
|
+
return {
|
|
5730
|
+
processId: agent?.id ?? null,
|
|
5731
|
+
status: detail.session.status,
|
|
5732
|
+
exitCode: agent?.exitCode ?? null
|
|
5733
|
+
};
|
|
5734
|
+
}
|
|
5735
|
+
return null;
|
|
5736
|
+
};
|
|
5737
|
+
var exitStatusOf = (end) => end.exitCode !== null && Number.isInteger(end.exitCode) && end.exitCode >= 0 && end.exitCode <= 255 ? end.exitCode : 1;
|
|
5738
|
+
var endLine = (end) => `${end.status} \xB7 ${end.exitCode === null ? "no exit code reported" : `code ${end.exitCode}`}`;
|
|
5739
|
+
var isRefusal = (error) => error instanceof MendRequestError && error.kind === "http" && (error.status === null || !GATEWAY_STATUSES.has(error.status));
|
|
5740
|
+
var beforeDeadline = async (work, clock2, deadline) => {
|
|
5741
|
+
if (deadline === null) return { done: true, value: await work };
|
|
5742
|
+
const left = deadline - clock2.now();
|
|
5743
|
+
if (left <= 0) {
|
|
5744
|
+
work.catch(() => void 0);
|
|
5745
|
+
return { done: false };
|
|
5746
|
+
}
|
|
5747
|
+
const timer = new AbortController();
|
|
5748
|
+
try {
|
|
5749
|
+
return await Promise.race([
|
|
5750
|
+
work.then((value) => ({ done: true, value })),
|
|
5751
|
+
clock2.sleep(left, timer.signal).then(() => ({ done: false }))
|
|
5752
|
+
]);
|
|
5753
|
+
} finally {
|
|
5754
|
+
timer.abort();
|
|
5755
|
+
}
|
|
5756
|
+
};
|
|
5757
|
+
var readThrough = async (read, clock2, deadline) => {
|
|
5758
|
+
const unreachableAfterMs = clock2.unreachableAfterMs ?? 6e4;
|
|
5759
|
+
const since = clock2.now();
|
|
5760
|
+
for (; ; ) {
|
|
5761
|
+
try {
|
|
5762
|
+
return await beforeDeadline(read(), clock2, deadline);
|
|
5763
|
+
} catch (error) {
|
|
5764
|
+
if (isRefusal(error) || clock2.now() - since >= unreachableAfterMs) throw error;
|
|
5765
|
+
const pause2 = deadline === null ? clock2.pollMs ?? 1e3 : Math.min(clock2.pollMs ?? 1e3, deadline - clock2.now());
|
|
5766
|
+
if (pause2 <= 0) return { done: false };
|
|
5767
|
+
await clock2.sleep(pause2);
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
};
|
|
5771
|
+
var unbounded = async (read, clock2) => {
|
|
5772
|
+
const got = await readThrough(read, clock2, null);
|
|
5773
|
+
if (!got.done) throw new Error("a read with no deadline timed out");
|
|
5774
|
+
return got.value;
|
|
5775
|
+
};
|
|
5776
|
+
var followLogs = async (options) => {
|
|
5777
|
+
const pollMs = options.pollMs ?? 250;
|
|
5778
|
+
const quietReadsAfterEnd = options.quietReadsAfterEnd ?? 2;
|
|
5779
|
+
let cursor = options.from;
|
|
5780
|
+
let quiet = 0;
|
|
5781
|
+
for (; ; ) {
|
|
5782
|
+
const from = cursor;
|
|
5783
|
+
const page = await unbounded(() => options.read(from), options);
|
|
5784
|
+
for (const chunk of page.chunks) await options.write(Buffer.from(chunk.dataBase64, "base64"));
|
|
5785
|
+
const advanced = page.nextFrom !== cursor;
|
|
5786
|
+
cursor = page.nextFrom;
|
|
5787
|
+
if (advanced) {
|
|
5788
|
+
quiet = 0;
|
|
5789
|
+
continue;
|
|
5790
|
+
}
|
|
5791
|
+
if (!options.follow) return { next: cursor, status: page.status };
|
|
5792
|
+
if (page.status === "exited" || page.status === "failed") {
|
|
5793
|
+
quiet += 1;
|
|
5794
|
+
if (quiet >= quietReadsAfterEnd) return { next: cursor, status: page.status };
|
|
5795
|
+
}
|
|
5796
|
+
await options.sleep(pollMs);
|
|
5797
|
+
}
|
|
5798
|
+
};
|
|
5799
|
+
var waitForCommand = async (options) => {
|
|
5800
|
+
const pollMs = options.pollMs ?? 1e3;
|
|
5801
|
+
let last = null;
|
|
5802
|
+
for (; ; ) {
|
|
5803
|
+
const got = await readThrough(options.read, options, options.deadline);
|
|
5804
|
+
if (!got.done) return { kind: "timeout", last };
|
|
5805
|
+
last = got.value;
|
|
5806
|
+
const end = commandEndOf(got.value, options.processId);
|
|
5807
|
+
if (end !== null) return { kind: "ended", end };
|
|
5808
|
+
const left = options.deadline === null ? pollMs : Math.min(pollMs, options.deadline - options.now());
|
|
5809
|
+
if (left <= 0) return { kind: "timeout", last };
|
|
5810
|
+
await options.sleep(left);
|
|
5811
|
+
}
|
|
5812
|
+
};
|
|
5813
|
+
var splitArgs = (args, valued, switches) => {
|
|
5814
|
+
const values = /* @__PURE__ */ new Map();
|
|
5815
|
+
const on = /* @__PURE__ */ new Set();
|
|
5816
|
+
let session = null;
|
|
5817
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
5818
|
+
const arg = args[index] ?? "";
|
|
5819
|
+
if (valued.includes(arg)) {
|
|
5820
|
+
const value = args[index + 1];
|
|
5821
|
+
if (value === void 0) return { error: `${arg} needs a value` };
|
|
5822
|
+
values.set(arg, value);
|
|
5823
|
+
index += 1;
|
|
5824
|
+
} else if (switches.includes(arg)) {
|
|
5825
|
+
on.add(arg);
|
|
5826
|
+
} else if (arg.startsWith("-")) {
|
|
5827
|
+
return { error: `unknown flag ${arg}` };
|
|
5828
|
+
} else if (session !== null) {
|
|
5829
|
+
return { error: `one session only; "${arg}" is extra` };
|
|
5830
|
+
} else {
|
|
5831
|
+
session = arg;
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
return { values, on, session };
|
|
5835
|
+
};
|
|
5836
|
+
var parseLogsArgs = (args) => {
|
|
5837
|
+
const split = splitArgs(args, ["--from", "--process"], ["--follow", "-f"]);
|
|
5838
|
+
if ("error" in split) return split;
|
|
5839
|
+
const from = split.values.get("--from") ?? "0";
|
|
5840
|
+
if (!/^(0|[1-9]\d*)$/u.test(from)) return { error: "--from takes a record sequence, e.g. 0" };
|
|
5841
|
+
return {
|
|
5842
|
+
args: {
|
|
5843
|
+
session: split.session,
|
|
5844
|
+
follow: split.on.has("--follow") || split.on.has("-f"),
|
|
5845
|
+
from,
|
|
5846
|
+
process: split.values.get("--process") ?? null
|
|
5847
|
+
}
|
|
5848
|
+
};
|
|
5849
|
+
};
|
|
5850
|
+
var parseWaitArgs = (args) => {
|
|
5851
|
+
const split = splitArgs(args, ["--timeout", "--process"], ["--json"]);
|
|
5852
|
+
if ("error" in split) return split;
|
|
5853
|
+
const timeout = split.values.get("--timeout");
|
|
5854
|
+
const seconds2 = timeout === void 0 ? null : Number(timeout);
|
|
5855
|
+
if (seconds2 !== null && !(Number.isFinite(seconds2) && seconds2 > 0)) {
|
|
5856
|
+
return { error: "--timeout takes a number of seconds above 0" };
|
|
5857
|
+
}
|
|
5858
|
+
return {
|
|
5859
|
+
args: {
|
|
5860
|
+
session: split.session,
|
|
5861
|
+
process: split.values.get("--process") ?? null,
|
|
5862
|
+
timeoutMs: seconds2 === null ? null : Math.ceil(seconds2 * 1e3),
|
|
5863
|
+
json: split.on.has("--json")
|
|
5864
|
+
}
|
|
5865
|
+
};
|
|
5866
|
+
};
|
|
5867
|
+
var WAIT_TIMED_OUT = 124;
|
|
5868
|
+
var isSessionId = (word) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(word);
|
|
5869
|
+
|
|
5572
5870
|
// src/secret-files.ts
|
|
5573
5871
|
import * as path14 from "node:path";
|
|
5574
5872
|
var secretFilePathOf = (input, home) => {
|
|
@@ -6542,6 +6840,7 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
|
|
|
6542
6840
|
"--context",
|
|
6543
6841
|
"--version",
|
|
6544
6842
|
"--bind",
|
|
6843
|
+
"--ssh-bind",
|
|
6545
6844
|
"--url",
|
|
6546
6845
|
"--origin",
|
|
6547
6846
|
"--port",
|
|
@@ -6552,7 +6851,8 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
|
|
|
6552
6851
|
"--edge",
|
|
6553
6852
|
"--no-edge",
|
|
6554
6853
|
"--exposure",
|
|
6555
|
-
"--tenancy"
|
|
6854
|
+
"--tenancy",
|
|
6855
|
+
"--declare"
|
|
6556
6856
|
]);
|
|
6557
6857
|
var parseExposure = (value) => {
|
|
6558
6858
|
if (!isExposure(value))
|
|
@@ -6564,6 +6864,21 @@ var parseTenancy = (value) => {
|
|
|
6564
6864
|
throw setupError(`--tenancy must be one of ${TENANCIES.join(", ")}, not "${value}".`);
|
|
6565
6865
|
return value;
|
|
6566
6866
|
};
|
|
6867
|
+
var parseDeclared = (values) => {
|
|
6868
|
+
if (values.length === 1 && values[0] === "none") return [];
|
|
6869
|
+
return [
|
|
6870
|
+
...new Set(
|
|
6871
|
+
values.map((value) => {
|
|
6872
|
+
if (!isDeclarableItem(value)) {
|
|
6873
|
+
throw setupError(
|
|
6874
|
+
`--declare takes ${DECLARABLE_ITEMS.join(", ")} or none, not "${value}". Every other gate item is observed by the server, never stated.`
|
|
6875
|
+
);
|
|
6876
|
+
}
|
|
6877
|
+
return value;
|
|
6878
|
+
})
|
|
6879
|
+
)
|
|
6880
|
+
];
|
|
6881
|
+
};
|
|
6567
6882
|
var parseEdge = (value) => {
|
|
6568
6883
|
const host = parseEdgeHost(value);
|
|
6569
6884
|
if (host === null) {
|
|
@@ -6588,7 +6903,7 @@ var parseSetupOptions = (args) => {
|
|
|
6588
6903
|
const [value, valueIndex] = nextFlagValue(args, index, flag);
|
|
6589
6904
|
index = valueIndex;
|
|
6590
6905
|
const previous = values.get(flag) ?? [];
|
|
6591
|
-
if (flag !== "--origin" && previous.length > 0) {
|
|
6906
|
+
if (flag !== "--origin" && flag !== "--declare" && previous.length > 0) {
|
|
6592
6907
|
throw setupError(`${flag} may be supplied only once.`);
|
|
6593
6908
|
}
|
|
6594
6909
|
previous.push(value);
|
|
@@ -6602,12 +6917,14 @@ var parseSetupOptions = (args) => {
|
|
|
6602
6917
|
const edge = flagValue3("--edge");
|
|
6603
6918
|
const exposure = flagValue3("--exposure");
|
|
6604
6919
|
const tenancy = flagValue3("--tenancy");
|
|
6920
|
+
const declared = values.get("--declare");
|
|
6605
6921
|
if (edge !== void 0 && values.has("--no-edge"))
|
|
6606
6922
|
throw setupError("--edge and --no-edge contradict each other.");
|
|
6607
6923
|
return {
|
|
6608
6924
|
context: flagValue3("--context"),
|
|
6609
6925
|
version: version === void 0 ? void 0 : parseVersion(version),
|
|
6610
6926
|
bind: flagValue3("--bind"),
|
|
6927
|
+
sshBind: flagValue3("--ssh-bind"),
|
|
6611
6928
|
url: flagValue3("--url"),
|
|
6612
6929
|
origins: origins === void 0 ? void 0 : origins,
|
|
6613
6930
|
appPort: appPort === void 0 ? void 0 : parsePort(appPort, "--port"),
|
|
@@ -6618,7 +6935,8 @@ var parseSetupOptions = (args) => {
|
|
|
6618
6935
|
edge: edge === void 0 ? void 0 : parseEdge(edge),
|
|
6619
6936
|
noEdge: values.has("--no-edge"),
|
|
6620
6937
|
exposure: exposure === void 0 ? void 0 : parseExposure(exposure),
|
|
6621
|
-
tenancy: tenancy === void 0 ? void 0 : parseTenancy(tenancy)
|
|
6938
|
+
tenancy: tenancy === void 0 ? void 0 : parseTenancy(tenancy),
|
|
6939
|
+
declared: declared === void 0 ? void 0 : parseDeclared(declared)
|
|
6622
6940
|
};
|
|
6623
6941
|
};
|
|
6624
6942
|
var parseUrl = (input, label) => {
|
|
@@ -6652,9 +6970,13 @@ var resolveAppUrl = (existing, options, appPort, edgeHost) => {
|
|
|
6652
6970
|
}
|
|
6653
6971
|
return origin;
|
|
6654
6972
|
}
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6973
|
+
if (existing?.edgeHost !== void 0) {
|
|
6974
|
+
return parseHttpOrigin(options.url ?? `http://localhost:${appPort}`, "--url");
|
|
6975
|
+
}
|
|
6976
|
+
const saved = existing === null ? null : new URL(existing.appUrl);
|
|
6977
|
+
const savedPointsAtOldPort = saved !== null && existing !== null && options.appPort !== void 0 && saved.protocol === "http:" && saved.port !== "" && Number(saved.port) === existing.appPort;
|
|
6978
|
+
if (saved !== null && savedPointsAtOldPort) saved.port = String(appPort);
|
|
6979
|
+
const fallback = saved === null ? `http://localhost:${appPort}` : saved.origin;
|
|
6658
6980
|
return parseHttpOrigin(options.url ?? fallback, "--url");
|
|
6659
6981
|
};
|
|
6660
6982
|
var checkExposurePair = (bind, appUrl, requireExplicitUrl) => {
|
|
@@ -6669,7 +6991,7 @@ var checkExposurePair = (bind, appUrl, requireExplicitUrl) => {
|
|
|
6669
6991
|
);
|
|
6670
6992
|
}
|
|
6671
6993
|
};
|
|
6672
|
-
var checkPosture = (bind, ports, edgeHost, exposure) => {
|
|
6994
|
+
var checkPosture = (bind, sshBind, ports, edgeHost, exposure, declared) => {
|
|
6673
6995
|
if (edgeHost !== void 0 && [ports.appPort, ports.sshPort].some((p) => p === 80 || p === 443)) {
|
|
6674
6996
|
throw setupError(
|
|
6675
6997
|
"With an edge, --port and --ssh-port must not be 80 or 443: the edge publishes both on every interface."
|
|
@@ -6690,6 +7012,16 @@ var checkPosture = (bind, ports, edgeHost, exposure) => {
|
|
|
6690
7012
|
"--exposure loopback contradicts a non-loopback --bind: the port is published beyond this machine."
|
|
6691
7013
|
);
|
|
6692
7014
|
}
|
|
7015
|
+
if (exposure === "loopback" && !isLoopbackBind(sshBind)) {
|
|
7016
|
+
throw setupError(
|
|
7017
|
+
"--exposure loopback contradicts a non-loopback --ssh-bind: the SSH port is published beyond this machine."
|
|
7018
|
+
);
|
|
7019
|
+
}
|
|
7020
|
+
if (exposure === "public" && !isLoopbackBind(sshBind) && !declared.includes("workspace-ssh")) {
|
|
7021
|
+
throw setupError(
|
|
7022
|
+
`--exposure public with workspace SSH published on ${publishedAddress(sshBind, ports.sshPort)} beside the edge needs a statement Mend cannot observe: who reaches that port. Check it from each network that should not reach it, then add --declare workspace-ssh; or publish SSH on loopback.`
|
|
7023
|
+
);
|
|
7024
|
+
}
|
|
6693
7025
|
if (exposure === "public" && edgeHost === void 0) {
|
|
6694
7026
|
throw setupError(
|
|
6695
7027
|
"--exposure public needs the edge: add --edge <host>. Without it nothing sets MEND_TRUSTED_PROXIES or an https origin, and the server refuses to start as public."
|
|
@@ -6708,10 +7040,18 @@ var validateExposure = (existing, options) => {
|
|
|
6708
7040
|
"--bind must be a literal IPv4 or IPv6 address, such as 127.0.0.1 or 0.0.0.0."
|
|
6709
7041
|
);
|
|
6710
7042
|
}
|
|
7043
|
+
const requestedSshBind = options.sshBind ?? existing?.sshBind;
|
|
7044
|
+
if (requestedSshBind !== void 0 && net3.isIP(requestedSshBind) === 0) {
|
|
7045
|
+
throw setupError(
|
|
7046
|
+
"--ssh-bind must be a literal IPv4 or IPv6 address, such as 127.0.0.1 or 0.0.0.0."
|
|
7047
|
+
);
|
|
7048
|
+
}
|
|
7049
|
+
const sshBind = requestedSshBind === bind ? void 0 : requestedSshBind;
|
|
6711
7050
|
const edgeHost = options.noEdge ? void 0 : options.edge ?? existing?.edgeHost;
|
|
6712
7051
|
const exposure = options.exposure ?? existing?.exposure;
|
|
6713
7052
|
const tenancy = options.tenancy ?? existing?.tenancy;
|
|
6714
|
-
|
|
7053
|
+
const declared = options.declared ?? existing?.declared ?? [];
|
|
7054
|
+
checkPosture(bind, sshBind ?? bind, { appPort, sshPort }, edgeHost, exposure, declared);
|
|
6715
7055
|
const appUrl = resolveAppUrl(existing, options, appPort, edgeHost);
|
|
6716
7056
|
if (edgeHost === void 0)
|
|
6717
7057
|
checkExposurePair(bind, appUrl, options.url === void 0 && existing === null);
|
|
@@ -6722,13 +7062,15 @@ var validateExposure = (existing, options) => {
|
|
|
6722
7062
|
];
|
|
6723
7063
|
return {
|
|
6724
7064
|
bind,
|
|
7065
|
+
...sshBind === void 0 ? {} : { sshBind },
|
|
6725
7066
|
appUrl,
|
|
6726
7067
|
allowedOrigins,
|
|
6727
7068
|
appPort,
|
|
6728
7069
|
sshPort,
|
|
6729
7070
|
...edgeHost === void 0 ? {} : { edgeHost },
|
|
6730
7071
|
...exposure === void 0 ? {} : { exposure },
|
|
6731
|
-
...tenancy === void 0 ? {} : { tenancy }
|
|
7072
|
+
...tenancy === void 0 ? {} : { tenancy },
|
|
7073
|
+
...declared.length === 0 ? {} : { declared }
|
|
6732
7074
|
};
|
|
6733
7075
|
};
|
|
6734
7076
|
var parseServerConfig = (raw) => {
|
|
@@ -6767,6 +7109,13 @@ var parseServerConfig = (raw) => {
|
|
|
6767
7109
|
if (tenancy !== void 0 && !isTenancy(tenancy)) {
|
|
6768
7110
|
throw setupError(`Server config is corrupt: tenancy must be one of ${TENANCIES.join(", ")}.`);
|
|
6769
7111
|
}
|
|
7112
|
+
const declaredField = fields.get("declared");
|
|
7113
|
+
if (declaredField !== void 0 && (!Array.isArray(declaredField) || !declaredField.every((item) => typeof item === "string" && isDeclarableItem(item)))) {
|
|
7114
|
+
throw setupError(
|
|
7115
|
+
`Server config is corrupt: declared must be an array of ${DECLARABLE_ITEMS.join(", ")}.`
|
|
7116
|
+
);
|
|
7117
|
+
}
|
|
7118
|
+
const declared = Array.isArray(declaredField) ? declaredField.filter((item) => isDeclarableItem(String(item))) : [];
|
|
6770
7119
|
const config = {
|
|
6771
7120
|
schemaVersion: requiredInteger(fields, "schemaVersion"),
|
|
6772
7121
|
assetContract: requiredString(fields, "assetContract"),
|
|
@@ -6776,6 +7125,7 @@ var parseServerConfig = (raw) => {
|
|
|
6776
7125
|
dockerSocket: parseDockerSocketPath(requiredString(fields, "dockerSocket")),
|
|
6777
7126
|
dockerSocketSource,
|
|
6778
7127
|
bind: requiredString(fields, "bind"),
|
|
7128
|
+
...fields.has("sshBind") ? { sshBind: requiredString(fields, "sshBind") } : {},
|
|
6779
7129
|
appUrl: parseHttpOrigin(requiredString(fields, "appUrl"), "Server config appUrl"),
|
|
6780
7130
|
allowedOrigins: origins.map(
|
|
6781
7131
|
(origin) => parseHttpOrigin(origin, "Server config allowedOrigins")
|
|
@@ -6786,7 +7136,8 @@ var parseServerConfig = (raw) => {
|
|
|
6786
7136
|
...fields.has("bucket") ? { bucket: requiredBucket(fields) } : {},
|
|
6787
7137
|
...edgeHost === void 0 ? {} : { edgeHost },
|
|
6788
7138
|
...exposure === void 0 ? {} : { exposure },
|
|
6789
|
-
...tenancy === void 0 ? {} : { tenancy }
|
|
7139
|
+
...tenancy === void 0 ? {} : { tenancy },
|
|
7140
|
+
...declared.length === 0 ? {} : { declared }
|
|
6790
7141
|
};
|
|
6791
7142
|
if (config.schemaVersion !== CONFIG_SCHEMA_VERSION || config.assetContract !== ASSET_CONTRACT && !LEGACY_ASSET_CONTRACTS.has(config.assetContract)) {
|
|
6792
7143
|
throw setupError(
|
|
@@ -6801,6 +7152,7 @@ var parseServerConfig = (raw) => {
|
|
|
6801
7152
|
context: void 0,
|
|
6802
7153
|
version: void 0,
|
|
6803
7154
|
bind: config.bind,
|
|
7155
|
+
sshBind: config.sshBind,
|
|
6804
7156
|
url: config.appUrl,
|
|
6805
7157
|
origins: config.allowedOrigins,
|
|
6806
7158
|
appPort: config.appPort,
|
|
@@ -6811,7 +7163,8 @@ var parseServerConfig = (raw) => {
|
|
|
6811
7163
|
edge: config.edgeHost,
|
|
6812
7164
|
noEdge: false,
|
|
6813
7165
|
exposure: config.exposure,
|
|
6814
|
-
tenancy: config.tenancy
|
|
7166
|
+
tenancy: config.tenancy,
|
|
7167
|
+
declared: config.declared
|
|
6815
7168
|
});
|
|
6816
7169
|
return config;
|
|
6817
7170
|
};
|
|
@@ -7148,9 +7501,10 @@ var renderGarage = (secrets, config) => {
|
|
|
7148
7501
|
];
|
|
7149
7502
|
};
|
|
7150
7503
|
var namespaceOf = (config) => config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
|
|
7504
|
+
var composeHost = (address) => net3.isIP(address) === 6 ? `[${address}]` : address;
|
|
7151
7505
|
var renderSecrets = (secrets, config) => {
|
|
7152
7506
|
const sshHost = parseUrl(config.appUrl, "Server config appUrl").hostname.replace(/^\[|\]$/g, "");
|
|
7153
|
-
const composeBind =
|
|
7507
|
+
const composeBind = composeHost(config.bind);
|
|
7154
7508
|
return [
|
|
7155
7509
|
`MEND_VERSION=${config.serverVersion}`,
|
|
7156
7510
|
"MEND_IMAGE_REPOSITORY=ghcr.io/sealant-sh/mend",
|
|
@@ -7158,6 +7512,7 @@ var renderSecrets = (secrets, config) => {
|
|
|
7158
7512
|
`APP_URL=${config.appUrl}`,
|
|
7159
7513
|
`MEND_ALLOWED_ORIGINS=${JSON.stringify(config.allowedOrigins)}`,
|
|
7160
7514
|
`MEND_BIND_HOST=${composeBind}`,
|
|
7515
|
+
...config.sshBind === void 0 ? [] : [`MEND_SSH_BIND_HOST=${composeHost(config.sshBind)}`],
|
|
7161
7516
|
`MEND_PORT=${config.appPort}`,
|
|
7162
7517
|
`MEND_SSH_PORT=${config.sshPort}`,
|
|
7163
7518
|
...config.registryPort === void 0 ? [] : [`MEND_REGISTRY_PORT=${config.registryPort}`],
|
|
@@ -7357,6 +7712,13 @@ var resolveDockerSocket = (runtime, options, existing, selectedContext) => {
|
|
|
7357
7712
|
dockerSocketSource: override === void 0 ? "detected" : "override"
|
|
7358
7713
|
};
|
|
7359
7714
|
};
|
|
7715
|
+
var checkSshBindAsset = (config, compose) => {
|
|
7716
|
+
if (config.sshBind !== void 0 && !compose.includes("MEND_SSH_BIND_HOST")) {
|
|
7717
|
+
throw setupError(
|
|
7718
|
+
`Mend ${config.serverVersion}'s ${COMPOSE_ASSET} publishes workspace SSH on --bind and cannot honour --ssh-bind ${config.sshBind}. Use a release that supports --ssh-bind, or pass --ssh-bind with the --bind address.`
|
|
7719
|
+
);
|
|
7720
|
+
}
|
|
7721
|
+
};
|
|
7360
7722
|
var resolveAssets = async (runtime, serverVersion, existing, store, options) => {
|
|
7361
7723
|
if (options.assetsDir !== void 0) {
|
|
7362
7724
|
try {
|
|
@@ -7590,7 +7952,28 @@ var startCompose = async (runtime, installation) => {
|
|
|
7590
7952
|
);
|
|
7591
7953
|
if (compose.status !== 0) throw commandFailure("Mend containers did not start", compose);
|
|
7592
7954
|
};
|
|
7593
|
-
var
|
|
7955
|
+
var SSH_PROBE_BOUND_MS = 1e4;
|
|
7956
|
+
var sshPublicationLine = async (runtime, config) => {
|
|
7957
|
+
if (config.sshBind === void 0 || isLoopbackBind(config.sshBind)) return null;
|
|
7958
|
+
const published = publishedAddress(config.sshBind, config.sshPort);
|
|
7959
|
+
if (runtime.probeSsh === void 0) return null;
|
|
7960
|
+
const boundMs = runtime.sshProbeBoundMs ?? SSH_PROBE_BOUND_MS;
|
|
7961
|
+
let bound;
|
|
7962
|
+
const probes = await Promise.race([
|
|
7963
|
+
runtime.probeSsh(config.sshBind, config.sshPort),
|
|
7964
|
+
new Promise((resolve8) => {
|
|
7965
|
+
bound = setTimeout(() => resolve8(null), boundMs);
|
|
7966
|
+
})
|
|
7967
|
+
]).finally(() => clearTimeout(bound));
|
|
7968
|
+
if (probes === null) {
|
|
7969
|
+
return `Workspace SSH is published on ${published}. This machine's look at it did not finish within ${boundMs / 1e3} s. Who reaches it is up to the network and its firewall; mend operator exposure reports it as workspace-ssh.`;
|
|
7970
|
+
}
|
|
7971
|
+
const observed = probes.length === 0 ? "this machine has no address to try it at" : probes.map(
|
|
7972
|
+
(probe) => probe.banner === null ? `${publishedAddress(probe.address, config.sshPort)} did not answer` : `${publishedAddress(probe.address, config.sshPort)} answers (${probe.banner})`
|
|
7973
|
+
).join(", ");
|
|
7974
|
+
return `Workspace SSH is published on ${published}. From this machine: ${observed}. Who else reaches it is up to the network and its firewall; mend operator exposure reports it as workspace-ssh.`;
|
|
7975
|
+
};
|
|
7976
|
+
var edgeStartedLine = (config) => config.edgeHost === void 0 ? null : `The edge for ${config.edgeHost} is up on 80 and 443. Caddy asks for its certificate once ${config.edgeHost} resolves to this machine and both ports reach it from the Internet. mend server status says whether it holds one, and mend server logs shows what Caddy tried.${config.sshBind === void 0 ? ` Workspace SSH is published on ${config.bind}:${config.sshPort} only, so Remote-SSH and mend ssh from another machine cannot reach it; --ssh-bind 0.0.0.0 publishes it.` : ` Workspace SSH is published on ${config.sshBind}:${config.sshPort}.`}`;
|
|
7594
7977
|
var setupServer = async (args, runtime, store) => {
|
|
7595
7978
|
if (runtime.platform !== "linux" && runtime.platform !== "darwin") {
|
|
7596
7979
|
throw setupError(`mend server setup supports Linux and macOS, not ${runtime.platform}.`);
|
|
@@ -7629,6 +8012,7 @@ var setupServer = async (args, runtime, store) => {
|
|
|
7629
8012
|
...validateExposure(existing?.config ?? null, options)
|
|
7630
8013
|
};
|
|
7631
8014
|
const assets = await resolveAssets(runtime, serverVersion, existing, store, options);
|
|
8015
|
+
checkSshBindAsset(configWithoutBucket, assets.compose);
|
|
7632
8016
|
const bucket = composeBucket(assets.compose);
|
|
7633
8017
|
if (bucket !== void 0 && runtime.dockerDaemonFacts !== void 0) {
|
|
7634
8018
|
const info = await runtime.run("docker", [
|
|
@@ -7679,6 +8063,8 @@ var setupServer = async (args, runtime, store) => {
|
|
|
7679
8063
|
runtime.writeLine(reachableLine(config));
|
|
7680
8064
|
const edgeStarted = edgeStartedLine(config);
|
|
7681
8065
|
if (edgeStarted !== null) runtime.writeLine(edgeStarted);
|
|
8066
|
+
const sshPublication = await sshPublicationLine(runtime, config);
|
|
8067
|
+
if (sshPublication !== null) runtime.writeLine(sshPublication);
|
|
7682
8068
|
if (existing?.config.edgeHost !== void 0 && config.edgeHost === void 0) {
|
|
7683
8069
|
runtime.writeLine(
|
|
7684
8070
|
`The edge for ${existing.config.edgeHost} is gone. Its certificate volumes stay until you remove them: docker --context ${config.dockerContext} volume rm mend_mend-edge-data mend_mend-edge-config`
|
|
@@ -8001,6 +8387,7 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
8001
8387
|
const previous = storeValue(store.readActive());
|
|
8002
8388
|
if (previous === null) throw setupError("The active server generation is missing.");
|
|
8003
8389
|
const assets = await resolveAssets(runtime, version, existing, store, options);
|
|
8390
|
+
checkSshBindAsset({ ...existing.config, serverVersion: version }, assets.compose);
|
|
8004
8391
|
const {
|
|
8005
8392
|
registryPort: _legacyRegistryPort,
|
|
8006
8393
|
bucket: _previousBucket,
|
|
@@ -8337,12 +8724,48 @@ var nodeServerRuntime = () => {
|
|
|
8337
8724
|
},
|
|
8338
8725
|
randomBytes,
|
|
8339
8726
|
sleep: (milliseconds) => new Promise((resolve8) => setTimeout(resolve8, milliseconds)),
|
|
8340
|
-
writeLine: (line) => process.stdout.write(`${line}
|
|
8727
|
+
writeLine: (line) => process.stdout.write(`${redactCredentials(line)}
|
|
8341
8728
|
`),
|
|
8342
8729
|
dockerDaemonFacts: hostDockerDaemonFacts,
|
|
8343
|
-
readLogin: (configDir) => savedLogin(configDir, environment)
|
|
8730
|
+
readLogin: (configDir) => savedLogin(configDir, environment),
|
|
8731
|
+
probeSsh: probeSshFromHere
|
|
8344
8732
|
};
|
|
8345
8733
|
};
|
|
8734
|
+
var sshBannerAt = async (host, port, timeoutMs) => {
|
|
8735
|
+
const socket = net3.connect({ host, port });
|
|
8736
|
+
let deadline;
|
|
8737
|
+
try {
|
|
8738
|
+
return await Promise.race([
|
|
8739
|
+
new Promise((resolve8) => {
|
|
8740
|
+
socket.once(
|
|
8741
|
+
"data",
|
|
8742
|
+
(bytes) => resolve8(bytes.toString("utf8").split(/\r?\n/)[0]?.trim() ?? "")
|
|
8743
|
+
);
|
|
8744
|
+
socket.once("end", () => resolve8(null));
|
|
8745
|
+
socket.once("close", () => resolve8(null));
|
|
8746
|
+
socket.once("error", () => resolve8(null));
|
|
8747
|
+
}),
|
|
8748
|
+
new Promise((resolve8) => {
|
|
8749
|
+
deadline = setTimeout(() => resolve8(null), timeoutMs);
|
|
8750
|
+
})
|
|
8751
|
+
]);
|
|
8752
|
+
} finally {
|
|
8753
|
+
clearTimeout(deadline);
|
|
8754
|
+
socket.destroy();
|
|
8755
|
+
}
|
|
8756
|
+
};
|
|
8757
|
+
var probeSshFromHere = async (bind, port) => {
|
|
8758
|
+
const unspecified = bind === "0.0.0.0" || bind === "::";
|
|
8759
|
+
const addresses = unspecified ? Object.values(os10.networkInterfaces()).flatMap((entries) => entries ?? []).filter(
|
|
8760
|
+
(entry) => !entry.internal && (entry.family === "IPv4" || bind === "::" && !entry.address.startsWith("fe80"))
|
|
8761
|
+
).map((entry) => entry.address).slice(0, 4) : [bind];
|
|
8762
|
+
return Promise.all(
|
|
8763
|
+
addresses.map(async (address) => ({
|
|
8764
|
+
address,
|
|
8765
|
+
banner: await sshBannerAt(address, port, 3e3)
|
|
8766
|
+
}))
|
|
8767
|
+
);
|
|
8768
|
+
};
|
|
8346
8769
|
var savedLogin = (configDir, environment) => {
|
|
8347
8770
|
let saved = null;
|
|
8348
8771
|
try {
|
|
@@ -9317,7 +9740,7 @@ var ansi = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${t
|
|
|
9317
9740
|
var dim5 = ansi("2");
|
|
9318
9741
|
var green5 = ansi("32");
|
|
9319
9742
|
var warn = ansi("33");
|
|
9320
|
-
var say5 = (line) => console.log(line);
|
|
9743
|
+
var say5 = (line) => console.log(redactCredentials(line));
|
|
9321
9744
|
var sshConfigPath = () => path19.join(os12.homedir(), ".ssh", "config");
|
|
9322
9745
|
var flagValue2 = (args, flag) => {
|
|
9323
9746
|
const index = args.indexOf(flag);
|
|
@@ -9757,10 +10180,10 @@ var upgradeUrl = async (input) => {
|
|
|
9757
10180
|
var paint5 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
9758
10181
|
var dim6 = paint5("2");
|
|
9759
10182
|
var green6 = paint5("32");
|
|
9760
|
-
var say6 = (line) => process.stdout.write(`${line}
|
|
10183
|
+
var say6 = (line) => process.stdout.write(`${redactCredentials(line)}
|
|
9761
10184
|
`);
|
|
9762
10185
|
var fail6 = (message) => {
|
|
9763
|
-
process.stderr.write(`mend: ${message}
|
|
10186
|
+
process.stderr.write(`mend: ${redactCredentials(message)}
|
|
9764
10187
|
`);
|
|
9765
10188
|
process.exit(1);
|
|
9766
10189
|
};
|
|
@@ -9922,7 +10345,7 @@ var loadConfig = () => {
|
|
|
9922
10345
|
};
|
|
9923
10346
|
};
|
|
9924
10347
|
var fail7 = (message) => {
|
|
9925
|
-
process.stderr.write(`mend: ${message}
|
|
10348
|
+
process.stderr.write(`mend: ${redactCredentials(message)}
|
|
9926
10349
|
`);
|
|
9927
10350
|
process.exit(1);
|
|
9928
10351
|
};
|
|
@@ -9933,12 +10356,16 @@ var parseMendUrl = (value) => {
|
|
|
9933
10356
|
return fail7(`"${value}" is not a valid Mend URL`);
|
|
9934
10357
|
}
|
|
9935
10358
|
};
|
|
9936
|
-
var
|
|
10359
|
+
var chrome = process.stdout;
|
|
10360
|
+
var chromeToStderr = () => {
|
|
10361
|
+
chrome = process.stderr;
|
|
10362
|
+
};
|
|
10363
|
+
var paint6 = (code) => (text) => chrome.isTTY ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
9937
10364
|
var dim7 = paint6("2");
|
|
9938
10365
|
var green7 = paint6("32");
|
|
9939
10366
|
var amber2 = paint6("33");
|
|
9940
10367
|
var cobalt2 = paint6("34");
|
|
9941
|
-
var say7 = (line) =>
|
|
10368
|
+
var say7 = (line) => chrome.write(`${redactCredentials(line)}
|
|
9942
10369
|
`);
|
|
9943
10370
|
var detachKeyEnabled = process.env["MEND_DETACH_KEY"] !== "none";
|
|
9944
10371
|
var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${dim7("Ctrl+]")}` : "";
|
|
@@ -10077,15 +10504,15 @@ var socketUrl = (config, target, params, extra, signal) => upgradeUrl({
|
|
|
10077
10504
|
});
|
|
10078
10505
|
var boundApi = (config) => (method, route, body) => api(config, method, route, body);
|
|
10079
10506
|
var withSpinner = async (label, work) => {
|
|
10080
|
-
if (
|
|
10507
|
+
if (chrome.isTTY !== true) return work;
|
|
10081
10508
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
10082
10509
|
const started = Date.now();
|
|
10083
10510
|
let frame = 0;
|
|
10084
10511
|
const timer = setInterval(() => {
|
|
10085
10512
|
const seconds2 = Math.round((Date.now() - started) / 1e3);
|
|
10086
10513
|
const text = typeof label === "string" ? label : label();
|
|
10087
|
-
|
|
10088
|
-
`\r\x1B[2K ${frames[frame % frames.length]} ${text} ${dim7(`${seconds2}s`)} `
|
|
10514
|
+
chrome.write(
|
|
10515
|
+
`\r\x1B[2K ${frames[frame % frames.length]} ${redactCredentials(text)} ${dim7(`${seconds2}s`)} `
|
|
10089
10516
|
);
|
|
10090
10517
|
frame += 1;
|
|
10091
10518
|
}, 120);
|
|
@@ -10093,7 +10520,7 @@ var withSpinner = async (label, work) => {
|
|
|
10093
10520
|
return await work;
|
|
10094
10521
|
} finally {
|
|
10095
10522
|
clearInterval(timer);
|
|
10096
|
-
|
|
10523
|
+
chrome.write("\r\x1B[2K");
|
|
10097
10524
|
}
|
|
10098
10525
|
};
|
|
10099
10526
|
var followStarting = async (config, sessionId, label, start) => {
|
|
@@ -10114,7 +10541,7 @@ var followStarting = async (config, sessionId, label, start) => {
|
|
|
10114
10541
|
read: () => request(config, "GET", `/sessions/${sessionId}`),
|
|
10115
10542
|
onLine: (next) => {
|
|
10116
10543
|
line = next === "starting" ? label : next;
|
|
10117
|
-
if (
|
|
10544
|
+
if (chrome.isTTY !== true) say7(dim7(` ${next}`));
|
|
10118
10545
|
},
|
|
10119
10546
|
sleep: sleep3,
|
|
10120
10547
|
now: Date.now
|
|
@@ -10301,8 +10728,12 @@ var workspaceLinesOf = async (config, sessions) => {
|
|
|
10301
10728
|
);
|
|
10302
10729
|
};
|
|
10303
10730
|
var launch = async (config, harness, args) => {
|
|
10731
|
+
if (harness === "run") chromeToStderr();
|
|
10304
10732
|
const parsed = parseLaunchArgs(args);
|
|
10305
10733
|
if (parsed.error !== null) return fail7(parsed.error);
|
|
10734
|
+
if (harness !== "run" && parsed.json) {
|
|
10735
|
+
return fail7(`mend ${harness} takes no --json \xB7 mend run --json prints what it started`);
|
|
10736
|
+
}
|
|
10306
10737
|
const structured = parsed.prompt !== null || parsed.model !== null || parsed.effort !== null || parsed.ask || parsed.fast;
|
|
10307
10738
|
if (harness === "run" && structured) {
|
|
10308
10739
|
return fail7(`mend run takes no prompt or harness flags \xB7 ${usageOf("run")}`);
|
|
@@ -10312,15 +10743,17 @@ var launch = async (config, harness, args) => {
|
|
|
10312
10743
|
"mend run takes no landing flags \u2014 a command has no turns to land after; land it with mend land"
|
|
10313
10744
|
);
|
|
10314
10745
|
}
|
|
10315
|
-
if (harness === "run" &&
|
|
10746
|
+
if (harness === "run" && parsed.foreground) {
|
|
10316
10747
|
return fail7(
|
|
10317
|
-
"mend run takes no
|
|
10748
|
+
"mend run takes no --foreground \u2014 Ctrl+C stops watching, not the command; mend stop ends it"
|
|
10318
10749
|
);
|
|
10319
10750
|
}
|
|
10320
10751
|
const argv = harness === "run" ? parsed.custom : HARNESS_COMMANDS[harness] ?? [];
|
|
10321
10752
|
if (argv.length === 0) {
|
|
10322
10753
|
return fail7(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
|
|
10323
10754
|
}
|
|
10755
|
+
const argvIssue = harness === "run" ? runArgvIssue(argv) : null;
|
|
10756
|
+
if (argvIssue !== null) return fail7(`${argvIssue} \xB7 nothing was created`);
|
|
10324
10757
|
const project2 = await findProject(config, parsed.project, true);
|
|
10325
10758
|
say7(
|
|
10326
10759
|
`${green7("\u2713")} project ${project2.name} ${dim7(`\xB7 ${project2.defaultBranch}${parsed.project === null ? " \xB7 from cwd" : ""}`)}`
|
|
@@ -10392,7 +10825,7 @@ var launch = async (config, harness, args) => {
|
|
|
10392
10825
|
);
|
|
10393
10826
|
say7(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
10394
10827
|
if (harness === "run") {
|
|
10395
|
-
return supervisedRun(config, session, argv);
|
|
10828
|
+
return supervisedRun(config, session, argv, { detach: parsed.detach, json: parsed.json });
|
|
10396
10829
|
}
|
|
10397
10830
|
const launchBody = {
|
|
10398
10831
|
...parsed.prompt === null ? {} : { prompt: parsed.prompt },
|
|
@@ -10559,7 +10992,7 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
10559
10992
|
process.stdout.write("\x07");
|
|
10560
10993
|
}
|
|
10561
10994
|
};
|
|
10562
|
-
const
|
|
10995
|
+
const connect3 = async () => {
|
|
10563
10996
|
let url;
|
|
10564
10997
|
try {
|
|
10565
10998
|
url = await socketUrl(
|
|
@@ -10622,13 +11055,13 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
10622
11055
|
watching = true;
|
|
10623
11056
|
const members = await request(config, "GET", "/organization/members").catch(() => []);
|
|
10624
11057
|
process.stdout.write(
|
|
10625
|
-
`\r\x1B[2K${dim7(watchNotice(ownerNameOf(detail?.session.ownerUserId, members)))}\r
|
|
11058
|
+
`\r\x1B[2K${dim7(redactCredentials(watchNotice(ownerNameOf(detail?.session.ownerUserId, members))))}\r
|
|
10626
11059
|
${dim7(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}\r
|
|
10627
11060
|
\r
|
|
10628
11061
|
`
|
|
10629
11062
|
);
|
|
10630
11063
|
}
|
|
10631
|
-
const connected = await
|
|
11064
|
+
const connected = await connect3();
|
|
10632
11065
|
stopConnecting();
|
|
10633
11066
|
if (connected === "unavailable") return "unavailable";
|
|
10634
11067
|
if (connected === "aborted") return abortReason ?? "interrupted";
|
|
@@ -10668,10 +11101,7 @@ ${dim7(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}
|
|
|
10668
11101
|
ws?.removeEventListener("message", onTtyFrame);
|
|
10669
11102
|
if (rawModeEnabled) process.stdin.setRawMode(false);
|
|
10670
11103
|
if (interactive && ws !== null && process.stdout.isTTY === true) {
|
|
10671
|
-
process.stdout.write(
|
|
10672
|
-
"\x1B[<u\x1B[=0;1u\x1B[?2004l\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1049l\x1B[?25h"
|
|
10673
|
-
// show the cursor
|
|
10674
|
-
);
|
|
11104
|
+
process.stdout.write(TERMINAL_MODES_RESET);
|
|
10675
11105
|
}
|
|
10676
11106
|
process.stdin.pause();
|
|
10677
11107
|
if (ws !== null && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING))
|
|
@@ -10679,6 +11109,7 @@ ${dim7(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}
|
|
|
10679
11109
|
stopHerdrHint();
|
|
10680
11110
|
}
|
|
10681
11111
|
};
|
|
11112
|
+
var TERMINAL_MODES_RESET = "\x1B[<u\x1B[=0;1u\x1B[?2004l\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1049l\x1B[?25h";
|
|
10682
11113
|
var stopSessionQuickly = async (config, sessionId) => {
|
|
10683
11114
|
const headers = { "content-type": "application/json" };
|
|
10684
11115
|
if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
|
|
@@ -11121,19 +11552,48 @@ var autoConnect = async (config, service) => {
|
|
|
11121
11552
|
say7(dim7(" remote server \u2014 tunneling the port here \xB7 Ctrl-C stops the tunnel, not the Service"));
|
|
11122
11553
|
await tunnelServices(config, [service], null);
|
|
11123
11554
|
};
|
|
11555
|
+
var SERVICE_WAIT_MS = (() => {
|
|
11556
|
+
const configured = Number(process.env["MEND_SERVICE_WAIT_MS"]);
|
|
11557
|
+
return Number.isFinite(configured) && configured > 0 ? configured : 9e4;
|
|
11558
|
+
})();
|
|
11559
|
+
var withinServiceWait = async (work, deadline) => {
|
|
11560
|
+
const bounded = await beforeDeadline(work, clock, deadline);
|
|
11561
|
+
if (bounded.done) return bounded.value;
|
|
11562
|
+
process.stderr.write(
|
|
11563
|
+
`mend: no answer within ${Math.round(SERVICE_WAIT_MS / 1e3)} s \xB7 the Service may still be starting \xB7 mend service list
|
|
11564
|
+
`
|
|
11565
|
+
);
|
|
11566
|
+
return exitFlushed(WAIT_TIMED_OUT);
|
|
11567
|
+
};
|
|
11568
|
+
var failUnlessAnswered = (config, service, wait) => {
|
|
11569
|
+
if (!wait) return;
|
|
11570
|
+
const name = service.label ?? service.id.slice(0, 8);
|
|
11571
|
+
if (service.status !== "reachable") {
|
|
11572
|
+
fail7(
|
|
11573
|
+
`nothing answered on :${service.workspacePort} \xB7 ${service.status} \xB7 the Service keeps running \xB7 mend service logs ${name}`
|
|
11574
|
+
);
|
|
11575
|
+
}
|
|
11576
|
+
if (willAutoConnect(config, service, false)) {
|
|
11577
|
+
say7(dim7(` connect: mend service connect ${name}`));
|
|
11578
|
+
}
|
|
11579
|
+
};
|
|
11124
11580
|
var serviceRun = async (config, args) => {
|
|
11125
11581
|
const dashdash = args.indexOf("--");
|
|
11126
11582
|
const usage2 = usageOf("service run");
|
|
11583
|
+
const wait = (dashdash === -1 ? args : args.slice(0, dashdash)).includes("--wait");
|
|
11584
|
+
const deadline = wait ? clock.now() + SERVICE_WAIT_MS : null;
|
|
11127
11585
|
if (dashdash === -1) {
|
|
11128
11586
|
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
11129
11587
|
const name2 = positionals.at(-1);
|
|
11130
11588
|
if (name2 === void 0) return fail7(usage2);
|
|
11131
11589
|
const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
|
|
11132
|
-
const session2 = await
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11590
|
+
const session2 = await withinServiceWait(
|
|
11591
|
+
resolveLiveSession(config, prefix2, "service run"),
|
|
11592
|
+
deadline
|
|
11593
|
+
);
|
|
11594
|
+
const recipes = await withinServiceWait(
|
|
11595
|
+
api(config, "GET", `/sessions/${session2.id}/recipes`),
|
|
11596
|
+
deadline
|
|
11137
11597
|
);
|
|
11138
11598
|
const recipe = recipes.find((entry) => entry.name === name2);
|
|
11139
11599
|
if (recipe === void 0) {
|
|
@@ -11142,16 +11602,28 @@ var serviceRun = async (config, args) => {
|
|
|
11142
11602
|
recipes.length === 0 ? `no mend.toml recipes in this worktree \u2014 declare [service.${name2}] first` : `no recipe named "${name2}" \u2014 declared: ${known}`
|
|
11143
11603
|
);
|
|
11144
11604
|
}
|
|
11605
|
+
if (wait && recipe.protocol === "udp") {
|
|
11606
|
+
return fail7(`--wait needs a TCP port: ${recipe.name} is UDP, which has no probe`);
|
|
11607
|
+
}
|
|
11608
|
+
if (wait && recipe.command === null) {
|
|
11609
|
+
return fail7(
|
|
11610
|
+
`--wait needs a command Mend starts: ${recipe.name} declares only a port, which Mend adopts with one probe \xB7 run it without --wait, or give the recipe a command`
|
|
11611
|
+
);
|
|
11612
|
+
}
|
|
11145
11613
|
const service2 = await withSpinner(
|
|
11146
11614
|
recipe.command === null ? `adopting ${recipe.name} on :${recipe.port}\u2026` : recipe.protocol === "udp" ? `starting ${recipe.name} (udp :${recipe.port})\u2026` : `starting ${recipe.name} \u2014 waiting for :${recipe.port} to answer\u2026`,
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11615
|
+
withinServiceWait(
|
|
11616
|
+
mutateService(config, "POST", `/sessions/${session2.id}/services/recipe`, {
|
|
11617
|
+
name: recipe.name
|
|
11618
|
+
}),
|
|
11619
|
+
deadline
|
|
11620
|
+
)
|
|
11150
11621
|
);
|
|
11151
|
-
const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect"));
|
|
11622
|
+
const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect") || wait);
|
|
11152
11623
|
say7(`${green7("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
|
|
11153
11624
|
printServiceEndpoint(config, service2, tunneling2);
|
|
11154
11625
|
say7(dim7(` logs: mend service logs ${service2.label ?? service2.id.slice(0, 8)}`));
|
|
11626
|
+
failUnlessAnswered(config, service2, wait);
|
|
11155
11627
|
if (tunneling2) await autoConnect(config, service2);
|
|
11156
11628
|
return;
|
|
11157
11629
|
}
|
|
@@ -11169,24 +11641,34 @@ var serviceRun = async (config, args) => {
|
|
|
11169
11641
|
if (http && https) return fail7(usage2);
|
|
11170
11642
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
11171
11643
|
if (protocol === "udp" && browserScheme !== null) return fail7(usage2);
|
|
11644
|
+
if (protocol === "udp" && wait) {
|
|
11645
|
+
return fail7("--wait needs a TCP port: UDP has no probe, so nothing could be waited for");
|
|
11646
|
+
}
|
|
11172
11647
|
const prefix = head.find(
|
|
11173
|
-
(a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
|
|
11648
|
+
(a, i) => !a.startsWith("--") && (portFlag === -1 || i !== portFlag + 1) && (nameFlag === -1 || i !== nameFlag + 1)
|
|
11649
|
+
);
|
|
11650
|
+
const session = await withinServiceWait(
|
|
11651
|
+
resolveLiveSession(config, prefix, "service run"),
|
|
11652
|
+
deadline
|
|
11174
11653
|
);
|
|
11175
|
-
const session = await resolveLiveSession(config, prefix, "service run");
|
|
11176
11654
|
const service = await withSpinner(
|
|
11177
11655
|
protocol === "udp" ? `starting ${name ?? argv[0]} (udp :${port})\u2026` : `starting ${name ?? argv[0]} \u2014 waiting for :${port} to answer\u2026`,
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11656
|
+
withinServiceWait(
|
|
11657
|
+
mutateService(config, "POST", `/sessions/${session.id}/services/run`, {
|
|
11658
|
+
argv,
|
|
11659
|
+
port,
|
|
11660
|
+
name,
|
|
11661
|
+
protocol,
|
|
11662
|
+
browserScheme
|
|
11663
|
+
}),
|
|
11664
|
+
deadline
|
|
11665
|
+
)
|
|
11185
11666
|
);
|
|
11186
|
-
const tunneling = willAutoConnect(config, service, head.includes("--no-connect"));
|
|
11667
|
+
const tunneling = willAutoConnect(config, service, head.includes("--no-connect") || wait);
|
|
11187
11668
|
say7(`${green7("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
|
|
11188
11669
|
printServiceEndpoint(config, service, tunneling);
|
|
11189
11670
|
say7(dim7(` logs: mend service logs ${service.label ?? service.id.slice(0, 8)}`));
|
|
11671
|
+
failUnlessAnswered(config, service, wait);
|
|
11190
11672
|
if (tunneling) await autoConnect(config, service);
|
|
11191
11673
|
};
|
|
11192
11674
|
var serviceLogs = async (config, args) => {
|
|
@@ -11308,7 +11790,7 @@ var serviceInit = async (args) => {
|
|
|
11308
11790
|
const toml = renderMendToml(proposals);
|
|
11309
11791
|
say7(dim7(`proposed ${target}:`));
|
|
11310
11792
|
say7("");
|
|
11311
|
-
process.stdout.write(toml);
|
|
11793
|
+
process.stdout.write(redactCredentials(toml));
|
|
11312
11794
|
say7("");
|
|
11313
11795
|
if (!args.includes("--yes")) {
|
|
11314
11796
|
if (process.stdin.isTTY !== true) {
|
|
@@ -11355,7 +11837,7 @@ var attachTunnels = (config, optOut) => {
|
|
|
11355
11837
|
return;
|
|
11356
11838
|
}
|
|
11357
11839
|
const rows = process.stdout.rows ?? 24;
|
|
11358
|
-
process.stdout.write(`\x1B7\x1B[${rows};1H\x1B[2K${line}\x1B8`);
|
|
11840
|
+
process.stdout.write(`\x1B7\x1B[${rows};1H\x1B[2K${redactCredentials(line)}\x1B8`);
|
|
11359
11841
|
};
|
|
11360
11842
|
const tunnels = createServiceTunnels({
|
|
11361
11843
|
listServices: () => fetchServices(config),
|
|
@@ -11634,7 +12116,7 @@ var accountsCommand = async (config) => {
|
|
|
11634
12116
|
} catch (error) {
|
|
11635
12117
|
return fail7(error instanceof Error ? error.message : String(error));
|
|
11636
12118
|
}
|
|
11637
|
-
for (const line of lines) process.stdout.write(`${line}
|
|
12119
|
+
for (const line of lines) process.stdout.write(`${redactCredentials(line)}
|
|
11638
12120
|
`);
|
|
11639
12121
|
};
|
|
11640
12122
|
var claudeGrantSecret = () => {
|
|
@@ -11645,7 +12127,7 @@ var doctorBundle = (config, args) => doctorBundleCommand(args, {
|
|
|
11645
12127
|
defaultDir: path21.join(mendCliHome(), "bundles"),
|
|
11646
12128
|
now: () => /* @__PURE__ */ new Date(),
|
|
11647
12129
|
say: say7,
|
|
11648
|
-
warn: (line) => process.stderr.write(`${line}
|
|
12130
|
+
warn: (line) => process.stderr.write(`${redactCredentials(line)}
|
|
11649
12131
|
`),
|
|
11650
12132
|
collectors: (tail) => bundleCollectors({
|
|
11651
12133
|
cliVersion: cliVersion(),
|
|
@@ -11833,7 +12315,7 @@ var connectCommand = async (config, args) => {
|
|
|
11833
12315
|
secret: narrowed.secret
|
|
11834
12316
|
})
|
|
11835
12317
|
);
|
|
11836
|
-
process.stdout.write(`${accountLine(account)}
|
|
12318
|
+
process.stdout.write(`${redactCredentials(accountLine(account))}
|
|
11837
12319
|
`);
|
|
11838
12320
|
const facts = claudeGrantFacts(narrowed.secret);
|
|
11839
12321
|
if (facts !== null) {
|
|
@@ -12443,6 +12925,7 @@ _mend() {
|
|
|
12443
12925
|
'adopt:adopt a repository into the store'
|
|
12444
12926
|
'codex:new session + codex' 'claude:new session + claude' 'opencode:new session + opencode' 'pi:new session + pi'
|
|
12445
12927
|
'run:new session + arbitrary command'
|
|
12928
|
+
'logs:recorded terminal output of a session' 'wait:wait for the command of a session to end'
|
|
12446
12929
|
'attach:reattach to a running session' 'stop:stop the agent \u2014 record and review remain'
|
|
12447
12930
|
'shell:open a shell in a live session workspace'
|
|
12448
12931
|
'service:reachable ports \u2014 add, list, stop'
|
|
@@ -12479,7 +12962,7 @@ _mend() {
|
|
|
12479
12962
|
esac
|
|
12480
12963
|
fi
|
|
12481
12964
|
;;
|
|
12482
|
-
shell|attach|stop|continue|resume|rejoin|land|pull)
|
|
12965
|
+
shell|attach|stop|continue|resume|rejoin|land|pull|logs|wait)
|
|
12483
12966
|
local -a sessions
|
|
12484
12967
|
sessions=(\${(f)"$(command mend __complete session 2>/dev/null | tr '\\t' ':')"})
|
|
12485
12968
|
(( \${#sessions} )) && _describe 'session' sessions
|
|
@@ -12491,7 +12974,7 @@ _mend "$@"
|
|
|
12491
12974
|
var BASH_COMPLETIONS = `_mend() {
|
|
12492
12975
|
local cur=\${COMP_WORDS[COMP_CWORD]}
|
|
12493
12976
|
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
12494
|
-
COMPREPLY=( $(compgen -W "adopt codex claude opencode pi run attach stop shell service server uninstall keys git-author skills memory connect pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
|
|
12977
|
+
COMPREPLY=( $(compgen -W "adopt codex claude opencode pi run logs wait attach stop shell service server uninstall keys git-author skills memory connect pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
|
|
12495
12978
|
return
|
|
12496
12979
|
fi
|
|
12497
12980
|
case \${COMP_WORDS[1]} in
|
|
@@ -12509,7 +12992,7 @@ var BASH_COMPLETIONS = `_mend() {
|
|
|
12509
12992
|
fi
|
|
12510
12993
|
COMPREPLY=( $(compgen -W "$options" -- "$cur") )
|
|
12511
12994
|
;;
|
|
12512
|
-
shell|attach|stop|continue|resume|rejoin|land|pull)
|
|
12995
|
+
shell|attach|stop|continue|resume|rejoin|land|pull|logs|wait)
|
|
12513
12996
|
COMPREPLY=( $(compgen -W "$(command mend __complete session 2>/dev/null | cut -f1)" -- "$cur") )
|
|
12514
12997
|
;;
|
|
12515
12998
|
esac
|
|
@@ -12528,57 +13011,292 @@ var completionsCommand = (args) => {
|
|
|
12528
13011
|
return fail7(`${usageOf("completions")} \xB7 e.g. mend completions zsh > "$fpath[1]/_mend"`);
|
|
12529
13012
|
}
|
|
12530
13013
|
};
|
|
12531
|
-
var
|
|
12532
|
-
const
|
|
13014
|
+
var pause = (ms, signal) => {
|
|
13015
|
+
const { promise, resolve: resolve8 } = Promise.withResolvers();
|
|
13016
|
+
const timer = setTimeout(resolve8, ms);
|
|
13017
|
+
signal?.addEventListener(
|
|
13018
|
+
"abort",
|
|
13019
|
+
() => {
|
|
13020
|
+
clearTimeout(timer);
|
|
13021
|
+
resolve8();
|
|
13022
|
+
},
|
|
13023
|
+
{ once: true }
|
|
13024
|
+
);
|
|
13025
|
+
return promise;
|
|
13026
|
+
};
|
|
13027
|
+
var clock = { sleep: pause, now: Date.now };
|
|
13028
|
+
var printJson = (value) => {
|
|
13029
|
+
process.stdout.write(`${redactCredentials(JSON.stringify(value, null, 2))}
|
|
13030
|
+
`);
|
|
13031
|
+
};
|
|
13032
|
+
var outputOnTerminal = false;
|
|
13033
|
+
var stdoutErrorsHandled = false;
|
|
13034
|
+
var outputStopped = false;
|
|
13035
|
+
var restoreTerminal = () => {
|
|
13036
|
+
if (!outputOnTerminal || process.stdout.isTTY !== true) return;
|
|
13037
|
+
outputOnTerminal = false;
|
|
13038
|
+
process.stdout.write(`${TERMINAL_MODES_RESET}\x1B[0m`);
|
|
13039
|
+
};
|
|
13040
|
+
process.once("exit", restoreTerminal);
|
|
13041
|
+
var writeOutput = (bytes) => {
|
|
13042
|
+
if (outputStopped) return Promise.reject(new Error("stopped by a signal"));
|
|
13043
|
+
if (process.stdout.isTTY === true) outputOnTerminal = true;
|
|
13044
|
+
if (!stdoutErrorsHandled) {
|
|
13045
|
+
stdoutErrorsHandled = true;
|
|
13046
|
+
process.stdout.on("error", () => void 0);
|
|
13047
|
+
}
|
|
13048
|
+
return new Promise((resolve8, reject) => {
|
|
13049
|
+
process.stdout.write(bytes, (error) => {
|
|
13050
|
+
if (error === null || error === void 0) resolve8();
|
|
13051
|
+
else reject(error);
|
|
13052
|
+
});
|
|
13053
|
+
});
|
|
13054
|
+
};
|
|
13055
|
+
var flushed = (stream) => new Promise((resolve8) => {
|
|
13056
|
+
if (stream.destroyed || !stream.writable) resolve8();
|
|
13057
|
+
else stream.write("", () => resolve8());
|
|
13058
|
+
});
|
|
13059
|
+
var EXIT_FLUSH_GRACE_MS = 5e3;
|
|
13060
|
+
var exitFlushed = async (code) => {
|
|
13061
|
+
restoreTerminal();
|
|
13062
|
+
const flush = await beforeDeadline(
|
|
13063
|
+
Promise.all([flushed(process.stdout), flushed(process.stderr)]),
|
|
13064
|
+
clock,
|
|
13065
|
+
clock.now() + EXIT_FLUSH_GRACE_MS
|
|
13066
|
+
);
|
|
13067
|
+
if (!flush.done) {
|
|
13068
|
+
process.stderr.write(
|
|
13069
|
+
`mend: output may be incomplete \xB7 the reader took none of it for ${EXIT_FLUSH_GRACE_MS / 1e3} s
|
|
13070
|
+
`
|
|
13071
|
+
);
|
|
13072
|
+
process.exit(code === 0 ? 1 : code);
|
|
13073
|
+
}
|
|
13074
|
+
process.exit(code);
|
|
13075
|
+
};
|
|
13076
|
+
var failFlushed = (message) => {
|
|
13077
|
+
process.stderr.write(`mend: ${redactCredentials(message)}
|
|
13078
|
+
`);
|
|
13079
|
+
return exitFlushed(1);
|
|
13080
|
+
};
|
|
13081
|
+
var stopOutputOnSignal = (line) => {
|
|
13082
|
+
const signals = [
|
|
13083
|
+
["SIGHUP", 129],
|
|
13084
|
+
["SIGINT", 130],
|
|
13085
|
+
["SIGTERM", 143]
|
|
13086
|
+
];
|
|
13087
|
+
const handlers = signals.map(([signal, code]) => {
|
|
13088
|
+
const handler = () => {
|
|
13089
|
+
if (outputStopped) {
|
|
13090
|
+
restoreTerminal();
|
|
13091
|
+
process.exit(code);
|
|
13092
|
+
}
|
|
13093
|
+
outputStopped = true;
|
|
13094
|
+
say7("");
|
|
13095
|
+
say7(`${amber2("\xB7")} ${line}`);
|
|
13096
|
+
void exitFlushed(code);
|
|
13097
|
+
};
|
|
13098
|
+
process.on(signal, handler);
|
|
13099
|
+
return () => process.off(signal, handler);
|
|
13100
|
+
});
|
|
13101
|
+
return () => {
|
|
13102
|
+
for (const undo of handlers) undo();
|
|
13103
|
+
};
|
|
13104
|
+
};
|
|
13105
|
+
var readLogPage = (config, processId, from) => request(
|
|
13106
|
+
config,
|
|
13107
|
+
"GET",
|
|
13108
|
+
`/processes/${processId}/logs?from=${encodeURIComponent(from)}&limit=256`
|
|
13109
|
+
);
|
|
13110
|
+
var writeProcessLogs = (config, processId, from, follow) => followLogs({
|
|
13111
|
+
from,
|
|
13112
|
+
follow,
|
|
13113
|
+
read: (cursor) => readLogPage(config, processId, cursor),
|
|
13114
|
+
write: writeOutput,
|
|
13115
|
+
...clock
|
|
13116
|
+
});
|
|
13117
|
+
var waitForSessionCommand = (config, sessionId, processId, deadline) => waitForCommand({
|
|
13118
|
+
read: () => request(config, "GET", `/sessions/${sessionId}`),
|
|
13119
|
+
processId,
|
|
13120
|
+
deadline,
|
|
13121
|
+
...clock
|
|
13122
|
+
});
|
|
13123
|
+
var runJsonOf = (config, session, processId, state) => ({
|
|
13124
|
+
version: 1,
|
|
13125
|
+
sessionId: session.id,
|
|
13126
|
+
processId,
|
|
13127
|
+
worktree: session.worktree,
|
|
13128
|
+
branch: session.branch,
|
|
13129
|
+
url: `${config.url}/sessions/${session.id}`,
|
|
13130
|
+
status: state.status,
|
|
13131
|
+
exitCode: state.exitCode
|
|
13132
|
+
});
|
|
13133
|
+
var observedState = (detail, processId, fallback) => {
|
|
13134
|
+
if (detail === null) return { status: fallback, exitCode: null };
|
|
13135
|
+
const row = (processId === null ? void 0 : processRowOf(detail, processId)) ?? detail.currentAgent;
|
|
13136
|
+
return row === null ? { status: detail.session.status, exitCode: null } : { status: row.status, exitCode: row.exitCode };
|
|
13137
|
+
};
|
|
13138
|
+
var exitWithCommandEnd = (config, session, end, json2, undelivered = null) => {
|
|
13139
|
+
const commandCode = exitStatusOf(end);
|
|
13140
|
+
const code = undelivered !== null && commandCode === 0 ? 1 : commandCode;
|
|
13141
|
+
say7(`${commandCode === 0 ? green7("\u2713") : amber2("\xB7")} ${endLine(end)} \xB7 recorded`);
|
|
13142
|
+
if (undelivered !== null) {
|
|
13143
|
+
say7(
|
|
13144
|
+
`${amber2("\xB7")} output not delivered \xB7 ${undelivered} \xB7 read it with mend logs ${session.id.slice(0, 8)}`
|
|
13145
|
+
);
|
|
13146
|
+
}
|
|
13147
|
+
say7(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
|
|
13148
|
+
if (json2) printJson(runJsonOf(config, session, end.processId, end));
|
|
13149
|
+
return exitFlushed(code);
|
|
13150
|
+
};
|
|
13151
|
+
var supervisedRun = async (config, session, argv, options) => {
|
|
13152
|
+
const outcome = await followStarting(
|
|
12533
13153
|
config,
|
|
12534
13154
|
session.id,
|
|
12535
13155
|
"provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
|
|
12536
13156
|
request(config, "POST", `/sessions/${session.id}/launch`, { argv })
|
|
12537
13157
|
);
|
|
13158
|
+
const ranAndEnded = outcome.kind === "settled" ? await request(config, "GET", `/sessions/${session.id}`).catch(() => null) : null;
|
|
13159
|
+
if (outcome.kind !== "live" && ranAndEnded?.currentAgent?.id === void 0) {
|
|
13160
|
+
startedOrExit(config, session.id, outcome);
|
|
13161
|
+
return;
|
|
13162
|
+
}
|
|
13163
|
+
const detail = ranAndEnded ?? await api(config, "GET", `/sessions/${session.id}`);
|
|
13164
|
+
const processId = detail.currentAgent?.id ?? null;
|
|
13165
|
+
const id8 = session.id.slice(0, 8);
|
|
13166
|
+
if (options.detach) {
|
|
13167
|
+
const ended = commandEndOf(detail, processId);
|
|
13168
|
+
if (ended === null) {
|
|
13169
|
+
say7(`${green7("\u2713 recording")} \xB7 running detached \xB7 session ${dim7(id8)}`);
|
|
13170
|
+
say7(`${cobalt2(" output")} \xB7 mend logs ${id8} --follow`);
|
|
13171
|
+
say7(`${cobalt2(" wait")} \xB7 mend wait ${id8}`);
|
|
13172
|
+
} else {
|
|
13173
|
+
say7(`${amber2("\xB7")} ended before detaching \xB7 ${endLine(ended)} \xB7 session ${dim7(id8)}`);
|
|
13174
|
+
say7(`${cobalt2(" output")} \xB7 mend logs ${id8}`);
|
|
13175
|
+
}
|
|
13176
|
+
if (options.json) {
|
|
13177
|
+
printJson(
|
|
13178
|
+
runJsonOf(
|
|
13179
|
+
config,
|
|
13180
|
+
session,
|
|
13181
|
+
processId,
|
|
13182
|
+
ended ?? observedState(detail, processId, detail.session.status)
|
|
13183
|
+
)
|
|
13184
|
+
);
|
|
13185
|
+
}
|
|
13186
|
+
return;
|
|
13187
|
+
}
|
|
12538
13188
|
say7(
|
|
12539
|
-
`${green7("\u2713 recording")} \xB7
|
|
13189
|
+
`${green7("\u2713 recording")} \xB7 ${argv[0] ?? "command"} \xB7 workspace mounts the worktree \xB7 Ctrl+C stops watching, not the command`
|
|
12540
13190
|
);
|
|
12541
|
-
say7(dim7(` ${argv.join(" ")} \u2014 live record:`));
|
|
12542
13191
|
say7("");
|
|
12543
|
-
|
|
12544
|
-
if (
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
const { done, value } = await reader.read();
|
|
12553
|
-
if (done) break;
|
|
12554
|
-
buffer += decoder.decode(value, { stream: true });
|
|
12555
|
-
const parts = buffer.split("\n\n");
|
|
12556
|
-
buffer = parts.pop() ?? "";
|
|
12557
|
-
for (const part of parts) {
|
|
12558
|
-
const data = part.split("\n").filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).join("");
|
|
12559
|
-
if (data === "") continue;
|
|
12560
|
-
let event;
|
|
12561
|
-
try {
|
|
12562
|
-
event = JSON.parse(data);
|
|
12563
|
-
} catch {
|
|
12564
|
-
continue;
|
|
12565
|
-
}
|
|
12566
|
-
if (event.sessionId !== session.id) continue;
|
|
12567
|
-
if (event.type === "session-progress" && event.line !== void 0) {
|
|
12568
|
-
say7(dim7(` ${event.line}`));
|
|
12569
|
-
}
|
|
12570
|
-
if (event.type === "session") {
|
|
12571
|
-
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
12572
|
-
const ended = agentOutcome(detail.currentAgent) ?? (["completed", "failed", "stopped"].includes(detail.session.status) ? detail.session.status : null);
|
|
12573
|
-
if (ended !== null) settled = ended;
|
|
12574
|
-
}
|
|
13192
|
+
let undelivered = null;
|
|
13193
|
+
if (!options.json) {
|
|
13194
|
+
stopOutputOnSignal(`stopped watching \xB7 the command keeps running \xB7 mend logs ${id8} --follow`);
|
|
13195
|
+
}
|
|
13196
|
+
if (processId !== null && !options.json) {
|
|
13197
|
+
try {
|
|
13198
|
+
await writeProcessLogs(config, processId, "0", true);
|
|
13199
|
+
} catch (error) {
|
|
13200
|
+
undelivered = error instanceof Error ? error.message : String(error);
|
|
12575
13201
|
}
|
|
12576
13202
|
}
|
|
12577
|
-
await
|
|
13203
|
+
const waited = await waitForSessionCommand(config, session.id, processId, null).catch(
|
|
13204
|
+
(error) => failFlushed(error instanceof Error ? error.message : String(error))
|
|
13205
|
+
);
|
|
13206
|
+
if (waited.kind === "timeout") return failFlushed("the wait ended without a timeout");
|
|
12578
13207
|
say7("");
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
13208
|
+
await exitWithCommandEnd(config, session, waited.end, options.json, undelivered);
|
|
13209
|
+
};
|
|
13210
|
+
var resolveAnySession = async (config, word) => {
|
|
13211
|
+
if (isSessionId(word)) {
|
|
13212
|
+
return (await api(config, "GET", `/sessions/${word}`)).session;
|
|
13213
|
+
}
|
|
13214
|
+
const projects = await api(config, "GET", "/projects");
|
|
13215
|
+
const details = await Promise.all(
|
|
13216
|
+
projects.map(
|
|
13217
|
+
(project2) => api(config, "GET", `/projects/${project2.id}?deadEnds=include`)
|
|
13218
|
+
)
|
|
13219
|
+
);
|
|
13220
|
+
const picked = pickSession(
|
|
13221
|
+
details.flatMap((detail) => detail.sessions),
|
|
13222
|
+
word
|
|
13223
|
+
);
|
|
13224
|
+
if ("error" in picked) return fail7(picked.error);
|
|
13225
|
+
return picked.session;
|
|
13226
|
+
};
|
|
13227
|
+
var logsCommand = async (config, args) => {
|
|
13228
|
+
chromeToStderr();
|
|
13229
|
+
const parsed = parseLogsArgs(args);
|
|
13230
|
+
if ("error" in parsed) return fail7(`${parsed.error} \xB7 ${usageOf("logs")}`);
|
|
13231
|
+
const { session: word, follow, from, process: processPrefix } = parsed.args;
|
|
13232
|
+
const session = word === null ? await resolveLiveSession(config, void 0, "logs") : await resolveAnySession(config, word);
|
|
13233
|
+
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
13234
|
+
const picked = pickProcess(detail, processPrefix);
|
|
13235
|
+
if ("error" in picked) return fail7(`session ${session.id.slice(0, 8)} \xB7 ${picked.error}`);
|
|
13236
|
+
const target = picked.process;
|
|
13237
|
+
if (target.id === void 0 || target.sealantSessionId === null) {
|
|
13238
|
+
return fail7(
|
|
13239
|
+
`session ${session.id.slice(0, 8)} \xB7 this process has no recorded terminal (an adopted port, or a server older than process ids)`
|
|
13240
|
+
);
|
|
13241
|
+
}
|
|
13242
|
+
stopOutputOnSignal(
|
|
13243
|
+
follow ? `stopped watching \xB7 the process keeps running \xB7 mend logs ${session.id.slice(0, 8)} --follow` : "stopped reading the record"
|
|
13244
|
+
);
|
|
13245
|
+
try {
|
|
13246
|
+
const last = await writeProcessLogs(config, target.id, from, follow);
|
|
13247
|
+
restoreTerminal();
|
|
13248
|
+
if (follow) say7(dim7(`
|
|
13249
|
+
record ended \xB7 ${last.status} \xB7 next sequence ${last.next}`));
|
|
13250
|
+
} catch (error) {
|
|
13251
|
+
return failFlushed(
|
|
13252
|
+
`output not delivered \xB7 ${error instanceof Error ? error.message : String(error)}`
|
|
13253
|
+
);
|
|
13254
|
+
}
|
|
13255
|
+
await exitFlushed(0);
|
|
13256
|
+
};
|
|
13257
|
+
var waitCommand = async (config, args) => {
|
|
13258
|
+
chromeToStderr();
|
|
13259
|
+
const parsed = parseWaitArgs(args);
|
|
13260
|
+
if ("error" in parsed) return fail7(`${parsed.error} \xB7 ${usageOf("wait")}`);
|
|
13261
|
+
const { session: word, timeoutMs, json: json2, process: processPrefix } = parsed.args;
|
|
13262
|
+
const deadline = timeoutMs === null ? null : clock.now() + timeoutMs;
|
|
13263
|
+
const after = `${Math.round((timeoutMs ?? 0) / 1e3)} s`;
|
|
13264
|
+
const timedOut = async (session2, processId2, last) => {
|
|
13265
|
+
say7(
|
|
13266
|
+
session2 === null ? `${amber2("\xB7")} no session found within ${after}` : `${amber2("\xB7")} session ${session2.id.slice(0, 8)} \xB7 still running after ${after} \xB7 the command keeps running`
|
|
13267
|
+
);
|
|
13268
|
+
if (json2 && session2 !== null) {
|
|
13269
|
+
const observed = processId2 ?? last?.currentAgent?.id ?? null;
|
|
13270
|
+
printJson(
|
|
13271
|
+
runJsonOf(config, session2, observed, observedState(last, observed, session2.status))
|
|
13272
|
+
);
|
|
13273
|
+
}
|
|
13274
|
+
return exitFlushed(WAIT_TIMED_OUT);
|
|
13275
|
+
};
|
|
13276
|
+
const resolved = await beforeDeadline(
|
|
13277
|
+
word === null ? resolveLiveSession(config, void 0, "wait") : resolveAnySession(config, word),
|
|
13278
|
+
clock,
|
|
13279
|
+
deadline
|
|
13280
|
+
);
|
|
13281
|
+
if (!resolved.done) return timedOut(null, null, null);
|
|
13282
|
+
const session = resolved.value;
|
|
13283
|
+
let processId = null;
|
|
13284
|
+
if (processPrefix !== null) {
|
|
13285
|
+
const read = await beforeDeadline(
|
|
13286
|
+
api(config, "GET", `/sessions/${session.id}`),
|
|
13287
|
+
clock,
|
|
13288
|
+
deadline
|
|
13289
|
+
);
|
|
13290
|
+
if (!read.done) return timedOut(session, null, null);
|
|
13291
|
+
const picked = pickProcess(read.value, processPrefix);
|
|
13292
|
+
if ("error" in picked) return fail7(`session ${session.id.slice(0, 8)} \xB7 ${picked.error}`);
|
|
13293
|
+
processId = picked.process.id ?? null;
|
|
13294
|
+
}
|
|
13295
|
+
const waited = await waitForSessionCommand(config, session.id, processId, deadline).catch(
|
|
13296
|
+
(error) => failFlushed(error instanceof Error ? error.message : String(error))
|
|
13297
|
+
);
|
|
13298
|
+
if (waited.kind === "timeout") return timedOut(session, processId, waited.last);
|
|
13299
|
+
await exitWithCommandEnd(config, session, waited.end, json2);
|
|
12582
13300
|
};
|
|
12583
13301
|
var continueSession = async (config, args) => {
|
|
12584
13302
|
const explicitSession = args.find((a) => !a.startsWith("--")) ?? null;
|
|
@@ -12769,22 +13487,39 @@ var rejoinCommand = async (config, args) => {
|
|
|
12769
13487
|
await finishAttach(config, session.id, outcome);
|
|
12770
13488
|
exitAfterSessionEnd(config, session.id);
|
|
12771
13489
|
};
|
|
12772
|
-
var projectsCommand = async (config) => {
|
|
13490
|
+
var projectsCommand = async (config, args) => {
|
|
13491
|
+
const unknown = args.find((arg) => arg !== "--json");
|
|
13492
|
+
if (unknown !== void 0) return fail7(`unknown argument ${unknown} \xB7 ${usageOf("projects")}`);
|
|
12773
13493
|
const [projects, active] = await Promise.all([
|
|
12774
13494
|
api(config, "GET", "/projects"),
|
|
12775
13495
|
api(config, "GET", "/sessions")
|
|
12776
13496
|
]);
|
|
12777
|
-
if (projects.length === 0) {
|
|
12778
|
-
say7(dim7("no adopted projects \u2014 mend adopt brings one in"));
|
|
12779
|
-
return;
|
|
12780
|
-
}
|
|
12781
13497
|
const liveByProject = /* @__PURE__ */ new Map();
|
|
12782
13498
|
for (const session of active) {
|
|
12783
13499
|
liveByProject.set(session.projectId, (liveByProject.get(session.projectId) ?? 0) + 1);
|
|
12784
13500
|
}
|
|
13501
|
+
const here = matchProjectByCwd(projects, cwdFacts(process.cwd()));
|
|
13502
|
+
if (args.includes("--json")) {
|
|
13503
|
+
printJson({
|
|
13504
|
+
version: 1,
|
|
13505
|
+
projects: projects.map((project2) => ({
|
|
13506
|
+
id: project2.id,
|
|
13507
|
+
name: project2.name,
|
|
13508
|
+
originUrl: project2.originUrl,
|
|
13509
|
+
defaultBranch: project2.defaultBranch,
|
|
13510
|
+
storePath: project2.storePath,
|
|
13511
|
+
liveSessions: liveByProject.get(project2.id) ?? 0,
|
|
13512
|
+
current: project2.id === here?.id
|
|
13513
|
+
}))
|
|
13514
|
+
});
|
|
13515
|
+
return;
|
|
13516
|
+
}
|
|
13517
|
+
if (projects.length === 0) {
|
|
13518
|
+
say7(dim7("no adopted projects \u2014 mend adopt brings one in"));
|
|
13519
|
+
return;
|
|
13520
|
+
}
|
|
12785
13521
|
const nameWidth = Math.max(...projects.map((p) => p.name.length));
|
|
12786
13522
|
const branchWidth = Math.max(...projects.map((p) => p.defaultBranch.length));
|
|
12787
|
-
const here = matchProjectByCwd(projects, cwdFacts(process.cwd()));
|
|
12788
13523
|
for (const project2 of projects) {
|
|
12789
13524
|
const live = liveByProject.get(project2.id) ?? 0;
|
|
12790
13525
|
const liveLabel = live > 0 ? green7(`${live} live`) : dim7("\u2014");
|
|
@@ -13087,7 +13822,7 @@ var dashboard = async (config, options = {}) => {
|
|
|
13087
13822
|
);
|
|
13088
13823
|
process.exit(rerun.status ?? 0);
|
|
13089
13824
|
}
|
|
13090
|
-
const { runDashboard } = await import("./dashboard-
|
|
13825
|
+
const { runDashboard } = await import("./dashboard-DQB5RNXJ.js");
|
|
13091
13826
|
const agentShare = await startShareIfBridge(config);
|
|
13092
13827
|
const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
|
|
13093
13828
|
listServices: () => fetchServices(config),
|
|
@@ -13191,6 +13926,10 @@ var main = async () => {
|
|
|
13191
13926
|
case "pi":
|
|
13192
13927
|
case "run":
|
|
13193
13928
|
return withAgentShare(config, () => launch(config, command, rest));
|
|
13929
|
+
case "logs":
|
|
13930
|
+
return logsCommand(config, rest);
|
|
13931
|
+
case "wait":
|
|
13932
|
+
return waitCommand(config, rest);
|
|
13194
13933
|
case "attach":
|
|
13195
13934
|
return withAgentShare(config, () => attach(config, rest));
|
|
13196
13935
|
case "stop":
|
|
@@ -13266,7 +14005,7 @@ var main = async () => {
|
|
|
13266
14005
|
case "rejoin":
|
|
13267
14006
|
return withAgentShare(config, () => rejoinCommand(config, rest));
|
|
13268
14007
|
case "projects":
|
|
13269
|
-
return projectsCommand(config);
|
|
14008
|
+
return projectsCommand(config, rest);
|
|
13270
14009
|
case "refresh":
|
|
13271
14010
|
return refreshCommand(config, rest);
|
|
13272
14011
|
case "land":
|