@sealant/mend 0.36.0-next.652 → 0.36.0-next.663

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-CXW54BC3.js → chunk-VOP6USOZ.js} +103 -77
  2. package/dist/{dashboard-KYXY2JIF.js → dashboard-CEJN6IOO.js} +1 -1
  3. package/dist/main.js +707 -116
  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 +1 -1
  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 +1 -1
  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-CXW54BC3.js";
71
+ } from "./chunk-VOP6USOZ.js";
72
72
 
73
73
  // src/main.ts
74
74
  import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
@@ -1962,13 +1962,71 @@ var COMMANDS = [
1962
1962
  name: "run",
1963
1963
  section: "sessions",
1964
1964
  summary: "run any command as a session",
1965
- synopsis: ["[--project <p>] -- <command...>"],
1965
+ synopsis: [
1966
+ "[--project <p>] [--name <n> | --worktree <n>] [--base <ref>] [--detach] [--json] -- <command...>"
1967
+ ],
1966
1968
  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."
1969
+ "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.",
1970
+ "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.",
1971
+ "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.",
1972
+ "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.",
1973
+ "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).",
1974
+ "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
1975
  ],
1969
- options: [project()],
1970
- examples: [{ command: "mend run -- aider --model gpt-5", text: "" }],
1971
- see: ["codex"]
1976
+ options: [
1977
+ project(),
1978
+ { flag: "--name <n>", text: "name the worktree; an existing name joins it" },
1979
+ { flag: "--worktree <n>", text: "join an existing worktree only" },
1980
+ { flag: "--base <ref>", text: "create the worktree from another git base" },
1981
+ { flag: "--detach, -d", text: "return once the command runs" },
1982
+ { flag: "--json", text: "print the session and process ids, and the end, as JSON" }
1983
+ ],
1984
+ examples: [
1985
+ { command: "mend run -- pnpm test", text: "the test output here, exit code included" },
1986
+ {
1987
+ command: "id=$(mend run --detach --json -- make build | jq -r .sessionId)",
1988
+ text: "start it, then mend wait $id"
1989
+ }
1990
+ ],
1991
+ see: ["logs", "wait", "codex"]
1992
+ },
1993
+ {
1994
+ name: "logs",
1995
+ section: "sessions",
1996
+ summary: "print a session's recorded terminal output",
1997
+ synopsis: ["[session] [--follow] [--from <sequence>] [--process <id>]"],
1998
+ description: [
1999
+ "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.",
2000
+ "<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."
2001
+ ],
2002
+ options: [
2003
+ { flag: "--follow, -f", text: "keep printing until the process ends" },
2004
+ { flag: "--from <sequence>", text: "start at a record sequence. Default: 0" },
2005
+ { flag: "--process <id>", text: "another process of the session, by a prefix of its id" }
2006
+ ],
2007
+ examples: [{ command: "mend logs 3f2a --follow", text: "" }],
2008
+ see: ["run", "wait", "service logs"]
2009
+ },
2010
+ {
2011
+ name: "wait",
2012
+ section: "sessions",
2013
+ summary: "wait for a session's command to end",
2014
+ synopsis: ["[session] [--timeout <seconds>] [--process <id>] [--json]"],
2015
+ description: [
2016
+ "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.",
2017
+ "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.",
2018
+ "<session> is the session id, a prefix of it, or the worktree's name. With none, the one live session is taken."
2019
+ ],
2020
+ options: [
2021
+ { flag: "--timeout <seconds>", text: "give up after this long; exit 124" },
2022
+ {
2023
+ flag: "--process <id>",
2024
+ text: "wait for this process of the session, by a prefix of its id"
2025
+ },
2026
+ { flag: "--json", text: "print the session, the process, the status and the exit code" }
2027
+ ],
2028
+ examples: [{ command: "mend wait 3f2a --timeout 600", text: "" }],
2029
+ see: ["run", "logs"]
1972
2030
  },
1973
2031
  {
1974
2032
  name: "continue",
@@ -2074,8 +2132,13 @@ var COMMANDS = [
2074
2132
  name: "projects",
2075
2133
  section: "sessions",
2076
2134
  summary: "adopted projects and their live sessions",
2077
- synopsis: [],
2078
- description: ["One line per project, with the sessions running in it."],
2135
+ synopsis: ["[--json]"],
2136
+ description: [
2137
+ "One line per project, with the sessions running in it.",
2138
+ '--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).',
2139
+ "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."
2140
+ ],
2141
+ options: [{ flag: "--json", text: "the projects as JSON" }],
2079
2142
  see: ["adopt", "sessions"]
2080
2143
  },
2081
2144
  {
@@ -2156,14 +2219,19 @@ var COMMANDS = [
2156
2219
  section: "services",
2157
2220
  summary: "start and supervise a server in the session's workspace",
2158
2221
  synopsis: [
2159
- "[session] --port <port> [--name <n>] [--udp] [--http|--https] [--no-connect] -- <command...>",
2160
- "[session] <name> [--no-connect]"
2222
+ "[session] --port <port> [--name <n>] [--udp] [--http|--https] [--wait] [--no-connect] -- <command...>",
2223
+ "[session] <name> [--wait] [--no-connect]"
2161
2224
  ],
2162
2225
  description: [
2163
2226
  "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.",
2227
+ "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
2228
  "The port is tunnelled to this machine's loopback as soon as it listens, unless --no-connect."
2165
2229
  ],
2166
2230
  options: [
2231
+ {
2232
+ flag: "--wait",
2233
+ text: "exit 1 when the port did not answer, 124 with no answer in 90 s; no tunnel"
2234
+ },
2167
2235
  { flag: "--port <port>", text: "the port the command listens on inside the workspace" },
2168
2236
  { flag: "--name <n>", text: "the service's name. Default: the command" },
2169
2237
  { flag: "--udp", text: "a UDP port" },
@@ -2216,7 +2284,10 @@ var COMMANDS = [
2216
2284
  section: "services",
2217
2285
  summary: "follow a supervised service's output",
2218
2286
  synopsis: ["<name-or-id> [--from <sequence>]"],
2219
- description: ["Replays the recorded output, then follows it live. Ctrl-C stops following."],
2287
+ description: [
2288
+ "Replays the recorded output, then follows it live. Ctrl-C stops following.",
2289
+ "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."
2290
+ ],
2220
2291
  options: [{ flag: "--from <sequence>", text: "start the replay at a record sequence" }],
2221
2292
  see: ["service run"]
2222
2293
  },
@@ -3998,8 +4069,10 @@ var doctorCommand = async (config, localCredential2, claudeGrant2) => {
3998
4069
  onPath,
3999
4070
  dockerShutdown: observeHostShutdownTimeout
4000
4071
  });
4001
- for (const check of checks) process.stdout.write(`${formatCheck(check, paintMark)}
4072
+ for (const check of checks) {
4073
+ process.stdout.write(`${redactCredentials(formatCheck(check, paintMark))}
4002
4074
  `);
4075
+ }
4003
4076
  if (checks.some((check) => check.state === "failed")) process.exitCode = 1;
4004
4077
  };
4005
4078
 
@@ -4197,10 +4270,10 @@ var paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text
4197
4270
  var dim = paint("2");
4198
4271
  var green = paint("32");
4199
4272
  var amber = paint("33");
4200
- var say = (line) => process.stdout.write(`${line}
4273
+ var say = (line) => process.stdout.write(`${redactCredentials(line)}
4201
4274
  `);
4202
4275
  var fail = (message) => {
4203
- process.stderr.write(`mend: ${message}
4276
+ process.stderr.write(`mend: ${redactCredentials(message)}
4204
4277
  `);
4205
4278
  process.exit(1);
4206
4279
  };
@@ -4667,10 +4740,10 @@ import * as os7 from "node:os";
4667
4740
  var paint2 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
4668
4741
  var dim2 = paint2("2");
4669
4742
  var green2 = paint2("32");
4670
- var say2 = (line) => process.stdout.write(`${line}
4743
+ var say2 = (line) => process.stdout.write(`${redactCredentials(line)}
4671
4744
  `);
4672
4745
  var fail2 = (message) => {
4673
- process.stderr.write(`mend: ${message}
4746
+ process.stderr.write(`mend: ${redactCredentials(message)}
4674
4747
  `);
4675
4748
  process.exit(1);
4676
4749
  };
@@ -4761,10 +4834,10 @@ var paint3 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m$
4761
4834
  var dim3 = paint3("2");
4762
4835
  var green3 = paint3("32");
4763
4836
  var cobalt = paint3("34");
4764
- var say3 = (line) => process.stdout.write(`${line}
4837
+ var say3 = (line) => process.stdout.write(`${redactCredentials(line)}
4765
4838
  `);
4766
4839
  var fail3 = (message) => {
4767
- process.stderr.write(`mend: ${message}
4840
+ process.stderr.write(`mend: ${redactCredentials(message)}
4768
4841
  `);
4769
4842
  process.exit(1);
4770
4843
  };
@@ -4942,10 +5015,10 @@ import * as path12 from "node:path";
4942
5015
  var paint4 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
4943
5016
  var dim4 = paint4("2");
4944
5017
  var green4 = paint4("32");
4945
- var say4 = (line) => process.stdout.write(`${line}
5018
+ var say4 = (line) => process.stdout.write(`${redactCredentials(line)}
4946
5019
  `);
4947
5020
  var fail4 = (message) => {
4948
- process.stderr.write(`mend: ${message}
5021
+ process.stderr.write(`mend: ${redactCredentials(message)}
4949
5022
  `);
4950
5023
  process.exit(1);
4951
5024
  };
@@ -5569,6 +5642,208 @@ var piProfileLines = (scan) => {
5569
5642
  ];
5570
5643
  };
5571
5644
 
5645
+ // src/run-scripts.ts
5646
+ var RUN_ARGV_MAX = 64;
5647
+ var whitespaceName = (character) => character === "\n" || character === "\r" ? "a newline" : character === " " ? "a tab" : character === " " ? "a space" : "whitespace";
5648
+ var runArgvIssue = (argv) => {
5649
+ if (argv.length > RUN_ARGV_MAX) {
5650
+ 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`;
5651
+ }
5652
+ for (const [index, word] of argv.entries()) {
5653
+ const which = index === 0 ? "the program" : `argument ${index}`;
5654
+ if (word === "") {
5655
+ return `${which} is empty \xB7 the platform refuses empty arguments`;
5656
+ }
5657
+ if (word.trim() !== word) {
5658
+ const leading = word.trimStart() !== word;
5659
+ const edge = leading ? word.charAt(0) : word.charAt(word.length - 1);
5660
+ return `${which} ${leading ? "starts" : "ends"} with ${whitespaceName(edge)} \xB7 the platform refuses arguments with leading or trailing whitespace \xB7 trim it and run again`;
5661
+ }
5662
+ }
5663
+ return null;
5664
+ };
5665
+ var pickProcess = (detail, prefix) => {
5666
+ const processes = detail.processes ?? [];
5667
+ if (prefix !== null) {
5668
+ const matches = processes.filter((candidate) => candidate.id?.startsWith(prefix) === true);
5669
+ const [only] = matches;
5670
+ if (matches.length > 1) {
5671
+ return { error: `"${prefix}" matches ${matches.length} processes; type more of the id` };
5672
+ }
5673
+ if (only === void 0) return { error: `no process of this session matches "${prefix}"` };
5674
+ return { process: only };
5675
+ }
5676
+ const chosen = detail.currentAgent ?? processes.at(-1);
5677
+ if (chosen === void 0) return { error: "this session has run nothing yet" };
5678
+ return { process: chosen };
5679
+ };
5680
+ var SETTLED2 = /* @__PURE__ */ new Set(["completed", "failed", "stopped"]);
5681
+ var processRowOf = (detail, processId) => {
5682
+ const rows = [
5683
+ ...detail.currentAgent === null ? [] : [detail.currentAgent],
5684
+ ...detail.processes ?? []
5685
+ ].filter((row) => row.id === processId);
5686
+ return rows.find((row) => row.exitedAt !== null) ?? rows[0];
5687
+ };
5688
+ var commandEndOf = (detail, processId = null) => {
5689
+ const settled = SETTLED2.has(detail.session.status);
5690
+ if (processId !== null) {
5691
+ const target = processRowOf(detail, processId);
5692
+ if (target !== void 0 && target.exitedAt !== null) {
5693
+ return { processId, status: target.status, exitCode: target.exitCode };
5694
+ }
5695
+ if (settled) {
5696
+ return { processId, status: detail.session.status, exitCode: target?.exitCode ?? null };
5697
+ }
5698
+ return null;
5699
+ }
5700
+ if (detail.session.status === "starting") return null;
5701
+ const agent = detail.currentAgent;
5702
+ if (agent !== null && agent.exitedAt !== null) {
5703
+ return { processId: agent.id ?? null, status: agent.status, exitCode: agent.exitCode };
5704
+ }
5705
+ if (settled) {
5706
+ return {
5707
+ processId: agent?.id ?? null,
5708
+ status: detail.session.status,
5709
+ exitCode: agent?.exitCode ?? null
5710
+ };
5711
+ }
5712
+ return null;
5713
+ };
5714
+ var exitStatusOf = (end) => end.exitCode !== null && Number.isInteger(end.exitCode) && end.exitCode >= 0 && end.exitCode <= 255 ? end.exitCode : 1;
5715
+ var endLine = (end) => `${end.status} \xB7 ${end.exitCode === null ? "no exit code reported" : `code ${end.exitCode}`}`;
5716
+ var isRefusal = (error) => error instanceof MendRequestError && error.kind === "http" && (error.status === null || !GATEWAY_STATUSES.has(error.status));
5717
+ var beforeDeadline = async (work, clock2, deadline) => {
5718
+ if (deadline === null) return { done: true, value: await work };
5719
+ const left = deadline - clock2.now();
5720
+ if (left <= 0) {
5721
+ work.catch(() => void 0);
5722
+ return { done: false };
5723
+ }
5724
+ const timer = new AbortController();
5725
+ try {
5726
+ return await Promise.race([
5727
+ work.then((value) => ({ done: true, value })),
5728
+ clock2.sleep(left, timer.signal).then(() => ({ done: false }))
5729
+ ]);
5730
+ } finally {
5731
+ timer.abort();
5732
+ }
5733
+ };
5734
+ var readThrough = async (read, clock2, deadline) => {
5735
+ const unreachableAfterMs = clock2.unreachableAfterMs ?? 6e4;
5736
+ const since = clock2.now();
5737
+ for (; ; ) {
5738
+ try {
5739
+ return await beforeDeadline(read(), clock2, deadline);
5740
+ } catch (error) {
5741
+ if (isRefusal(error) || clock2.now() - since >= unreachableAfterMs) throw error;
5742
+ const pause2 = deadline === null ? clock2.pollMs ?? 1e3 : Math.min(clock2.pollMs ?? 1e3, deadline - clock2.now());
5743
+ if (pause2 <= 0) return { done: false };
5744
+ await clock2.sleep(pause2);
5745
+ }
5746
+ }
5747
+ };
5748
+ var unbounded = async (read, clock2) => {
5749
+ const got = await readThrough(read, clock2, null);
5750
+ if (!got.done) throw new Error("a read with no deadline timed out");
5751
+ return got.value;
5752
+ };
5753
+ var followLogs = async (options) => {
5754
+ const pollMs = options.pollMs ?? 250;
5755
+ const quietReadsAfterEnd = options.quietReadsAfterEnd ?? 2;
5756
+ let cursor = options.from;
5757
+ let quiet = 0;
5758
+ for (; ; ) {
5759
+ const from = cursor;
5760
+ const page = await unbounded(() => options.read(from), options);
5761
+ for (const chunk of page.chunks) await options.write(Buffer.from(chunk.dataBase64, "base64"));
5762
+ const advanced = page.nextFrom !== cursor;
5763
+ cursor = page.nextFrom;
5764
+ if (advanced) {
5765
+ quiet = 0;
5766
+ continue;
5767
+ }
5768
+ if (!options.follow) return { next: cursor, status: page.status };
5769
+ if (page.status === "exited" || page.status === "failed") {
5770
+ quiet += 1;
5771
+ if (quiet >= quietReadsAfterEnd) return { next: cursor, status: page.status };
5772
+ }
5773
+ await options.sleep(pollMs);
5774
+ }
5775
+ };
5776
+ var waitForCommand = async (options) => {
5777
+ const pollMs = options.pollMs ?? 1e3;
5778
+ let last = null;
5779
+ for (; ; ) {
5780
+ const got = await readThrough(options.read, options, options.deadline);
5781
+ if (!got.done) return { kind: "timeout", last };
5782
+ last = got.value;
5783
+ const end = commandEndOf(got.value, options.processId);
5784
+ if (end !== null) return { kind: "ended", end };
5785
+ const left = options.deadline === null ? pollMs : Math.min(pollMs, options.deadline - options.now());
5786
+ if (left <= 0) return { kind: "timeout", last };
5787
+ await options.sleep(left);
5788
+ }
5789
+ };
5790
+ var splitArgs = (args, valued, switches) => {
5791
+ const values = /* @__PURE__ */ new Map();
5792
+ const on = /* @__PURE__ */ new Set();
5793
+ let session = null;
5794
+ for (let index = 0; index < args.length; index += 1) {
5795
+ const arg = args[index] ?? "";
5796
+ if (valued.includes(arg)) {
5797
+ const value = args[index + 1];
5798
+ if (value === void 0) return { error: `${arg} needs a value` };
5799
+ values.set(arg, value);
5800
+ index += 1;
5801
+ } else if (switches.includes(arg)) {
5802
+ on.add(arg);
5803
+ } else if (arg.startsWith("-")) {
5804
+ return { error: `unknown flag ${arg}` };
5805
+ } else if (session !== null) {
5806
+ return { error: `one session only; "${arg}" is extra` };
5807
+ } else {
5808
+ session = arg;
5809
+ }
5810
+ }
5811
+ return { values, on, session };
5812
+ };
5813
+ var parseLogsArgs = (args) => {
5814
+ const split = splitArgs(args, ["--from", "--process"], ["--follow", "-f"]);
5815
+ if ("error" in split) return split;
5816
+ const from = split.values.get("--from") ?? "0";
5817
+ if (!/^(0|[1-9]\d*)$/u.test(from)) return { error: "--from takes a record sequence, e.g. 0" };
5818
+ return {
5819
+ args: {
5820
+ session: split.session,
5821
+ follow: split.on.has("--follow") || split.on.has("-f"),
5822
+ from,
5823
+ process: split.values.get("--process") ?? null
5824
+ }
5825
+ };
5826
+ };
5827
+ var parseWaitArgs = (args) => {
5828
+ const split = splitArgs(args, ["--timeout", "--process"], ["--json"]);
5829
+ if ("error" in split) return split;
5830
+ const timeout = split.values.get("--timeout");
5831
+ const seconds2 = timeout === void 0 ? null : Number(timeout);
5832
+ if (seconds2 !== null && !(Number.isFinite(seconds2) && seconds2 > 0)) {
5833
+ return { error: "--timeout takes a number of seconds above 0" };
5834
+ }
5835
+ return {
5836
+ args: {
5837
+ session: split.session,
5838
+ process: split.values.get("--process") ?? null,
5839
+ timeoutMs: seconds2 === null ? null : Math.ceil(seconds2 * 1e3),
5840
+ json: split.on.has("--json")
5841
+ }
5842
+ };
5843
+ };
5844
+ var WAIT_TIMED_OUT = 124;
5845
+ 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);
5846
+
5572
5847
  // src/secret-files.ts
5573
5848
  import * as path14 from "node:path";
5574
5849
  var secretFilePathOf = (input, home) => {
@@ -8337,7 +8612,7 @@ var nodeServerRuntime = () => {
8337
8612
  },
8338
8613
  randomBytes,
8339
8614
  sleep: (milliseconds) => new Promise((resolve8) => setTimeout(resolve8, milliseconds)),
8340
- writeLine: (line) => process.stdout.write(`${line}
8615
+ writeLine: (line) => process.stdout.write(`${redactCredentials(line)}
8341
8616
  `),
8342
8617
  dockerDaemonFacts: hostDockerDaemonFacts,
8343
8618
  readLogin: (configDir) => savedLogin(configDir, environment)
@@ -9317,7 +9592,7 @@ var ansi = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${t
9317
9592
  var dim5 = ansi("2");
9318
9593
  var green5 = ansi("32");
9319
9594
  var warn = ansi("33");
9320
- var say5 = (line) => console.log(line);
9595
+ var say5 = (line) => console.log(redactCredentials(line));
9321
9596
  var sshConfigPath = () => path19.join(os12.homedir(), ".ssh", "config");
9322
9597
  var flagValue2 = (args, flag) => {
9323
9598
  const index = args.indexOf(flag);
@@ -9757,10 +10032,10 @@ var upgradeUrl = async (input) => {
9757
10032
  var paint5 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
9758
10033
  var dim6 = paint5("2");
9759
10034
  var green6 = paint5("32");
9760
- var say6 = (line) => process.stdout.write(`${line}
10035
+ var say6 = (line) => process.stdout.write(`${redactCredentials(line)}
9761
10036
  `);
9762
10037
  var fail6 = (message) => {
9763
- process.stderr.write(`mend: ${message}
10038
+ process.stderr.write(`mend: ${redactCredentials(message)}
9764
10039
  `);
9765
10040
  process.exit(1);
9766
10041
  };
@@ -9922,7 +10197,7 @@ var loadConfig = () => {
9922
10197
  };
9923
10198
  };
9924
10199
  var fail7 = (message) => {
9925
- process.stderr.write(`mend: ${message}
10200
+ process.stderr.write(`mend: ${redactCredentials(message)}
9926
10201
  `);
9927
10202
  process.exit(1);
9928
10203
  };
@@ -9933,12 +10208,16 @@ var parseMendUrl = (value) => {
9933
10208
  return fail7(`"${value}" is not a valid Mend URL`);
9934
10209
  }
9935
10210
  };
9936
- var paint6 = (code) => (text) => process.stdout.isTTY ? `\x1B[${code}m${text}\x1B[0m` : text;
10211
+ var chrome = process.stdout;
10212
+ var chromeToStderr = () => {
10213
+ chrome = process.stderr;
10214
+ };
10215
+ var paint6 = (code) => (text) => chrome.isTTY ? `\x1B[${code}m${text}\x1B[0m` : text;
9937
10216
  var dim7 = paint6("2");
9938
10217
  var green7 = paint6("32");
9939
10218
  var amber2 = paint6("33");
9940
10219
  var cobalt2 = paint6("34");
9941
- var say7 = (line) => process.stdout.write(`${line}
10220
+ var say7 = (line) => chrome.write(`${redactCredentials(line)}
9942
10221
  `);
9943
10222
  var detachKeyEnabled = process.env["MEND_DETACH_KEY"] !== "none";
9944
10223
  var detachHint = () => detachKeyEnabled ? ` \xB7 detach: ${dim7("Ctrl+]")}` : "";
@@ -10077,15 +10356,15 @@ var socketUrl = (config, target, params, extra, signal) => upgradeUrl({
10077
10356
  });
10078
10357
  var boundApi = (config) => (method, route, body) => api(config, method, route, body);
10079
10358
  var withSpinner = async (label, work) => {
10080
- if (process.stdout.isTTY !== true) return work;
10359
+ if (chrome.isTTY !== true) return work;
10081
10360
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
10082
10361
  const started = Date.now();
10083
10362
  let frame = 0;
10084
10363
  const timer = setInterval(() => {
10085
10364
  const seconds2 = Math.round((Date.now() - started) / 1e3);
10086
10365
  const text = typeof label === "string" ? label : label();
10087
- process.stdout.write(
10088
- `\r\x1B[2K ${frames[frame % frames.length]} ${text} ${dim7(`${seconds2}s`)} `
10366
+ chrome.write(
10367
+ `\r\x1B[2K ${frames[frame % frames.length]} ${redactCredentials(text)} ${dim7(`${seconds2}s`)} `
10089
10368
  );
10090
10369
  frame += 1;
10091
10370
  }, 120);
@@ -10093,7 +10372,7 @@ var withSpinner = async (label, work) => {
10093
10372
  return await work;
10094
10373
  } finally {
10095
10374
  clearInterval(timer);
10096
- process.stdout.write("\r\x1B[2K");
10375
+ chrome.write("\r\x1B[2K");
10097
10376
  }
10098
10377
  };
10099
10378
  var followStarting = async (config, sessionId, label, start) => {
@@ -10114,7 +10393,7 @@ var followStarting = async (config, sessionId, label, start) => {
10114
10393
  read: () => request(config, "GET", `/sessions/${sessionId}`),
10115
10394
  onLine: (next) => {
10116
10395
  line = next === "starting" ? label : next;
10117
- if (process.stdout.isTTY !== true) say7(dim7(` ${next}`));
10396
+ if (chrome.isTTY !== true) say7(dim7(` ${next}`));
10118
10397
  },
10119
10398
  sleep: sleep3,
10120
10399
  now: Date.now
@@ -10301,8 +10580,12 @@ var workspaceLinesOf = async (config, sessions) => {
10301
10580
  );
10302
10581
  };
10303
10582
  var launch = async (config, harness, args) => {
10583
+ if (harness === "run") chromeToStderr();
10304
10584
  const parsed = parseLaunchArgs(args);
10305
10585
  if (parsed.error !== null) return fail7(parsed.error);
10586
+ if (harness !== "run" && parsed.json) {
10587
+ return fail7(`mend ${harness} takes no --json \xB7 mend run --json prints what it started`);
10588
+ }
10306
10589
  const structured = parsed.prompt !== null || parsed.model !== null || parsed.effort !== null || parsed.ask || parsed.fast;
10307
10590
  if (harness === "run" && structured) {
10308
10591
  return fail7(`mend run takes no prompt or harness flags \xB7 ${usageOf("run")}`);
@@ -10312,15 +10595,17 @@ var launch = async (config, harness, args) => {
10312
10595
  "mend run takes no landing flags \u2014 a command has no turns to land after; land it with mend land"
10313
10596
  );
10314
10597
  }
10315
- if (harness === "run" && (parsed.detach || parsed.foreground)) {
10598
+ if (harness === "run" && parsed.foreground) {
10316
10599
  return fail7(
10317
- "mend run takes no lifecycle flags \u2014 it tails the record; Ctrl+C stops watching, not the command"
10600
+ "mend run takes no --foreground \u2014 Ctrl+C stops watching, not the command; mend stop ends it"
10318
10601
  );
10319
10602
  }
10320
10603
  const argv = harness === "run" ? parsed.custom : HARNESS_COMMANDS[harness] ?? [];
10321
10604
  if (argv.length === 0) {
10322
10605
  return fail7(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
10323
10606
  }
10607
+ const argvIssue = harness === "run" ? runArgvIssue(argv) : null;
10608
+ if (argvIssue !== null) return fail7(`${argvIssue} \xB7 nothing was created`);
10324
10609
  const project2 = await findProject(config, parsed.project, true);
10325
10610
  say7(
10326
10611
  `${green7("\u2713")} project ${project2.name} ${dim7(`\xB7 ${project2.defaultBranch}${parsed.project === null ? " \xB7 from cwd" : ""}`)}`
@@ -10392,7 +10677,7 @@ var launch = async (config, harness, args) => {
10392
10677
  );
10393
10678
  say7(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${session.id}`);
10394
10679
  if (harness === "run") {
10395
- return supervisedRun(config, session, argv);
10680
+ return supervisedRun(config, session, argv, { detach: parsed.detach, json: parsed.json });
10396
10681
  }
10397
10682
  const launchBody = {
10398
10683
  ...parsed.prompt === null ? {} : { prompt: parsed.prompt },
@@ -10622,7 +10907,7 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
10622
10907
  watching = true;
10623
10908
  const members = await request(config, "GET", "/organization/members").catch(() => []);
10624
10909
  process.stdout.write(
10625
- `\r\x1B[2K${dim7(watchNotice(ownerNameOf(detail?.session.ownerUserId, members)))}\r
10910
+ `\r\x1B[2K${dim7(redactCredentials(watchNotice(ownerNameOf(detail?.session.ownerUserId, members))))}\r
10626
10911
  ${dim7(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}\r
10627
10912
  \r
10628
10913
  `
@@ -10668,10 +10953,7 @@ ${dim7(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}
10668
10953
  ws?.removeEventListener("message", onTtyFrame);
10669
10954
  if (rawModeEnabled) process.stdin.setRawMode(false);
10670
10955
  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
- );
10956
+ process.stdout.write(TERMINAL_MODES_RESET);
10675
10957
  }
10676
10958
  process.stdin.pause();
10677
10959
  if (ws !== null && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING))
@@ -10679,6 +10961,7 @@ ${dim7(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}
10679
10961
  stopHerdrHint();
10680
10962
  }
10681
10963
  };
10964
+ var TERMINAL_MODES_RESET = "\x1B[<u\x1B[=0;1u\x1B[?2004l\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1049l\x1B[?25h";
10682
10965
  var stopSessionQuickly = async (config, sessionId) => {
10683
10966
  const headers = { "content-type": "application/json" };
10684
10967
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
@@ -11121,19 +11404,48 @@ var autoConnect = async (config, service) => {
11121
11404
  say7(dim7(" remote server \u2014 tunneling the port here \xB7 Ctrl-C stops the tunnel, not the Service"));
11122
11405
  await tunnelServices(config, [service], null);
11123
11406
  };
11407
+ var SERVICE_WAIT_MS = (() => {
11408
+ const configured = Number(process.env["MEND_SERVICE_WAIT_MS"]);
11409
+ return Number.isFinite(configured) && configured > 0 ? configured : 9e4;
11410
+ })();
11411
+ var withinServiceWait = async (work, deadline) => {
11412
+ const bounded = await beforeDeadline(work, clock, deadline);
11413
+ if (bounded.done) return bounded.value;
11414
+ process.stderr.write(
11415
+ `mend: no answer within ${Math.round(SERVICE_WAIT_MS / 1e3)} s \xB7 the Service may still be starting \xB7 mend service list
11416
+ `
11417
+ );
11418
+ return exitFlushed(WAIT_TIMED_OUT);
11419
+ };
11420
+ var failUnlessAnswered = (config, service, wait) => {
11421
+ if (!wait) return;
11422
+ const name = service.label ?? service.id.slice(0, 8);
11423
+ if (service.status !== "reachable") {
11424
+ fail7(
11425
+ `nothing answered on :${service.workspacePort} \xB7 ${service.status} \xB7 the Service keeps running \xB7 mend service logs ${name}`
11426
+ );
11427
+ }
11428
+ if (willAutoConnect(config, service, false)) {
11429
+ say7(dim7(` connect: mend service connect ${name}`));
11430
+ }
11431
+ };
11124
11432
  var serviceRun = async (config, args) => {
11125
11433
  const dashdash = args.indexOf("--");
11126
11434
  const usage2 = usageOf("service run");
11435
+ const wait = (dashdash === -1 ? args : args.slice(0, dashdash)).includes("--wait");
11436
+ const deadline = wait ? clock.now() + SERVICE_WAIT_MS : null;
11127
11437
  if (dashdash === -1) {
11128
11438
  const positionals = args.filter((a) => !a.startsWith("--"));
11129
11439
  const name2 = positionals.at(-1);
11130
11440
  if (name2 === void 0) return fail7(usage2);
11131
11441
  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`
11442
+ const session2 = await withinServiceWait(
11443
+ resolveLiveSession(config, prefix2, "service run"),
11444
+ deadline
11445
+ );
11446
+ const recipes = await withinServiceWait(
11447
+ api(config, "GET", `/sessions/${session2.id}/recipes`),
11448
+ deadline
11137
11449
  );
11138
11450
  const recipe = recipes.find((entry) => entry.name === name2);
11139
11451
  if (recipe === void 0) {
@@ -11142,16 +11454,28 @@ var serviceRun = async (config, args) => {
11142
11454
  recipes.length === 0 ? `no mend.toml recipes in this worktree \u2014 declare [service.${name2}] first` : `no recipe named "${name2}" \u2014 declared: ${known}`
11143
11455
  );
11144
11456
  }
11457
+ if (wait && recipe.protocol === "udp") {
11458
+ return fail7(`--wait needs a TCP port: ${recipe.name} is UDP, which has no probe`);
11459
+ }
11460
+ if (wait && recipe.command === null) {
11461
+ return fail7(
11462
+ `--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`
11463
+ );
11464
+ }
11145
11465
  const service2 = await withSpinner(
11146
11466
  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
- })
11467
+ withinServiceWait(
11468
+ mutateService(config, "POST", `/sessions/${session2.id}/services/recipe`, {
11469
+ name: recipe.name
11470
+ }),
11471
+ deadline
11472
+ )
11150
11473
  );
11151
- const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect"));
11474
+ const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect") || wait);
11152
11475
  say7(`${green7("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
11153
11476
  printServiceEndpoint(config, service2, tunneling2);
11154
11477
  say7(dim7(` logs: mend service logs ${service2.label ?? service2.id.slice(0, 8)}`));
11478
+ failUnlessAnswered(config, service2, wait);
11155
11479
  if (tunneling2) await autoConnect(config, service2);
11156
11480
  return;
11157
11481
  }
@@ -11169,24 +11493,34 @@ var serviceRun = async (config, args) => {
11169
11493
  if (http && https) return fail7(usage2);
11170
11494
  const browserScheme = https ? "https" : http ? "http" : null;
11171
11495
  if (protocol === "udp" && browserScheme !== null) return fail7(usage2);
11496
+ if (protocol === "udp" && wait) {
11497
+ return fail7("--wait needs a TCP port: UDP has no probe, so nothing could be waited for");
11498
+ }
11172
11499
  const prefix = head.find(
11173
- (a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1
11500
+ (a, i) => !a.startsWith("--") && (portFlag === -1 || i !== portFlag + 1) && (nameFlag === -1 || i !== nameFlag + 1)
11501
+ );
11502
+ const session = await withinServiceWait(
11503
+ resolveLiveSession(config, prefix, "service run"),
11504
+ deadline
11174
11505
  );
11175
- const session = await resolveLiveSession(config, prefix, "service run");
11176
11506
  const service = await withSpinner(
11177
11507
  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
- })
11508
+ withinServiceWait(
11509
+ mutateService(config, "POST", `/sessions/${session.id}/services/run`, {
11510
+ argv,
11511
+ port,
11512
+ name,
11513
+ protocol,
11514
+ browserScheme
11515
+ }),
11516
+ deadline
11517
+ )
11185
11518
  );
11186
- const tunneling = willAutoConnect(config, service, head.includes("--no-connect"));
11519
+ const tunneling = willAutoConnect(config, service, head.includes("--no-connect") || wait);
11187
11520
  say7(`${green7("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
11188
11521
  printServiceEndpoint(config, service, tunneling);
11189
11522
  say7(dim7(` logs: mend service logs ${service.label ?? service.id.slice(0, 8)}`));
11523
+ failUnlessAnswered(config, service, wait);
11190
11524
  if (tunneling) await autoConnect(config, service);
11191
11525
  };
11192
11526
  var serviceLogs = async (config, args) => {
@@ -11308,7 +11642,7 @@ var serviceInit = async (args) => {
11308
11642
  const toml = renderMendToml(proposals);
11309
11643
  say7(dim7(`proposed ${target}:`));
11310
11644
  say7("");
11311
- process.stdout.write(toml);
11645
+ process.stdout.write(redactCredentials(toml));
11312
11646
  say7("");
11313
11647
  if (!args.includes("--yes")) {
11314
11648
  if (process.stdin.isTTY !== true) {
@@ -11355,7 +11689,7 @@ var attachTunnels = (config, optOut) => {
11355
11689
  return;
11356
11690
  }
11357
11691
  const rows = process.stdout.rows ?? 24;
11358
- process.stdout.write(`\x1B7\x1B[${rows};1H\x1B[2K${line}\x1B8`);
11692
+ process.stdout.write(`\x1B7\x1B[${rows};1H\x1B[2K${redactCredentials(line)}\x1B8`);
11359
11693
  };
11360
11694
  const tunnels = createServiceTunnels({
11361
11695
  listServices: () => fetchServices(config),
@@ -11634,7 +11968,7 @@ var accountsCommand = async (config) => {
11634
11968
  } catch (error) {
11635
11969
  return fail7(error instanceof Error ? error.message : String(error));
11636
11970
  }
11637
- for (const line of lines) process.stdout.write(`${line}
11971
+ for (const line of lines) process.stdout.write(`${redactCredentials(line)}
11638
11972
  `);
11639
11973
  };
11640
11974
  var claudeGrantSecret = () => {
@@ -11645,7 +11979,7 @@ var doctorBundle = (config, args) => doctorBundleCommand(args, {
11645
11979
  defaultDir: path21.join(mendCliHome(), "bundles"),
11646
11980
  now: () => /* @__PURE__ */ new Date(),
11647
11981
  say: say7,
11648
- warn: (line) => process.stderr.write(`${line}
11982
+ warn: (line) => process.stderr.write(`${redactCredentials(line)}
11649
11983
  `),
11650
11984
  collectors: (tail) => bundleCollectors({
11651
11985
  cliVersion: cliVersion(),
@@ -11833,7 +12167,7 @@ var connectCommand = async (config, args) => {
11833
12167
  secret: narrowed.secret
11834
12168
  })
11835
12169
  );
11836
- process.stdout.write(`${accountLine(account)}
12170
+ process.stdout.write(`${redactCredentials(accountLine(account))}
11837
12171
  `);
11838
12172
  const facts = claudeGrantFacts(narrowed.secret);
11839
12173
  if (facts !== null) {
@@ -12443,6 +12777,7 @@ _mend() {
12443
12777
  'adopt:adopt a repository into the store'
12444
12778
  'codex:new session + codex' 'claude:new session + claude' 'opencode:new session + opencode' 'pi:new session + pi'
12445
12779
  'run:new session + arbitrary command'
12780
+ 'logs:recorded terminal output of a session' 'wait:wait for the command of a session to end'
12446
12781
  'attach:reattach to a running session' 'stop:stop the agent \u2014 record and review remain'
12447
12782
  'shell:open a shell in a live session workspace'
12448
12783
  'service:reachable ports \u2014 add, list, stop'
@@ -12479,7 +12814,7 @@ _mend() {
12479
12814
  esac
12480
12815
  fi
12481
12816
  ;;
12482
- shell|attach|stop|continue|resume|rejoin|land|pull)
12817
+ shell|attach|stop|continue|resume|rejoin|land|pull|logs|wait)
12483
12818
  local -a sessions
12484
12819
  sessions=(\${(f)"$(command mend __complete session 2>/dev/null | tr '\\t' ':')"})
12485
12820
  (( \${#sessions} )) && _describe 'session' sessions
@@ -12491,7 +12826,7 @@ _mend "$@"
12491
12826
  var BASH_COMPLETIONS = `_mend() {
12492
12827
  local cur=\${COMP_WORDS[COMP_CWORD]}
12493
12828
  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") )
12829
+ 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
12830
  return
12496
12831
  fi
12497
12832
  case \${COMP_WORDS[1]} in
@@ -12509,7 +12844,7 @@ var BASH_COMPLETIONS = `_mend() {
12509
12844
  fi
12510
12845
  COMPREPLY=( $(compgen -W "$options" -- "$cur") )
12511
12846
  ;;
12512
- shell|attach|stop|continue|resume|rejoin|land|pull)
12847
+ shell|attach|stop|continue|resume|rejoin|land|pull|logs|wait)
12513
12848
  COMPREPLY=( $(compgen -W "$(command mend __complete session 2>/dev/null | cut -f1)" -- "$cur") )
12514
12849
  ;;
12515
12850
  esac
@@ -12528,57 +12863,292 @@ var completionsCommand = (args) => {
12528
12863
  return fail7(`${usageOf("completions")} \xB7 e.g. mend completions zsh > "$fpath[1]/_mend"`);
12529
12864
  }
12530
12865
  };
12531
- var supervisedRun = async (config, session, argv) => {
12532
- const launched = await startAndFollow(
12866
+ var pause = (ms, signal) => {
12867
+ const { promise, resolve: resolve8 } = Promise.withResolvers();
12868
+ const timer = setTimeout(resolve8, ms);
12869
+ signal?.addEventListener(
12870
+ "abort",
12871
+ () => {
12872
+ clearTimeout(timer);
12873
+ resolve8();
12874
+ },
12875
+ { once: true }
12876
+ );
12877
+ return promise;
12878
+ };
12879
+ var clock = { sleep: pause, now: Date.now };
12880
+ var printJson = (value) => {
12881
+ process.stdout.write(`${redactCredentials(JSON.stringify(value, null, 2))}
12882
+ `);
12883
+ };
12884
+ var outputOnTerminal = false;
12885
+ var stdoutErrorsHandled = false;
12886
+ var outputStopped = false;
12887
+ var restoreTerminal = () => {
12888
+ if (!outputOnTerminal || process.stdout.isTTY !== true) return;
12889
+ outputOnTerminal = false;
12890
+ process.stdout.write(`${TERMINAL_MODES_RESET}\x1B[0m`);
12891
+ };
12892
+ process.once("exit", restoreTerminal);
12893
+ var writeOutput = (bytes) => {
12894
+ if (outputStopped) return Promise.reject(new Error("stopped by a signal"));
12895
+ if (process.stdout.isTTY === true) outputOnTerminal = true;
12896
+ if (!stdoutErrorsHandled) {
12897
+ stdoutErrorsHandled = true;
12898
+ process.stdout.on("error", () => void 0);
12899
+ }
12900
+ return new Promise((resolve8, reject) => {
12901
+ process.stdout.write(bytes, (error) => {
12902
+ if (error === null || error === void 0) resolve8();
12903
+ else reject(error);
12904
+ });
12905
+ });
12906
+ };
12907
+ var flushed = (stream) => new Promise((resolve8) => {
12908
+ if (stream.destroyed || !stream.writable) resolve8();
12909
+ else stream.write("", () => resolve8());
12910
+ });
12911
+ var EXIT_FLUSH_GRACE_MS = 5e3;
12912
+ var exitFlushed = async (code) => {
12913
+ restoreTerminal();
12914
+ const flush = await beforeDeadline(
12915
+ Promise.all([flushed(process.stdout), flushed(process.stderr)]),
12916
+ clock,
12917
+ clock.now() + EXIT_FLUSH_GRACE_MS
12918
+ );
12919
+ if (!flush.done) {
12920
+ process.stderr.write(
12921
+ `mend: output may be incomplete \xB7 the reader took none of it for ${EXIT_FLUSH_GRACE_MS / 1e3} s
12922
+ `
12923
+ );
12924
+ process.exit(code === 0 ? 1 : code);
12925
+ }
12926
+ process.exit(code);
12927
+ };
12928
+ var failFlushed = (message) => {
12929
+ process.stderr.write(`mend: ${redactCredentials(message)}
12930
+ `);
12931
+ return exitFlushed(1);
12932
+ };
12933
+ var stopOutputOnSignal = (line) => {
12934
+ const signals = [
12935
+ ["SIGHUP", 129],
12936
+ ["SIGINT", 130],
12937
+ ["SIGTERM", 143]
12938
+ ];
12939
+ const handlers = signals.map(([signal, code]) => {
12940
+ const handler = () => {
12941
+ if (outputStopped) {
12942
+ restoreTerminal();
12943
+ process.exit(code);
12944
+ }
12945
+ outputStopped = true;
12946
+ say7("");
12947
+ say7(`${amber2("\xB7")} ${line}`);
12948
+ void exitFlushed(code);
12949
+ };
12950
+ process.on(signal, handler);
12951
+ return () => process.off(signal, handler);
12952
+ });
12953
+ return () => {
12954
+ for (const undo of handlers) undo();
12955
+ };
12956
+ };
12957
+ var readLogPage = (config, processId, from) => request(
12958
+ config,
12959
+ "GET",
12960
+ `/processes/${processId}/logs?from=${encodeURIComponent(from)}&limit=256`
12961
+ );
12962
+ var writeProcessLogs = (config, processId, from, follow) => followLogs({
12963
+ from,
12964
+ follow,
12965
+ read: (cursor) => readLogPage(config, processId, cursor),
12966
+ write: writeOutput,
12967
+ ...clock
12968
+ });
12969
+ var waitForSessionCommand = (config, sessionId, processId, deadline) => waitForCommand({
12970
+ read: () => request(config, "GET", `/sessions/${sessionId}`),
12971
+ processId,
12972
+ deadline,
12973
+ ...clock
12974
+ });
12975
+ var runJsonOf = (config, session, processId, state) => ({
12976
+ version: 1,
12977
+ sessionId: session.id,
12978
+ processId,
12979
+ worktree: session.worktree,
12980
+ branch: session.branch,
12981
+ url: `${config.url}/sessions/${session.id}`,
12982
+ status: state.status,
12983
+ exitCode: state.exitCode
12984
+ });
12985
+ var observedState = (detail, processId, fallback) => {
12986
+ if (detail === null) return { status: fallback, exitCode: null };
12987
+ const row = (processId === null ? void 0 : processRowOf(detail, processId)) ?? detail.currentAgent;
12988
+ return row === null ? { status: detail.session.status, exitCode: null } : { status: row.status, exitCode: row.exitCode };
12989
+ };
12990
+ var exitWithCommandEnd = (config, session, end, json2, undelivered = null) => {
12991
+ const commandCode = exitStatusOf(end);
12992
+ const code = undelivered !== null && commandCode === 0 ? 1 : commandCode;
12993
+ say7(`${commandCode === 0 ? green7("\u2713") : amber2("\xB7")} ${endLine(end)} \xB7 recorded`);
12994
+ if (undelivered !== null) {
12995
+ say7(
12996
+ `${amber2("\xB7")} output not delivered \xB7 ${undelivered} \xB7 read it with mend logs ${session.id.slice(0, 8)}`
12997
+ );
12998
+ }
12999
+ say7(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
13000
+ if (json2) printJson(runJsonOf(config, session, end.processId, end));
13001
+ return exitFlushed(code);
13002
+ };
13003
+ var supervisedRun = async (config, session, argv, options) => {
13004
+ const outcome = await followStarting(
12533
13005
  config,
12534
13006
  session.id,
12535
13007
  "provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
12536
13008
  request(config, "POST", `/sessions/${session.id}/launch`, { argv })
12537
13009
  );
13010
+ const ranAndEnded = outcome.kind === "settled" ? await request(config, "GET", `/sessions/${session.id}`).catch(() => null) : null;
13011
+ if (outcome.kind !== "live" && ranAndEnded?.currentAgent?.id === void 0) {
13012
+ startedOrExit(config, session.id, outcome);
13013
+ return;
13014
+ }
13015
+ const detail = ranAndEnded ?? await api(config, "GET", `/sessions/${session.id}`);
13016
+ const processId = detail.currentAgent?.id ?? null;
13017
+ const id8 = session.id.slice(0, 8);
13018
+ if (options.detach) {
13019
+ const ended = commandEndOf(detail, processId);
13020
+ if (ended === null) {
13021
+ say7(`${green7("\u2713 recording")} \xB7 running detached \xB7 session ${dim7(id8)}`);
13022
+ say7(`${cobalt2(" output")} \xB7 mend logs ${id8} --follow`);
13023
+ say7(`${cobalt2(" wait")} \xB7 mend wait ${id8}`);
13024
+ } else {
13025
+ say7(`${amber2("\xB7")} ended before detaching \xB7 ${endLine(ended)} \xB7 session ${dim7(id8)}`);
13026
+ say7(`${cobalt2(" output")} \xB7 mend logs ${id8}`);
13027
+ }
13028
+ if (options.json) {
13029
+ printJson(
13030
+ runJsonOf(
13031
+ config,
13032
+ session,
13033
+ processId,
13034
+ ended ?? observedState(detail, processId, detail.session.status)
13035
+ )
13036
+ );
13037
+ }
13038
+ return;
13039
+ }
12538
13040
  say7(
12539
- `${green7("\u2713 recording")} \xB7 run ${dim7(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
13041
+ `${green7("\u2713 recording")} \xB7 ${argv[0] ?? "command"} \xB7 workspace mounts the worktree \xB7 Ctrl+C stops watching, not the command`
12540
13042
  );
12541
- say7(dim7(` ${argv.join(" ")} \u2014 live record:`));
12542
13043
  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
- }
13044
+ let undelivered = null;
13045
+ if (!options.json) {
13046
+ stopOutputOnSignal(`stopped watching \xB7 the command keeps running \xB7 mend logs ${id8} --follow`);
13047
+ }
13048
+ if (processId !== null && !options.json) {
13049
+ try {
13050
+ await writeProcessLogs(config, processId, "0", true);
13051
+ } catch (error) {
13052
+ undelivered = error instanceof Error ? error.message : String(error);
12575
13053
  }
12576
13054
  }
12577
- await reader.cancel().catch(() => void 0);
13055
+ const waited = await waitForSessionCommand(config, session.id, processId, null).catch(
13056
+ (error) => failFlushed(error instanceof Error ? error.message : String(error))
13057
+ );
13058
+ if (waited.kind === "timeout") return failFlushed("the wait ended without a timeout");
12578
13059
  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);
13060
+ await exitWithCommandEnd(config, session, waited.end, options.json, undelivered);
13061
+ };
13062
+ var resolveAnySession = async (config, word) => {
13063
+ if (isSessionId(word)) {
13064
+ return (await api(config, "GET", `/sessions/${word}`)).session;
13065
+ }
13066
+ const projects = await api(config, "GET", "/projects");
13067
+ const details = await Promise.all(
13068
+ projects.map(
13069
+ (project2) => api(config, "GET", `/projects/${project2.id}?deadEnds=include`)
13070
+ )
13071
+ );
13072
+ const picked = pickSession(
13073
+ details.flatMap((detail) => detail.sessions),
13074
+ word
13075
+ );
13076
+ if ("error" in picked) return fail7(picked.error);
13077
+ return picked.session;
13078
+ };
13079
+ var logsCommand = async (config, args) => {
13080
+ chromeToStderr();
13081
+ const parsed = parseLogsArgs(args);
13082
+ if ("error" in parsed) return fail7(`${parsed.error} \xB7 ${usageOf("logs")}`);
13083
+ const { session: word, follow, from, process: processPrefix } = parsed.args;
13084
+ const session = word === null ? await resolveLiveSession(config, void 0, "logs") : await resolveAnySession(config, word);
13085
+ const detail = await api(config, "GET", `/sessions/${session.id}`);
13086
+ const picked = pickProcess(detail, processPrefix);
13087
+ if ("error" in picked) return fail7(`session ${session.id.slice(0, 8)} \xB7 ${picked.error}`);
13088
+ const target = picked.process;
13089
+ if (target.id === void 0 || target.sealantSessionId === null) {
13090
+ return fail7(
13091
+ `session ${session.id.slice(0, 8)} \xB7 this process has no recorded terminal (an adopted port, or a server older than process ids)`
13092
+ );
13093
+ }
13094
+ stopOutputOnSignal(
13095
+ follow ? `stopped watching \xB7 the process keeps running \xB7 mend logs ${session.id.slice(0, 8)} --follow` : "stopped reading the record"
13096
+ );
13097
+ try {
13098
+ const last = await writeProcessLogs(config, target.id, from, follow);
13099
+ restoreTerminal();
13100
+ if (follow) say7(dim7(`
13101
+ record ended \xB7 ${last.status} \xB7 next sequence ${last.next}`));
13102
+ } catch (error) {
13103
+ return failFlushed(
13104
+ `output not delivered \xB7 ${error instanceof Error ? error.message : String(error)}`
13105
+ );
13106
+ }
13107
+ await exitFlushed(0);
13108
+ };
13109
+ var waitCommand = async (config, args) => {
13110
+ chromeToStderr();
13111
+ const parsed = parseWaitArgs(args);
13112
+ if ("error" in parsed) return fail7(`${parsed.error} \xB7 ${usageOf("wait")}`);
13113
+ const { session: word, timeoutMs, json: json2, process: processPrefix } = parsed.args;
13114
+ const deadline = timeoutMs === null ? null : clock.now() + timeoutMs;
13115
+ const after = `${Math.round((timeoutMs ?? 0) / 1e3)} s`;
13116
+ const timedOut = async (session2, processId2, last) => {
13117
+ say7(
13118
+ 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`
13119
+ );
13120
+ if (json2 && session2 !== null) {
13121
+ const observed = processId2 ?? last?.currentAgent?.id ?? null;
13122
+ printJson(
13123
+ runJsonOf(config, session2, observed, observedState(last, observed, session2.status))
13124
+ );
13125
+ }
13126
+ return exitFlushed(WAIT_TIMED_OUT);
13127
+ };
13128
+ const resolved = await beforeDeadline(
13129
+ word === null ? resolveLiveSession(config, void 0, "wait") : resolveAnySession(config, word),
13130
+ clock,
13131
+ deadline
13132
+ );
13133
+ if (!resolved.done) return timedOut(null, null, null);
13134
+ const session = resolved.value;
13135
+ let processId = null;
13136
+ if (processPrefix !== null) {
13137
+ const read = await beforeDeadline(
13138
+ api(config, "GET", `/sessions/${session.id}`),
13139
+ clock,
13140
+ deadline
13141
+ );
13142
+ if (!read.done) return timedOut(session, null, null);
13143
+ const picked = pickProcess(read.value, processPrefix);
13144
+ if ("error" in picked) return fail7(`session ${session.id.slice(0, 8)} \xB7 ${picked.error}`);
13145
+ processId = picked.process.id ?? null;
13146
+ }
13147
+ const waited = await waitForSessionCommand(config, session.id, processId, deadline).catch(
13148
+ (error) => failFlushed(error instanceof Error ? error.message : String(error))
13149
+ );
13150
+ if (waited.kind === "timeout") return timedOut(session, processId, waited.last);
13151
+ await exitWithCommandEnd(config, session, waited.end, json2);
12582
13152
  };
12583
13153
  var continueSession = async (config, args) => {
12584
13154
  const explicitSession = args.find((a) => !a.startsWith("--")) ?? null;
@@ -12769,22 +13339,39 @@ var rejoinCommand = async (config, args) => {
12769
13339
  await finishAttach(config, session.id, outcome);
12770
13340
  exitAfterSessionEnd(config, session.id);
12771
13341
  };
12772
- var projectsCommand = async (config) => {
13342
+ var projectsCommand = async (config, args) => {
13343
+ const unknown = args.find((arg) => arg !== "--json");
13344
+ if (unknown !== void 0) return fail7(`unknown argument ${unknown} \xB7 ${usageOf("projects")}`);
12773
13345
  const [projects, active] = await Promise.all([
12774
13346
  api(config, "GET", "/projects"),
12775
13347
  api(config, "GET", "/sessions")
12776
13348
  ]);
12777
- if (projects.length === 0) {
12778
- say7(dim7("no adopted projects \u2014 mend adopt brings one in"));
12779
- return;
12780
- }
12781
13349
  const liveByProject = /* @__PURE__ */ new Map();
12782
13350
  for (const session of active) {
12783
13351
  liveByProject.set(session.projectId, (liveByProject.get(session.projectId) ?? 0) + 1);
12784
13352
  }
13353
+ const here = matchProjectByCwd(projects, cwdFacts(process.cwd()));
13354
+ if (args.includes("--json")) {
13355
+ printJson({
13356
+ version: 1,
13357
+ projects: projects.map((project2) => ({
13358
+ id: project2.id,
13359
+ name: project2.name,
13360
+ originUrl: project2.originUrl,
13361
+ defaultBranch: project2.defaultBranch,
13362
+ storePath: project2.storePath,
13363
+ liveSessions: liveByProject.get(project2.id) ?? 0,
13364
+ current: project2.id === here?.id
13365
+ }))
13366
+ });
13367
+ return;
13368
+ }
13369
+ if (projects.length === 0) {
13370
+ say7(dim7("no adopted projects \u2014 mend adopt brings one in"));
13371
+ return;
13372
+ }
12785
13373
  const nameWidth = Math.max(...projects.map((p) => p.name.length));
12786
13374
  const branchWidth = Math.max(...projects.map((p) => p.defaultBranch.length));
12787
- const here = matchProjectByCwd(projects, cwdFacts(process.cwd()));
12788
13375
  for (const project2 of projects) {
12789
13376
  const live = liveByProject.get(project2.id) ?? 0;
12790
13377
  const liveLabel = live > 0 ? green7(`${live} live`) : dim7("\u2014");
@@ -13087,7 +13674,7 @@ var dashboard = async (config, options = {}) => {
13087
13674
  );
13088
13675
  process.exit(rerun.status ?? 0);
13089
13676
  }
13090
- const { runDashboard } = await import("./dashboard-KYXY2JIF.js");
13677
+ const { runDashboard } = await import("./dashboard-CEJN6IOO.js");
13091
13678
  const agentShare = await startShareIfBridge(config);
13092
13679
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
13093
13680
  listServices: () => fetchServices(config),
@@ -13191,6 +13778,10 @@ var main = async () => {
13191
13778
  case "pi":
13192
13779
  case "run":
13193
13780
  return withAgentShare(config, () => launch(config, command, rest));
13781
+ case "logs":
13782
+ return logsCommand(config, rest);
13783
+ case "wait":
13784
+ return waitCommand(config, rest);
13194
13785
  case "attach":
13195
13786
  return withAgentShare(config, () => attach(config, rest));
13196
13787
  case "stop":
@@ -13266,7 +13857,7 @@ var main = async () => {
13266
13857
  case "rejoin":
13267
13858
  return withAgentShare(config, () => rejoinCommand(config, rest));
13268
13859
  case "projects":
13269
- return projectsCommand(config);
13860
+ return projectsCommand(config, rest);
13270
13861
  case "refresh":
13271
13862
  return refreshCommand(config, rest);
13272
13863
  case "land":