@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.
Files changed (92) hide show
  1. package/dist/{chunk-GKRTZRV7.js → chunk-K5WNRH4V.js} +32 -0
  2. package/dist/{dashboard-IBHSVQ5S.js → dashboard-DQB5RNXJ.js} +1 -1
  3. package/dist/main.js +878 -139
  4. package/man/mend-accounts.1 +1 -1
  5. package/man/mend-adopt.1 +1 -1
  6. package/man/mend-attach.1 +1 -1
  7. package/man/mend-codex.1 +1 -1
  8. package/man/mend-completions.1 +1 -1
  9. package/man/mend-connect-pi.1 +1 -1
  10. package/man/mend-connect.1 +1 -1
  11. package/man/mend-continue.1 +1 -1
  12. package/man/mend-doctor.1 +1 -1
  13. package/man/mend-dotfiles-repo.1 +1 -1
  14. package/man/mend-dotfiles-sync.1 +1 -1
  15. package/man/mend-dotfiles.1 +1 -1
  16. package/man/mend-env-cluster.1 +1 -1
  17. package/man/mend-env-load.1 +1 -1
  18. package/man/mend-env-show.1 +1 -1
  19. package/man/mend-folder-create.1 +1 -1
  20. package/man/mend-folder-list.1 +1 -1
  21. package/man/mend-folder-push.1 +1 -1
  22. package/man/mend-folder-rm.1 +1 -1
  23. package/man/mend-git-author.1 +1 -1
  24. package/man/mend-help.1 +1 -1
  25. package/man/mend-invite.1 +1 -1
  26. package/man/mend-keys-init.1 +1 -1
  27. package/man/mend-keys-mode.1 +1 -1
  28. package/man/mend-keys-share.1 +1 -1
  29. package/man/mend-keys-show.1 +1 -1
  30. package/man/mend-land.1 +1 -1
  31. package/man/mend-login.1 +1 -1
  32. package/man/mend-logout.1 +1 -1
  33. package/man/mend-logs.1 +26 -0
  34. package/man/mend-man.1 +1 -1
  35. package/man/mend-members.1 +1 -1
  36. package/man/mend-memory-import.1 +1 -1
  37. package/man/mend-memory-rm.1 +1 -1
  38. package/man/mend-memory-show.1 +1 -1
  39. package/man/mend-memory.1 +1 -1
  40. package/man/mend-models.1 +1 -1
  41. package/man/mend-operator-exposure.1 +2 -2
  42. package/man/mend-operator-gate.1 +1 -1
  43. package/man/mend-operator-grant-owner.1 +1 -1
  44. package/man/mend-operator-org-create.1 +1 -1
  45. package/man/mend-operator-org-invite-owner.1 +1 -1
  46. package/man/mend-operator-org-list.1 +1 -1
  47. package/man/mend-operator-org-rename.1 +1 -1
  48. package/man/mend-operator-reset-link.1 +1 -1
  49. package/man/mend-pair.1 +1 -1
  50. package/man/mend-projects.1 +10 -2
  51. package/man/mend-pull.1 +1 -1
  52. package/man/mend-refresh.1 +1 -1
  53. package/man/mend-rejoin.1 +1 -1
  54. package/man/mend-resume.1 +1 -1
  55. package/man/mend-run.1 +35 -4
  56. package/man/mend-secrets-add.1 +1 -1
  57. package/man/mend-secrets-rm.1 +1 -1
  58. package/man/mend-secrets.1 +1 -1
  59. package/man/mend-server-logs.1 +1 -1
  60. package/man/mend-server-restart.1 +1 -1
  61. package/man/mend-server-setup.1 +12 -2
  62. package/man/mend-server-start.1 +1 -1
  63. package/man/mend-server-status.1 +1 -1
  64. package/man/mend-server-stop.1 +1 -1
  65. package/man/mend-server-upgrade.1 +1 -1
  66. package/man/mend-server.1 +1 -1
  67. package/man/mend-service-add.1 +1 -1
  68. package/man/mend-service-connect.1 +1 -1
  69. package/man/mend-service-init.1 +1 -1
  70. package/man/mend-service-list.1 +1 -1
  71. package/man/mend-service-logs.1 +3 -1
  72. package/man/mend-service-restart.1 +1 -1
  73. package/man/mend-service-run.1 +8 -3
  74. package/man/mend-service-stop.1 +1 -1
  75. package/man/mend-session-share.1 +1 -1
  76. package/man/mend-sessions.1 +1 -1
  77. package/man/mend-shell.1 +1 -1
  78. package/man/mend-skills-push.1 +1 -1
  79. package/man/mend-skills.1 +1 -1
  80. package/man/mend-snake.1 +1 -1
  81. package/man/mend-ssh-setup.1 +1 -1
  82. package/man/mend-ssh.1 +1 -1
  83. package/man/mend-stop.1 +1 -1
  84. package/man/mend-ui.1 +1 -1
  85. package/man/mend-uninstall.1 +1 -1
  86. package/man/mend-version.1 +1 -1
  87. package/man/mend-wait.1 +28 -0
  88. package/man/mend-workspace-replace.1 +1 -1
  89. package/man/mend-worktrees-rm.1 +1 -1
  90. package/man/mend-worktrees.1 +1 -1
  91. package/man/mend.1 +8 -2
  92. 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-GKRTZRV7.js";
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: ["[--project <p>] -- <command...>"],
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: [project()],
1970
- examples: [{ command: "mend run -- aider --model gpt-5", text: "" }],
1971
- see: ["codex"]
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: ["One line per project, with the sessions running in it."],
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: ["Replays the recorded output, then follows it live. Ctrl-C stops following."],
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 is open. Items it cannot observe say what would verify them; once you have verified core-private or edge-tls from outside, name it in MEND_EXPOSURE_DECLARED. The report is what was observed; it is not a statement that the instance is fit to expose."
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) process.stdout.write(`${formatCheck(check, paintMark)}
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 = (connect2) => new Agent({ allowH2: false, ...connect2 === void 0 ? {} : { connect: connect2 } });
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 unobservable = open.length - blocking;
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} this build can observe${blocking === 0 ? "" : "; MEND_EXPOSURE=public refuses to start"} \xB7 ${unobservable} it cannot`
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
- const oldLocalUrl = existing === null ? null : `http://localhost:${existing.appPort}`;
6656
- const portChangedOnLocalhost = options.appPort !== void 0 && existing?.appUrl === oldLocalUrl;
6657
- const fallback = portChangedOnLocalhost || existing?.edgeHost !== void 0 ? `http://localhost:${appPort}` : existing?.appUrl ?? `http://localhost:${appPort}`;
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
- checkPosture(bind, { appPort, sshPort }, edgeHost, exposure);
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 = net3.isIP(config.bind) === 6 ? `[${config.bind}]` : config.bind;
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 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.`;
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 paint6 = (code) => (text) => process.stdout.isTTY ? `\x1B[${code}m${text}\x1B[0m` : text;
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) => process.stdout.write(`${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 (process.stdout.isTTY !== true) return work;
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
- process.stdout.write(
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
- process.stdout.write("\r\x1B[2K");
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 (process.stdout.isTTY !== true) say7(dim7(` ${next}`));
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" && (parsed.detach || parsed.foreground)) {
10746
+ if (harness === "run" && parsed.foreground) {
10316
10747
  return fail7(
10317
- "mend run takes no lifecycle flags \u2014 it tails the record; Ctrl+C stops watching, not the command"
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 connect2 = async () => {
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 connect2();
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 resolveLiveSession(config, prefix2, "service run");
11133
- const recipes = await api(
11134
- config,
11135
- "GET",
11136
- `/sessions/${session2.id}/recipes`
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
- mutateService(config, "POST", `/sessions/${session2.id}/services/recipe`, {
11148
- name: recipe.name
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
- mutateService(config, "POST", `/sessions/${session.id}/services/run`, {
11179
- argv,
11180
- port,
11181
- name,
11182
- protocol,
11183
- browserScheme
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 supervisedRun = async (config, session, argv) => {
12532
- const launched = await startAndFollow(
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 run ${dim7(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
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
- const headers = {};
12544
- if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
12545
- const response = await fetch(`${config.url}/api/events`, { headers });
12546
- if (response.body === null) return fail7("event stream unavailable");
12547
- const reader = response.body.getReader();
12548
- const decoder = new TextDecoder();
12549
- let buffer = "";
12550
- let settled = null;
12551
- while (settled === null) {
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 reader.cancel().catch(() => void 0);
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
- say7(`${green7("\u2713")} session ${settled ?? "settled"} \xB7 recorded \xB7 checkpoint taken`);
12580
- say7(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
12581
- process.exit(settled === "failed" ? 1 : 0);
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-IBHSVQ5S.js");
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":