@sealant/mend 0.33.0 → 0.34.4

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 (80) hide show
  1. package/README.md +74 -13
  2. package/dist/{chunk-GJFMD7XF.js → chunk-AAAXHRWL.js} +457 -87
  3. package/dist/{dashboard-NWHR7I5W.js → dashboard-DWXXYFPE.js} +41 -13
  4. package/dist/main.js +916 -322
  5. package/man/mend-accounts.1 +1 -1
  6. package/man/mend-adopt.1 +1 -1
  7. package/man/mend-attach.1 +1 -1
  8. package/man/mend-codex.1 +1 -1
  9. package/man/mend-completions.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 +2 -2
  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-man.1 +1 -1
  34. package/man/mend-members.1 +1 -1
  35. package/man/mend-operator-exposure.1 +1 -1
  36. package/man/mend-operator-gate.1 +1 -1
  37. package/man/mend-operator-grant-owner.1 +1 -1
  38. package/man/mend-operator-org-create.1 +1 -1
  39. package/man/mend-operator-org-invite-owner.1 +1 -1
  40. package/man/mend-operator-org-list.1 +1 -1
  41. package/man/mend-operator-org-rename.1 +1 -1
  42. package/man/mend-operator-reset-link.1 +1 -1
  43. package/man/mend-pair.1 +1 -1
  44. package/man/mend-projects.1 +1 -1
  45. package/man/mend-pull.1 +1 -1
  46. package/man/mend-refresh.1 +1 -1
  47. package/man/mend-rejoin.1 +1 -1
  48. package/man/mend-resume.1 +1 -1
  49. package/man/mend-run.1 +1 -1
  50. package/man/mend-server-logs.1 +1 -1
  51. package/man/mend-server-restart.1 +1 -1
  52. package/man/mend-server-setup.1 +1 -1
  53. package/man/mend-server-start.1 +1 -1
  54. package/man/mend-server-status.1 +1 -1
  55. package/man/mend-server-stop.1 +1 -1
  56. package/man/mend-server-upgrade.1 +1 -1
  57. package/man/mend-server.1 +1 -1
  58. package/man/mend-service-add.1 +1 -1
  59. package/man/mend-service-connect.1 +1 -1
  60. package/man/mend-service-init.1 +1 -1
  61. package/man/mend-service-list.1 +1 -1
  62. package/man/mend-service-logs.1 +1 -1
  63. package/man/mend-service-restart.1 +1 -1
  64. package/man/mend-service-run.1 +1 -1
  65. package/man/mend-service-stop.1 +1 -1
  66. package/man/mend-session-share.1 +1 -1
  67. package/man/mend-sessions.1 +1 -1
  68. package/man/mend-shell.1 +1 -1
  69. package/man/mend-skills-push.1 +1 -1
  70. package/man/mend-skills.1 +1 -1
  71. package/man/mend-snake.1 +1 -1
  72. package/man/mend-ssh-setup.1 +1 -1
  73. package/man/mend-ssh.1 +1 -1
  74. package/man/mend-stop.1 +1 -1
  75. package/man/mend-ui.1 +1 -1
  76. package/man/mend-uninstall.1 +1 -1
  77. package/man/mend-version.1 +1 -1
  78. package/man/mend-worktrees.1 +1 -1
  79. package/man/mend.1 +1 -1
  80. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ GATEWAY_STATUSES,
3
4
  HARNESS_COMMANDS,
4
5
  LIVE_STATUSES,
6
+ MendRequestError,
5
7
  agentIsLive,
6
8
  agentOutcome,
9
+ captureLineOf,
7
10
  claudeGrantFacts,
8
11
  createSseParser,
9
12
  cwdFacts,
@@ -17,6 +20,7 @@ import {
17
20
  landingFactsFromWire,
18
21
  matchProjectByCwd,
19
22
  narrowCredential,
23
+ noAnswerError,
20
24
  normalizeGitAuthor,
21
25
  normalizeProjectName,
22
26
  normalizeRemoteUrl,
@@ -26,16 +30,17 @@ import {
26
30
  sameGrant,
27
31
  servicesHoldOf,
28
32
  sessionDisplayName,
33
+ spoken,
29
34
  trackBracketedPaste
30
- } from "./chunk-GJFMD7XF.js";
35
+ } from "./chunk-AAAXHRWL.js";
31
36
 
32
37
  // src/main.ts
33
- import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
34
- import * as fs14 from "node:fs";
38
+ import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
39
+ import * as fs15 from "node:fs";
35
40
  import { createRequire } from "node:module";
36
41
  import * as net4 from "node:net";
37
- import * as os9 from "node:os";
38
- import * as path15 from "node:path";
42
+ import * as os10 from "node:os";
43
+ import * as path16 from "node:path";
39
44
 
40
45
  // src/agent-share.ts
41
46
  import * as net from "node:net";
@@ -1139,7 +1144,7 @@ var COMMANDS = [
1139
1144
  summary: "check this machine's setup, or bundle it for a bug report",
1140
1145
  synopsis: ["", "--bundle [--out <path>] [--tail <n>]"],
1141
1146
  description: [
1142
- "Read-only. One line per fact: the server, the sign-in, the platform connection, the provider accounts, the git key. Each unfinished line ends with the command that fixes it.",
1147
+ "Read-only. One line per fact: the server, the sign-in, the platform connection, the provider accounts, the git key, and where docker is on PATH its daemon's shutdown-timeout against the capture grace. Each unfinished line ends with the command that fixes it.",
1143
1148
  "--bundle collects what a maintainer asks for one output at a time into one tar.gz: this CLI and its environment, the doctor lines, the server's health, the local server's configuration (its compose file and the names of its .env keys, never their values), docker version, info, contexts, the Mend and workspace containers with their inspect facts, the local server's container logs, the running workspace containers' logs, every session with its processes, exit codes and argv, the recorded terminal output of each, the connected accounts, and the versions and paths of claude, codex, gh, git and docker on this machine.",
1144
1149
  "Each part is collected on its own: one that fails leaves a <name>.error.txt in the bundle instead of stopping it. One redactor runs over every file before it is written: bearer and header values, Slack, OpenAI, GitHub and AWS token shapes, JWTs, passwords in URLs, and any password, secret or token value. The archive is written with mode 0600. It still contains logs and configuration; read it before you share it."
1145
1150
  ],
@@ -2505,8 +2510,333 @@ var doctorBundleCommand = async (args, deps) => {
2505
2510
  };
2506
2511
 
2507
2512
  // src/doctor.ts
2513
+ import * as fs6 from "node:fs";
2514
+ import * as path6 from "node:path";
2515
+
2516
+ // src/docker-shutdown.ts
2517
+ import { spawnSync as spawnSync2 } from "node:child_process";
2508
2518
  import * as fs5 from "node:fs";
2519
+ import * as os3 from "node:os";
2509
2520
  import * as path5 from "node:path";
2521
+ var DOCKER_CAPTURE_STOP_GRACE_SECONDS = 3600;
2522
+ var DOCKERD_DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 15;
2523
+ var flagValue = (argv, name) => {
2524
+ for (let index = 0; index < argv.length; index += 1) {
2525
+ const arg = argv[index] ?? "";
2526
+ if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1);
2527
+ if (arg === name) return argv[index + 1] ?? "";
2528
+ }
2529
+ return null;
2530
+ };
2531
+ var seconds = (value) => {
2532
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
2533
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) return Number(value.trim());
2534
+ return null;
2535
+ };
2536
+ var daemonKindOf = (info) => {
2537
+ if (/docker desktop/i.test(info.operatingSystem)) return "desktop";
2538
+ if (/orbstack/i.test(info.operatingSystem)) return "orbstack";
2539
+ if (info.securityOptions.some((option) => option.includes("name=rootless"))) return "rootless";
2540
+ return "dockerd";
2541
+ };
2542
+ var defaultConfigPath = (facts, daemon) => {
2543
+ switch (daemon) {
2544
+ case "desktop":
2545
+ return path5.join(facts.home, ".docker", "daemon.json");
2546
+ case "orbstack":
2547
+ return path5.join(facts.home, ".orbstack", "config", "docker.json");
2548
+ case "rootless":
2549
+ return path5.join(
2550
+ facts.xdgConfigHome ?? path5.join(facts.home, ".config"),
2551
+ "docker",
2552
+ "daemon.json"
2553
+ );
2554
+ case "dockerd":
2555
+ return "/etc/docker/daemon.json";
2556
+ }
2557
+ };
2558
+ var readShutdownTimeout = (facts) => {
2559
+ if (facts.info === null) return { kind: "unknown", why: "docker info did not answer" };
2560
+ const daemon = daemonKindOf(facts.info);
2561
+ const managed = daemon === "desktop" || daemon === "orbstack";
2562
+ const argv = managed ? null : facts.dockerdArgv;
2563
+ const unresolved = facts.dockerdUnresolved ?? null;
2564
+ if (argv === null && !managed && unresolved !== null) return { kind: "unknown", why: unresolved };
2565
+ const configPath = (argv === null ? null : flagValue(argv, "--config-file")) ?? defaultConfigPath(facts, daemon);
2566
+ if (argv !== null) {
2567
+ const flag = flagValue(argv, "--shutdown-timeout");
2568
+ if (flag !== null) {
2569
+ const value = seconds(flag);
2570
+ return value === null ? { kind: "unknown", why: `dockerd --shutdown-timeout ${flag} unreadable` } : {
2571
+ kind: "observed",
2572
+ seconds: value,
2573
+ source: "dockerd --shutdown-timeout",
2574
+ daemon,
2575
+ configPath,
2576
+ fromFlag: true
2577
+ };
2578
+ }
2579
+ }
2580
+ const file = facts.readFile(configPath);
2581
+ if (file.kind === "unreadable") return { kind: "unknown", why: `${configPath} unreadable` };
2582
+ if (file.kind === "read") {
2583
+ let parsed;
2584
+ try {
2585
+ parsed = JSON.parse(file.text);
2586
+ } catch {
2587
+ return { kind: "unknown", why: `${configPath} is not JSON` };
2588
+ }
2589
+ const configured = typeof parsed === "object" && parsed !== null && "shutdown-timeout" in parsed ? parsed["shutdown-timeout"] : void 0;
2590
+ if (configured !== void 0) {
2591
+ const value = seconds(configured);
2592
+ return value === null ? { kind: "unknown", why: `shutdown-timeout in ${configPath} unreadable` } : {
2593
+ kind: "observed",
2594
+ seconds: value,
2595
+ source: configPath,
2596
+ daemon,
2597
+ configPath,
2598
+ fromFlag: false
2599
+ };
2600
+ }
2601
+ }
2602
+ if (argv === null && !managed) {
2603
+ return {
2604
+ kind: "unknown",
2605
+ why: `no dockerd process observed here \xB7 not set in ${configPath}`
2606
+ };
2607
+ }
2608
+ return {
2609
+ kind: "observed",
2610
+ seconds: DOCKERD_DEFAULT_SHUTDOWN_TIMEOUT_SECONDS,
2611
+ source: `dockerd default \xB7 not set in ${configPath}`,
2612
+ daemon,
2613
+ configPath,
2614
+ fromFlag: false
2615
+ };
2616
+ };
2617
+ var restartWords = (daemon) => {
2618
+ switch (daemon) {
2619
+ case "desktop":
2620
+ return "restart Docker Desktop";
2621
+ case "orbstack":
2622
+ return "restart OrbStack";
2623
+ case "rootless":
2624
+ return "systemctl --user restart docker";
2625
+ case "dockerd":
2626
+ return "restart dockerd";
2627
+ }
2628
+ };
2629
+ var raiseShutdownTimeoutWords = (reading, grace) => reading.fromFlag ? `raise dockerd --shutdown-timeout to ${grace}, then ${restartWords(reading.daemon)}` : `set "shutdown-timeout": ${grace} in ${reading.configPath}, then ${restartWords(reading.daemon)}`;
2630
+ var dockerShutdownCheck = (reading, grace = DOCKER_CAPTURE_STOP_GRACE_SECONDS) => {
2631
+ if (reading.kind === "unknown") {
2632
+ return {
2633
+ label: "docker",
2634
+ state: "todo",
2635
+ detail: `shutdown-timeout not observed \xB7 ${reading.why}`,
2636
+ fix: null
2637
+ };
2638
+ }
2639
+ if (reading.seconds >= grace) {
2640
+ return {
2641
+ label: "docker",
2642
+ state: "ok",
2643
+ detail: `shutdown-timeout ${reading.seconds} s \xB7 ${reading.source} \xB7 covers the ${grace} s capture grace`,
2644
+ fix: null
2645
+ };
2646
+ }
2647
+ return {
2648
+ label: "docker",
2649
+ state: "todo",
2650
+ detail: `shutdown-timeout ${reading.seconds} s \xB7 ${reading.source} \xB7 below the ${grace} s capture grace`,
2651
+ fix: raiseShutdownTimeoutWords(reading, grace)
2652
+ };
2653
+ };
2654
+ var dockerShutdownSetupLine = (reading, grace = DOCKER_CAPTURE_STOP_GRACE_SECONDS) => {
2655
+ if (reading.kind === "unknown") {
2656
+ return `Docker shutdown-timeout not observed (${reading.why}); a daemon shutdown below ${grace} s kills capture workspaces before they save. See docs/SELF-HOSTING.md.`;
2657
+ }
2658
+ if (reading.seconds >= grace) return null;
2659
+ return `Docker shutdown-timeout is ${reading.seconds} s (${reading.source}), below the ${grace} s capture grace: a host restart or daemon stop kills capture workspaces after ${reading.seconds} s, before they save. To raise it: ${raiseShutdownTimeoutWords(reading, grace)}.`;
2660
+ };
2661
+ var parseDockerInfo = (stdout) => {
2662
+ let parsed;
2663
+ try {
2664
+ parsed = JSON.parse(stdout);
2665
+ } catch {
2666
+ return null;
2667
+ }
2668
+ if (typeof parsed !== "object" || parsed === null || !("OperatingSystem" in parsed)) return null;
2669
+ const operatingSystem = parsed.OperatingSystem;
2670
+ if (typeof operatingSystem !== "string") return null;
2671
+ const options = "SecurityOptions" in parsed ? parsed.SecurityOptions : [];
2672
+ return {
2673
+ operatingSystem,
2674
+ securityOptions: Array.isArray(options) ? options.filter((option) => typeof option === "string") : []
2675
+ };
2676
+ };
2677
+ var readHostFile = (file) => {
2678
+ try {
2679
+ return { kind: "read", text: fs5.readFileSync(file, "utf8") };
2680
+ } catch (cause) {
2681
+ return cause instanceof Error && "code" in cause && cause.code === "ENOENT" ? { kind: "absent" } : { kind: "unreadable" };
2682
+ }
2683
+ };
2684
+ var hostProcView = {
2685
+ list: (dir) => {
2686
+ try {
2687
+ return fs5.readdirSync(dir);
2688
+ } catch {
2689
+ return null;
2690
+ }
2691
+ },
2692
+ read: (file) => {
2693
+ try {
2694
+ return fs5.readFileSync(file, "utf8");
2695
+ } catch {
2696
+ return null;
2697
+ }
2698
+ },
2699
+ link: (file) => {
2700
+ try {
2701
+ return fs5.readlinkSync(file);
2702
+ } catch {
2703
+ return null;
2704
+ }
2705
+ }
2706
+ };
2707
+ var DOCKER_DEFAULT_HOST = "unix:///var/run/docker.sock";
2708
+ var socketPath = (file) => file.replace(/^\/var\/run\//, "/run/");
2709
+ var unixSocketOf = (host) => host.startsWith("unix://") && host.length > "unix://".length ? socketPath(host.slice("unix://".length)) : null;
2710
+ var hostFlags = (argv) => {
2711
+ const hosts = [];
2712
+ for (let index = 1; index < argv.length; index += 1) {
2713
+ const arg = argv[index] ?? "";
2714
+ for (const name of ["-H", "--host"]) {
2715
+ if (arg === name) hosts.push(argv[index + 1] ?? "");
2716
+ else if (arg.startsWith(`${name}=`)) hosts.push(arg.slice(name.length + 1));
2717
+ }
2718
+ }
2719
+ return hosts;
2720
+ };
2721
+ var listeningInodes = (table, socket) => {
2722
+ const inodes = /* @__PURE__ */ new Set();
2723
+ for (const line of table.split("\n").slice(1)) {
2724
+ const fields = line.trim().split(/\s+/);
2725
+ const flags = fields[3];
2726
+ const inode = fields[6];
2727
+ const bound = fields.slice(7).join(" ");
2728
+ if (flags === void 0 || inode === void 0 || bound === "") continue;
2729
+ if ((Number.parseInt(flags, 16) & 65536) === 0) continue;
2730
+ if (socketPath(bound) === socket) inodes.add(inode);
2731
+ }
2732
+ return inodes;
2733
+ };
2734
+ var observeDockerd = (proc, host) => {
2735
+ const daemons = [];
2736
+ for (const entry of proc.list("/proc") ?? []) {
2737
+ if (!/^\d+$/.test(entry)) continue;
2738
+ const cmdline = proc.read(`/proc/${entry}/cmdline`);
2739
+ if (cmdline === null) continue;
2740
+ const argv = cmdline.split("\0").filter((arg) => arg !== "");
2741
+ if (path5.basename(argv[0] ?? "") === "dockerd") daemons.push({ pid: entry, argv });
2742
+ }
2743
+ if (daemons.length === 0) return { kind: "none" };
2744
+ const socket = unixSocketOf(host);
2745
+ if (socket === null) {
2746
+ return {
2747
+ kind: "unresolved",
2748
+ why: `docker talks to ${host} \xB7 not a socket on this machine`
2749
+ };
2750
+ }
2751
+ const selfTable = proc.read("/proc/self/net/unix");
2752
+ const seenHere = selfTable === null ? /* @__PURE__ */ new Set() : listeningInodes(selfTable, socket);
2753
+ const candidates = [];
2754
+ for (const daemon of daemons) {
2755
+ const hosts = hostFlags(daemon.argv);
2756
+ const named = hosts.some((value) => unixSocketOf(value) === socket);
2757
+ const activated = hosts.some((value) => value.startsWith("fd://"));
2758
+ if (hosts.length > 0 && !named && !activated) continue;
2759
+ const table = proc.read(`/proc/${daemon.pid}/net/unix`);
2760
+ const listening = table === null ? null : listeningInodes(table, socket);
2761
+ if (listening !== null) {
2762
+ if (listening.size === 0) continue;
2763
+ if (seenHere.size > 0 && ![...listening].some((inode) => seenHere.has(inode))) continue;
2764
+ }
2765
+ const fds = proc.list(`/proc/${daemon.pid}/fd`);
2766
+ const held = fds === null || listening === null ? null : fds.some((fd) => {
2767
+ const target = proc.link(`/proc/${daemon.pid}/fd/${fd}`);
2768
+ const inode = target?.match(/^socket:\[(\d+)\]$/)?.[1];
2769
+ return inode !== void 0 && listening.has(inode);
2770
+ });
2771
+ if (held === false) continue;
2772
+ const nsPids = proc.read(`/proc/${daemon.pid}/status`)?.split("\n").find((line) => line.startsWith("NSpid:"))?.slice("NSpid:".length).trim().split(/\s+/);
2773
+ candidates.push({
2774
+ ...daemon,
2775
+ owns: held === true,
2776
+ named,
2777
+ nested: nsPids !== void 0 && nsPids.length > 1
2778
+ });
2779
+ }
2780
+ const narrowed = [
2781
+ (candidate) => candidate.owns,
2782
+ (candidate) => candidate.named,
2783
+ (candidate) => !candidate.nested
2784
+ ].reduce((left, prefer) => {
2785
+ const preferred = left.filter(prefer);
2786
+ return preferred.length > 0 ? preferred : left;
2787
+ }, candidates);
2788
+ const [only, ...others] = narrowed;
2789
+ if (only !== void 0 && others.length === 0) {
2790
+ return { kind: "observed", pid: only.pid, argv: only.argv };
2791
+ }
2792
+ const seen = `${daemons.length} dockerd process${daemons.length === 1 ? "" : "es"}`;
2793
+ return {
2794
+ kind: "unresolved",
2795
+ why: only === void 0 ? `${seen} \xB7 none observed serving ${socket}` : `${seen} \xB7 could not tell which serves ${socket}`
2796
+ };
2797
+ };
2798
+ var hostDockerEndpoint = () => {
2799
+ const fromEnv = process.env["DOCKER_HOST"];
2800
+ if (fromEnv !== void 0 && fromEnv.trim() !== "") return fromEnv.trim();
2801
+ const context = spawnSync2(
2802
+ "docker",
2803
+ ["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"],
2804
+ {
2805
+ encoding: "utf8",
2806
+ timeout: 3e3,
2807
+ stdio: ["ignore", "pipe", "ignore"]
2808
+ }
2809
+ );
2810
+ const endpoint = context.status === 0 ? context.stdout.trim() : "";
2811
+ return endpoint === "" ? DOCKER_DEFAULT_HOST : endpoint;
2812
+ };
2813
+ var dockerdFactsOf = (observation) => {
2814
+ switch (observation.kind) {
2815
+ case "observed":
2816
+ return { dockerdArgv: observation.argv, dockerdUnresolved: null };
2817
+ case "none":
2818
+ return { dockerdArgv: null, dockerdUnresolved: null };
2819
+ case "unresolved":
2820
+ return { dockerdArgv: null, dockerdUnresolved: observation.why };
2821
+ }
2822
+ };
2823
+ var hostDockerDaemonFacts = (infoStdout) => ({
2824
+ info: infoStdout === null ? null : parseDockerInfo(infoStdout),
2825
+ ...dockerdFactsOf(observeDockerd(hostProcView, hostDockerEndpoint())),
2826
+ readFile: readHostFile,
2827
+ home: os3.homedir(),
2828
+ xdgConfigHome: process.env["XDG_CONFIG_HOME"] ?? null
2829
+ });
2830
+ var observeHostShutdownTimeout = () => {
2831
+ const info = spawnSync2("docker", ["info", "--format", "{{json .}}"], {
2832
+ encoding: "utf8",
2833
+ timeout: 3e3,
2834
+ stdio: ["ignore", "pipe", "ignore"]
2835
+ });
2836
+ return readShutdownTimeout(hostDockerDaemonFacts(info.status === 0 ? info.stdout : null));
2837
+ };
2838
+
2839
+ // src/doctor.ts
2510
2840
  var TIMEOUT_MS = 3e3;
2511
2841
  var MARKS = { ok: "\u2713", todo: "\u25CB", failed: "\u2717" };
2512
2842
  var LABEL_WIDTH = 11;
@@ -2721,13 +3051,16 @@ var runChecks = async (config, probes) => {
2721
3051
  const machine = signedIn ? await getJson(config, "/machine") : null;
2722
3052
  const exposure = machine === null || machine.value === null ? void 0 : machine.value.exposure;
2723
3053
  checks.push(exposure === void 0 ? notChecked("exposure") : exposureCheck(exposure));
3054
+ if (probes.dockerShutdown !== void 0 && probes.onPath("docker")) {
3055
+ checks.push(dockerShutdownCheck(probes.dockerShutdown()));
3056
+ }
2724
3057
  return checks;
2725
3058
  };
2726
3059
  var onPath = (command) => {
2727
- for (const directory of (process.env["PATH"] ?? "").split(path5.delimiter)) {
3060
+ for (const directory of (process.env["PATH"] ?? "").split(path6.delimiter)) {
2728
3061
  if (directory === "") continue;
2729
3062
  try {
2730
- fs5.accessSync(path5.join(directory, command), fs5.constants.X_OK);
3063
+ fs6.accessSync(path6.join(directory, command), fs6.constants.X_OK);
2731
3064
  return true;
2732
3065
  } catch {
2733
3066
  }
@@ -2740,15 +3073,20 @@ var paintMark = (state, mark) => {
2740
3073
  return `\x1B[${code}m${mark}\x1B[0m`;
2741
3074
  };
2742
3075
  var doctorCommand = async (config, localCredential2, claudeGrant2) => {
2743
- const checks = await runChecks(config, { localCredential: localCredential2, claudeGrant: claudeGrant2, onPath });
3076
+ const checks = await runChecks(config, {
3077
+ localCredential: localCredential2,
3078
+ claudeGrant: claudeGrant2,
3079
+ onPath,
3080
+ dockerShutdown: observeHostShutdownTimeout
3081
+ });
2744
3082
  for (const check of checks) process.stdout.write(`${formatCheck(check, paintMark)}
2745
3083
  `);
2746
3084
  if (checks.some((check) => check.state === "failed")) process.exitCode = 1;
2747
3085
  };
2748
3086
 
2749
3087
  // src/dotfiles.ts
2750
- import * as fs6 from "node:fs";
2751
- import * as path6 from "node:path";
3088
+ import * as fs7 from "node:fs";
3089
+ import * as path7 from "node:path";
2752
3090
  var DOTFILE_CANDIDATES = [
2753
3091
  {
2754
3092
  group: "shell",
@@ -2791,7 +3129,7 @@ var DOTFILE_CANDIDATES = [
2791
3129
  var MAX_FILE_BYTES = 1024 * 1024;
2792
3130
  var statFile = (home, relative4) => {
2793
3131
  try {
2794
- const stat = fs6.statSync(path6.join(home, relative4));
3132
+ const stat = fs7.statSync(path7.join(home, relative4));
2795
3133
  return stat.isFile() ? stat : null;
2796
3134
  } catch {
2797
3135
  return null;
@@ -2804,9 +3142,9 @@ var scanDotfileCandidates = (home) => DOTFILE_CANDIDATES.flatMap(
2804
3142
  })
2805
3143
  );
2806
3144
  var homeRelativePath = (home, requested) => {
2807
- const relative4 = path6.relative(home, path6.resolve(home, requested));
2808
- const outside = relative4 === "" || relative4 === ".." || relative4.startsWith(`..${path6.sep}`) || path6.isAbsolute(relative4);
2809
- return outside ? null : relative4.split(path6.sep).join("/");
3145
+ const relative4 = path7.relative(home, path7.resolve(home, requested));
3146
+ const outside = relative4 === "" || relative4 === ".." || relative4.startsWith(`..${path7.sep}`) || path7.isAbsolute(relative4);
3147
+ return outside ? null : relative4.split(path7.sep).join("/");
2810
3148
  };
2811
3149
  var readSyncFiles = (home, paths_) => {
2812
3150
  const files = [];
@@ -2826,7 +3164,7 @@ var readSyncFiles = (home, paths_) => {
2826
3164
  }
2827
3165
  files.push({
2828
3166
  path: relative4,
2829
- contentsBase64: fs6.readFileSync(path6.join(home, relative4)).toString("base64"),
3167
+ contentsBase64: fs7.readFileSync(path7.join(home, relative4)).toString("base64"),
2830
3168
  // eslint-disable-next-line no-bitwise -- permission bits are the point
2831
3169
  mode: (stat.mode & 511).toString(8)
2832
3170
  });
@@ -2925,10 +3263,10 @@ var parseGitAuthorArgs = (args) => {
2925
3263
  var gitAuthorLine = (author) => `${author.name} <${author.email}> \xB7 ${author.source === "setting" ? "your setting" : "your account's name and email"}`;
2926
3264
 
2927
3265
  // src/landing.ts
2928
- import { spawnSync as spawnSync2 } from "node:child_process";
2929
- import * as fs7 from "node:fs";
2930
- import * as os3 from "node:os";
2931
- import * as path7 from "node:path";
3266
+ import { spawnSync as spawnSync3 } from "node:child_process";
3267
+ import * as fs8 from "node:fs";
3268
+ import * as os4 from "node:os";
3269
+ import * as path8 from "node:path";
2932
3270
  var paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
2933
3271
  var dim = paint("2");
2934
3272
  var green = paint("32");
@@ -3143,7 +3481,7 @@ var landCommand = async (api2, args) => {
3143
3481
  if (!landingSucceeded(report)) process.exitCode = 1;
3144
3482
  };
3145
3483
  var git = (cwd, args) => {
3146
- const result = spawnSync2("git", args, { cwd, encoding: "utf8" });
3484
+ const result = spawnSync3("git", args, { cwd, encoding: "utf8" });
3147
3485
  return {
3148
3486
  status: result.status ?? 1,
3149
3487
  stdout: result.stdout ?? "",
@@ -3166,9 +3504,9 @@ var gitRemotes = (cwd) => {
3166
3504
  var remoteForOrigin = (remotes, originUrl, cwd) => {
3167
3505
  if (originUrl === null) return null;
3168
3506
  const origin = normalizeRemoteUrl(originUrl);
3169
- const originPath = origin === null ? path7.resolve(originUrl.replace(/^file:\/\//, "")) : null;
3507
+ const originPath = origin === null ? path8.resolve(originUrl.replace(/^file:\/\//, "")) : null;
3170
3508
  return remotes.find(
3171
- (remote) => origin === null ? path7.resolve(cwd, remote.url.replace(/^file:\/\//, "")) === originPath : normalizeRemoteUrl(remote.url) === origin
3509
+ (remote) => origin === null ? path8.resolve(cwd, remote.url.replace(/^file:\/\//, "")) === originPath : normalizeRemoteUrl(remote.url) === origin
3172
3510
  ) ?? null;
3173
3511
  };
3174
3512
  var LOG_LINES = 10;
@@ -3191,10 +3529,10 @@ var fetchBundle = (cwd, bundle) => {
3191
3529
  const before = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
3192
3530
  const previous = before.status === 0 ? before.stdout.trim() : null;
3193
3531
  if (previous !== bundle.tip) {
3194
- const dir = fs7.mkdtempSync(path7.join(os3.tmpdir(), "mend-pull-"));
3532
+ const dir = fs8.mkdtempSync(path8.join(os4.tmpdir(), "mend-pull-"));
3195
3533
  try {
3196
- const file = path7.join(dir, "change.bundle");
3197
- fs7.writeFileSync(file, bundle.bytes, { mode: 384 });
3534
+ const file = path8.join(dir, "change.bundle");
3535
+ fs8.writeFileSync(file, bundle.bytes, { mode: 384 });
3198
3536
  const fetched = git(cwd, [
3199
3537
  "fetch",
3200
3538
  "--no-tags",
@@ -3204,7 +3542,7 @@ var fetchBundle = (cwd, bundle) => {
3204
3542
  ]);
3205
3543
  if (fetched.status !== 0) return refusedWith(gitWords(fetched));
3206
3544
  } finally {
3207
- fs7.rmSync(dir, { recursive: true, force: true });
3545
+ fs8.rmSync(dir, { recursive: true, force: true });
3208
3546
  }
3209
3547
  }
3210
3548
  const after = git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).stdout.trim();
@@ -3321,9 +3659,81 @@ var pullCommand = async (api2, download2, args, cwd = process.cwd()) => {
3321
3659
  for (const line of fetchedLines(fetched)) say(line);
3322
3660
  };
3323
3661
 
3662
+ // src/launch-follow.ts
3663
+ var SETTLED = /* @__PURE__ */ new Set(["completed", "failed", "stopped", "stopping"]);
3664
+ var agentRuns = (agent) => agent !== null && agent.kind !== "agent-protocol" && agent.exitedAt === null && agent.status === "running" && agent.sealantSessionId !== null;
3665
+ var startingLineOf = (session) => {
3666
+ const summary = session.summary?.trim() ?? "";
3667
+ if (summary === "") return session.status;
3668
+ if (/^[a-z][a-z -]*·/u.test(summary)) return summary;
3669
+ return `${session.status} \xB7 ${summary}`;
3670
+ };
3671
+ var refusalOf = (error) => error instanceof MendRequestError && error.kind === "http" && (error.status === null || !GATEWAY_STATUSES.has(error.status)) ? error.message : null;
3672
+ var followStart = async (options) => {
3673
+ const pollMs = options.pollMs ?? 2e3;
3674
+ const unreachableAfterMs = options.unreachableAfterMs ?? 6e4;
3675
+ const launch2 = {
3676
+ state: options.start === null ? { kind: "no-answer" } : { kind: "pending" }
3677
+ };
3678
+ const start = options.start;
3679
+ const launched = (async () => {
3680
+ if (start === null) return;
3681
+ try {
3682
+ launch2.state = { kind: "answered", session: await start };
3683
+ } catch (error) {
3684
+ const refusal = refusalOf(error);
3685
+ launch2.state = refusal === null ? { kind: "no-answer" } : { kind: "refused", message: refusal };
3686
+ }
3687
+ })();
3688
+ let lastLine = null;
3689
+ let failingSince = null;
3690
+ let seenStarting = false;
3691
+ for (; ; ) {
3692
+ if (launch2.state.kind === "pending") await Promise.race([launched, options.sleep(pollMs)]);
3693
+ const state = launch2.state;
3694
+ if (state.kind === "refused") return { kind: "refused", message: state.message };
3695
+ if (state.kind === "answered" && state.session.status === "running") {
3696
+ return { kind: "live", session: state.session };
3697
+ }
3698
+ let detail;
3699
+ try {
3700
+ detail = await options.read();
3701
+ } catch (error) {
3702
+ const refusal = refusalOf(error);
3703
+ if (refusal !== null) return { kind: "refused", message: refusal };
3704
+ failingSince ??= options.now();
3705
+ if (state.kind !== "pending" && options.now() - failingSince >= unreachableAfterMs) {
3706
+ return {
3707
+ kind: "unreachable",
3708
+ message: error instanceof Error ? error.message : String(error)
3709
+ };
3710
+ }
3711
+ if (state.kind !== "pending") await options.sleep(pollMs);
3712
+ continue;
3713
+ }
3714
+ failingSince = null;
3715
+ const { session, currentAgent } = detail;
3716
+ if (agentRuns(currentAgent)) return { kind: "live", session };
3717
+ if (SETTLED.has(session.status)) {
3718
+ if (seenStarting || state.kind !== "pending") return { kind: "settled", session };
3719
+ } else {
3720
+ seenStarting = true;
3721
+ }
3722
+ if (currentAgent === null && state.kind === "answered" && state.session.status === "running" && session.status === "running") {
3723
+ return { kind: "live", session };
3724
+ }
3725
+ const line = startingLineOf(session);
3726
+ if (line !== lastLine) {
3727
+ lastLine = line;
3728
+ options.onLine(line);
3729
+ }
3730
+ if (state.kind !== "pending") await options.sleep(pollMs);
3731
+ }
3732
+ };
3733
+
3324
3734
  // src/login.ts
3325
3735
  import { spawn as spawn2 } from "node:child_process";
3326
- import * as os4 from "node:os";
3736
+ import * as os5 from "node:os";
3327
3737
 
3328
3738
  // src/pair.ts
3329
3739
  var paint2 = (code) => (text) => process.stdout.isTTY === true ? `\x1B[${code}m${text}\x1B[0m` : text;
@@ -3492,8 +3902,8 @@ var post = async (url, body) => {
3492
3902
  };
3493
3903
  var retryAfterSecondsOf = (json2) => {
3494
3904
  if (typeof json2 === "object" && json2 !== null && "retryAfterSeconds" in json2) {
3495
- const seconds = Number(json2.retryAfterSeconds);
3496
- if (Number.isFinite(seconds) && seconds > 0) return seconds;
3905
+ const seconds2 = Number(json2.retryAfterSeconds);
3906
+ if (Number.isFinite(seconds2) && seconds2 > 0) return seconds2;
3497
3907
  }
3498
3908
  return 5;
3499
3909
  };
@@ -3515,7 +3925,7 @@ var resolveServerUrl = async (args, deps) => {
3515
3925
  };
3516
3926
  var loginCommand = async (args, deps) => {
3517
3927
  const base = await resolveServerUrl(args, deps);
3518
- const started = await post(`${base}/api/cli/auth`, { name: os4.hostname() });
3928
+ const started = await post(`${base}/api/cli/auth`, { name: os5.hostname() });
3519
3929
  if (started.status === 429) {
3520
3930
  return fail3(
3521
3931
  `the server asked for a pause \u2014 try again in ${retryAfterSecondsOf(started.json)}s`
@@ -3566,8 +3976,8 @@ var loginCommand = async (args, deps) => {
3566
3976
  };
3567
3977
 
3568
3978
  // src/organization.ts
3569
- import * as fs8 from "node:fs";
3570
- import * as path8 from "node:path";
3979
+ import * as fs9 from "node:fs";
3980
+ import * as path9 from "node:path";
3571
3981
  var paint4 = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
3572
3982
  var dim4 = paint4("2");
3573
3983
  var green4 = paint4("32");
@@ -3631,7 +4041,7 @@ var scanFolder = (dir) => {
3631
4041
  const files = [];
3632
4042
  const skipped = [];
3633
4043
  const walk = (relative4) => {
3634
- const entries = fs8.readdirSync(path8.join(dir, relative4), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
4044
+ const entries = fs9.readdirSync(path9.join(dir, relative4), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
3635
4045
  for (const entry of entries) {
3636
4046
  const child = relative4 === "" ? entry.name : `${relative4}/${entry.name}`;
3637
4047
  if (entry.isSymbolicLink()) {
@@ -3640,7 +4050,7 @@ var scanFolder = (dir) => {
3640
4050
  if (SKIPPED_DIRECTORIES.has(entry.name)) skipped.push({ path: child, reason: "skipped" });
3641
4051
  else walk(child);
3642
4052
  } else if (entry.isFile()) {
3643
- const size = fs8.statSync(path8.join(dir, child)).size;
4053
+ const size = fs9.statSync(path9.join(dir, child)).size;
3644
4054
  if (size > FOLDER_FILE_BYTES) skipped.push({ path: child, reason: "over 1 MiB" });
3645
4055
  else files.push({ path: child, size });
3646
4056
  }
@@ -3693,7 +4103,7 @@ var folderCommand = async (api2, args) => {
3693
4103
  }
3694
4104
  case "push": {
3695
4105
  if (dir === void 0) return fail4("usage: mend folder push <name> <dir> [--replace]");
3696
- if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) {
4106
+ if (!fs9.existsSync(dir) || !fs9.statSync(dir).isDirectory()) {
3697
4107
  return fail4(`${dir} is not a directory`);
3698
4108
  }
3699
4109
  const folder = await folderNamed(api2, name);
@@ -3708,7 +4118,7 @@ var folderCommand = async (api2, args) => {
3708
4118
  files: batch.map((file) => ({
3709
4119
  path: file.path,
3710
4120
  // Read one batch at a time, so a large tree never sits in memory at once.
3711
- contentsBase64: fs8.readFileSync(path8.join(dir, file.path)).toString("base64")
4121
+ contentsBase64: fs9.readFileSync(path9.join(dir, file.path)).toString("base64")
3712
4122
  })),
3713
4123
  // --replace empties the folder with the first batch; the rest add to it.
3714
4124
  merge: !(replace && index === 0)
@@ -3892,8 +4302,8 @@ var operatorCommand = async (api2, tryApi, baseUrl, args) => {
3892
4302
  import { createHmac, randomBytes } from "node:crypto";
3893
4303
  import { readFile } from "node:fs/promises";
3894
4304
  import * as net2 from "node:net";
3895
- import * as os6 from "node:os";
3896
- import * as path10 from "node:path";
4305
+ import * as os7 from "node:os";
4306
+ import * as path11 from "node:path";
3897
4307
 
3898
4308
  // src/server-docker-volumes.ts
3899
4309
  import { createHash as createHash2 } from "node:crypto";
@@ -4111,9 +4521,9 @@ var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "v
4111
4521
 
4112
4522
  // src/server-store.ts
4113
4523
  import { randomUUID } from "node:crypto";
4114
- import * as fs9 from "node:fs";
4115
- import * as os5 from "node:os";
4116
- import * as path9 from "node:path";
4524
+ import * as fs10 from "node:fs";
4525
+ import * as os6 from "node:os";
4526
+ import * as path10 from "node:path";
4117
4527
  var ServerStoreError = class extends Error {
4118
4528
  _tag = "ServerStoreError";
4119
4529
  };
@@ -4134,28 +4544,28 @@ var attempt = (operation) => {
4134
4544
  var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
4135
4545
  var readOptional = (file) => {
4136
4546
  try {
4137
- return fs9.readFileSync(file, "utf8");
4547
+ return fs10.readFileSync(file, "utf8");
4138
4548
  } catch (cause) {
4139
4549
  if (hasCode(cause, "ENOENT")) return null;
4140
4550
  throw cause;
4141
4551
  }
4142
4552
  };
4143
4553
  var syncDirectory = (directory) => {
4144
- const fd = fs9.openSync(directory, "r");
4554
+ const fd = fs10.openSync(directory, "r");
4145
4555
  try {
4146
- fs9.fsyncSync(fd);
4556
+ fs10.fsyncSync(fd);
4147
4557
  } finally {
4148
- fs9.closeSync(fd);
4558
+ fs10.closeSync(fd);
4149
4559
  }
4150
4560
  };
4151
4561
  var writeDurable = (file, content, mode = 384) => {
4152
- const fd = fs9.openSync(file, "wx", mode);
4562
+ const fd = fs10.openSync(file, "wx", mode);
4153
4563
  try {
4154
- fs9.writeFileSync(fd, content, "utf8");
4155
- fs9.fchmodSync(fd, mode);
4156
- fs9.fsyncSync(fd);
4564
+ fs10.writeFileSync(fd, content, "utf8");
4565
+ fs10.fchmodSync(fd, mode);
4566
+ fs10.fsyncSync(fd);
4157
4567
  } finally {
4158
- fs9.closeSync(fd);
4568
+ fs10.closeSync(fd);
4159
4569
  }
4160
4570
  };
4161
4571
  var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
@@ -4175,11 +4585,11 @@ var parseLockOwner = (raw) => {
4175
4585
  };
4176
4586
  var describeOwner = (lockDir) => {
4177
4587
  try {
4178
- const raw = JSON.parse(fs9.readFileSync(path9.join(lockDir, "owner.json"), "utf8"));
4588
+ const raw = JSON.parse(fs10.readFileSync(path10.join(lockDir, "owner.json"), "utf8"));
4179
4589
  const owner = parseLockOwner(raw);
4180
4590
  if (owner === null) return "owner metadata is invalid";
4181
4591
  const label = `PID ${owner.pid} on ${owner.hostname}`;
4182
- if (owner.hostname !== os5.hostname()) return label;
4592
+ if (owner.hostname !== os6.hostname()) return label;
4183
4593
  try {
4184
4594
  process.kill(owner.pid, 0);
4185
4595
  return `${label} is still live`;
@@ -4195,28 +4605,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
4195
4605
  );
4196
4606
  var acquireLock = (configDir, create) => {
4197
4607
  if (create) {
4198
- fs9.mkdirSync(configDir, { recursive: true, mode: 448 });
4199
- fs9.chmodSync(configDir, 448);
4200
- } else if (!fs9.existsSync(configDir)) {
4608
+ fs10.mkdirSync(configDir, { recursive: true, mode: 448 });
4609
+ fs10.chmodSync(configDir, 448);
4610
+ } else if (!fs10.existsSync(configDir)) {
4201
4611
  throw new ServerStoreError(
4202
4612
  "No Mend server is configured. Run mend server setup explicitly to install one."
4203
4613
  );
4204
4614
  }
4205
- const lockDir = path9.join(configDir, "server.lock");
4615
+ const lockDir = path10.join(configDir, "server.lock");
4206
4616
  try {
4207
- fs9.mkdirSync(lockDir, { mode: 448 });
4617
+ fs10.mkdirSync(lockDir, { mode: 448 });
4208
4618
  } catch (cause) {
4209
4619
  if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
4210
4620
  throw cause;
4211
4621
  }
4212
- const owned = fs9.statSync(lockDir);
4213
- const ownerFile = path9.join(lockDir, "owner.json");
4214
- const owner = `${JSON.stringify({ pid: process.pid, hostname: os5.hostname(), token: randomUUID() })}
4622
+ const owned = fs10.statSync(lockDir);
4623
+ const ownerFile = path10.join(lockDir, "owner.json");
4624
+ const owner = `${JSON.stringify({ pid: process.pid, hostname: os6.hostname(), token: randomUUID() })}
4215
4625
  `;
4216
4626
  let open = true;
4217
4627
  const assertOwned = () => {
4218
- const current = fs9.statSync(lockDir);
4219
- if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs9.readFileSync(ownerFile, "utf8") !== owner) {
4628
+ const current = fs10.statSync(lockDir);
4629
+ if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs10.readFileSync(ownerFile, "utf8") !== owner) {
4220
4630
  throw new ServerStoreError(
4221
4631
  "Server lock ownership was lost. Stop and check for another server command before retrying."
4222
4632
  );
@@ -4232,8 +4642,8 @@ var acquireLock = (configDir, create) => {
4232
4642
  release: () => {
4233
4643
  const result = attempt(() => {
4234
4644
  assertOwned();
4235
- fs9.unlinkSync(ownerFile);
4236
- fs9.rmdirSync(lockDir);
4645
+ fs10.unlinkSync(ownerFile);
4646
+ fs10.rmdirSync(lockDir);
4237
4647
  });
4238
4648
  open = false;
4239
4649
  return result;
@@ -4242,7 +4652,7 @@ var acquireLock = (configDir, create) => {
4242
4652
  };
4243
4653
  var readIdentity = (paths) => {
4244
4654
  const identity = readOptional(paths.identity);
4245
- if (identity === null && (fs9.existsSync(paths.active) || fs9.existsSync(paths.generations) && fs9.readdirSync(paths.generations).length > 0)) {
4655
+ if (identity === null && (fs10.existsSync(paths.active) || fs10.existsSync(paths.generations) && fs10.readdirSync(paths.generations).length > 0)) {
4246
4656
  throw new ServerStoreError(
4247
4657
  "Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
4248
4658
  );
@@ -4252,7 +4662,7 @@ var readIdentity = (paths) => {
4252
4662
  var readActive = (paths) => {
4253
4663
  let target;
4254
4664
  try {
4255
- target = fs9.readlinkSync(paths.active);
4665
+ target = fs10.readlinkSync(paths.active);
4256
4666
  } catch (cause) {
4257
4667
  if (hasCode(cause, "ENOENT")) return null;
4258
4668
  throw cause;
@@ -4262,13 +4672,13 @@ var readActive = (paths) => {
4262
4672
  "Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
4263
4673
  );
4264
4674
  }
4265
- const directory = path9.join(paths.configDir, target);
4675
+ const directory = path10.join(paths.configDir, target);
4266
4676
  const files = {
4267
- identity: fs9.readFileSync(path9.join(directory, fileNames.identity), "utf8"),
4268
- config: fs9.readFileSync(path9.join(directory, fileNames.config), "utf8"),
4269
- env: fs9.readFileSync(path9.join(directory, fileNames.env), "utf8"),
4270
- compose: fs9.readFileSync(path9.join(directory, fileNames.compose), "utf8"),
4271
- postgresInit: fs9.readFileSync(path9.join(directory, fileNames.postgresInit), "utf8")
4677
+ identity: fs10.readFileSync(path10.join(directory, fileNames.identity), "utf8"),
4678
+ config: fs10.readFileSync(path10.join(directory, fileNames.config), "utf8"),
4679
+ env: fs10.readFileSync(path10.join(directory, fileNames.env), "utf8"),
4680
+ compose: fs10.readFileSync(path10.join(directory, fileNames.compose), "utf8"),
4681
+ postgresInit: fs10.readFileSync(path10.join(directory, fileNames.postgresInit), "utf8")
4272
4682
  };
4273
4683
  if (readIdentity(paths) !== files.identity) {
4274
4684
  throw new ServerStoreError(
@@ -4278,20 +4688,20 @@ var readActive = (paths) => {
4278
4688
  return { directory, files };
4279
4689
  };
4280
4690
  var publishIdentity = (paths, identity) => {
4281
- const temporary = path9.join(paths.configDir, `.identity-${randomUUID()}`);
4691
+ const temporary = path10.join(paths.configDir, `.identity-${randomUUID()}`);
4282
4692
  writeDurable(temporary, identity);
4283
- fs9.linkSync(temporary, paths.identity);
4693
+ fs10.linkSync(temporary, paths.identity);
4284
4694
  syncDirectory(paths.configDir);
4285
- fs9.unlinkSync(temporary);
4695
+ fs10.unlinkSync(temporary);
4286
4696
  };
4287
4697
  var prepareGeneration = (paths, files) => {
4288
- fs9.mkdirSync(paths.generations, { recursive: true, mode: 448 });
4698
+ fs10.mkdirSync(paths.generations, { recursive: true, mode: 448 });
4289
4699
  const generationName = `gen-${randomUUID()}`;
4290
- const directory = path9.join(paths.generations, generationName);
4291
- fs9.mkdirSync(directory, { mode: 448 });
4700
+ const directory = path10.join(paths.generations, generationName);
4701
+ fs10.mkdirSync(directory, { mode: 448 });
4292
4702
  for (const key of fileKeys) {
4293
4703
  writeDurable(
4294
- path9.join(directory, fileNames[key]),
4704
+ path10.join(directory, fileNames[key]),
4295
4705
  files[key],
4296
4706
  key === "postgresInit" ? 493 : 384
4297
4707
  );
@@ -4302,18 +4712,18 @@ var prepareGeneration = (paths, files) => {
4302
4712
  return { directory, files };
4303
4713
  };
4304
4714
  var activateGeneration = (paths, generation) => {
4305
- const relative4 = path9.relative(paths.configDir, generation.directory);
4306
- if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative4) || readIdentity(paths) !== fs9.readFileSync(path9.join(generation.directory, "identity.env"), "utf8")) {
4715
+ const relative4 = path10.relative(paths.configDir, generation.directory);
4716
+ if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative4) || readIdentity(paths) !== fs10.readFileSync(path10.join(generation.directory, "identity.env"), "utf8")) {
4307
4717
  throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
4308
4718
  }
4309
4719
  for (const key of fileKeys) {
4310
- if (fs9.readFileSync(path9.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
4720
+ if (fs10.readFileSync(path10.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
4311
4721
  throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
4312
4722
  }
4313
4723
  }
4314
- const pointer = path9.join(paths.configDir, `.active-${randomUUID()}`);
4315
- fs9.symlinkSync(relative4, pointer);
4316
- fs9.renameSync(pointer, paths.active);
4724
+ const pointer = path10.join(paths.configDir, `.active-${randomUUID()}`);
4725
+ fs10.symlinkSync(relative4, pointer);
4726
+ fs10.renameSync(pointer, paths.active);
4317
4727
  syncDirectory(paths.configDir);
4318
4728
  };
4319
4729
  var prepareFiles = (paths, files) => {
@@ -4322,7 +4732,7 @@ var prepareFiles = (paths, files) => {
4322
4732
  throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
4323
4733
  }
4324
4734
  const active = readActive(paths);
4325
- if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs9.statSync(path9.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
4735
+ if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs10.statSync(path10.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
4326
4736
  return active;
4327
4737
  if (identity === null) publishIdentity(paths, files.identity);
4328
4738
  return prepareGeneration(paths, files);
@@ -4335,9 +4745,9 @@ var commitGeneration = (paths, files) => {
4335
4745
  var createStore = (configDir, lock) => {
4336
4746
  const paths = {
4337
4747
  configDir,
4338
- identity: path9.join(configDir, "identity.env"),
4339
- generations: path9.join(configDir, "generations"),
4340
- active: path9.join(configDir, "active")
4748
+ identity: path10.join(configDir, "identity.env"),
4749
+ generations: path10.join(configDir, "generations"),
4750
+ active: path10.join(configDir, "active")
4341
4751
  };
4342
4752
  const whileOwned = (operation) => attempt(() => {
4343
4753
  lock.assertOwned();
@@ -4350,13 +4760,13 @@ var createStore = (configDir, lock) => {
4350
4760
  prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
4351
4761
  activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
4352
4762
  createBackup: (previous, target) => whileOwned(() => {
4353
- const backups = path9.join(configDir, "backups");
4354
- fs9.mkdirSync(backups, { recursive: true, mode: 448 });
4355
- fs9.chmodSync(backups, 448);
4356
- const directory = path9.join(backups, `upgrade-${randomUUID()}`);
4357
- fs9.mkdirSync(directory, { mode: 448 });
4763
+ const backups = path10.join(configDir, "backups");
4764
+ fs10.mkdirSync(backups, { recursive: true, mode: 448 });
4765
+ fs10.chmodSync(backups, 448);
4766
+ const directory = path10.join(backups, `upgrade-${randomUUID()}`);
4767
+ fs10.mkdirSync(directory, { mode: 448 });
4358
4768
  writeDurable(
4359
- path9.join(directory, "recovery.json"),
4769
+ path10.join(directory, "recovery.json"),
4360
4770
  `${JSON.stringify(
4361
4771
  {
4362
4772
  previousGeneration: previous.directory,
@@ -4372,21 +4782,21 @@ var createStore = (configDir, lock) => {
4372
4782
  syncDirectory(directory);
4373
4783
  syncDirectory(backups);
4374
4784
  syncDirectory(configDir);
4375
- const partialFile = path9.join(directory, "database.sql.partial");
4785
+ const partialFile = path10.join(directory, "database.sql.partial");
4376
4786
  return {
4377
4787
  directory,
4378
4788
  partialFile,
4379
4789
  complete: () => whileOwned(() => {
4380
- if (fs9.statSync(partialFile).size === 0)
4790
+ if (fs10.statSync(partialFile).size === 0)
4381
4791
  throw new ServerStoreError("Database backup is empty; refusing target startup.");
4382
- const fd = fs9.openSync(partialFile, "r");
4792
+ const fd = fs10.openSync(partialFile, "r");
4383
4793
  try {
4384
- fs9.fchmodSync(fd, 384);
4385
- fs9.fsyncSync(fd);
4794
+ fs10.fchmodSync(fd, 384);
4795
+ fs10.fsyncSync(fd);
4386
4796
  } finally {
4387
- fs9.closeSync(fd);
4797
+ fs10.closeSync(fd);
4388
4798
  }
4389
- fs9.renameSync(partialFile, path9.join(directory, "database.sql"));
4799
+ fs10.renameSync(partialFile, path10.join(directory, "database.sql"));
4390
4800
  syncDirectory(directory);
4391
4801
  })
4392
4802
  };
@@ -4395,7 +4805,7 @@ var createStore = (configDir, lock) => {
4395
4805
  };
4396
4806
  var refuseFlatLayout = (configDir) => {
4397
4807
  if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
4398
- (name) => fs9.existsSync(path9.join(configDir, name))
4808
+ (name) => fs10.existsSync(path10.join(configDir, name))
4399
4809
  )) {
4400
4810
  throw new ServerStoreError(
4401
4811
  "Unreleased flat server configuration found. Preserve its credentials and volumes; migrate it to the generation layout before retrying. Setup will not replace this identity."
@@ -4403,7 +4813,7 @@ var refuseFlatLayout = (configDir) => {
4403
4813
  }
4404
4814
  };
4405
4815
  var withServerStore = async (directory, operation, options = {}) => {
4406
- const configDir = path9.resolve(directory);
4816
+ const configDir = path10.resolve(directory);
4407
4817
  let lock;
4408
4818
  let result;
4409
4819
  try {
@@ -4420,9 +4830,9 @@ var withServerStore = async (directory, operation, options = {}) => {
4420
4830
  };
4421
4831
 
4422
4832
  // src/version.ts
4423
- import * as fs10 from "node:fs";
4833
+ import * as fs11 from "node:fs";
4424
4834
  var cliVersion = () => {
4425
- const raw = fs10.readFileSync(new URL("../package.json", import.meta.url), "utf8");
4835
+ const raw = fs11.readFileSync(new URL("../package.json", import.meta.url), "utf8");
4426
4836
  const parsed = JSON.parse(raw);
4427
4837
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
4428
4838
  const version = parsed.version;
@@ -4554,21 +4964,21 @@ var parseSetupOptions = (args) => {
4554
4964
  previous.push(value);
4555
4965
  values.set(flag, previous);
4556
4966
  }
4557
- const flagValue2 = (flag) => values.get(flag)?.[0];
4558
- const version = flagValue2("--version");
4559
- const appPort = flagValue2("--port");
4560
- const sshPort = flagValue2("--ssh-port");
4967
+ const flagValue3 = (flag) => values.get(flag)?.[0];
4968
+ const version = flagValue3("--version");
4969
+ const appPort = flagValue3("--port");
4970
+ const sshPort = flagValue3("--ssh-port");
4561
4971
  const origins = values.get("--origin");
4562
4972
  return {
4563
- context: flagValue2("--context"),
4973
+ context: flagValue3("--context"),
4564
4974
  version: version === void 0 ? void 0 : parseVersion(version),
4565
- bind: flagValue2("--bind"),
4566
- url: flagValue2("--url"),
4975
+ bind: flagValue3("--bind"),
4976
+ url: flagValue3("--url"),
4567
4977
  origins: origins === void 0 ? void 0 : origins,
4568
4978
  appPort: appPort === void 0 ? void 0 : parsePort(appPort, "--port"),
4569
4979
  sshPort: sshPort === void 0 ? void 0 : parsePort(sshPort, "--ssh-port"),
4570
- dockerSocket: flagValue2("--docker-socket"),
4571
- assetsDir: flagValue2("--assets-dir"),
4980
+ dockerSocket: flagValue3("--docker-socket"),
4981
+ assetsDir: flagValue3("--assets-dir"),
4572
4982
  offline: values.has("--offline")
4573
4983
  };
4574
4984
  };
@@ -5125,7 +5535,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
5125
5535
  return version;
5126
5536
  };
5127
5537
  var parseDockerSocketPath = (socket) => {
5128
- if (!path10.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
5538
+ if (!path11.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
5129
5539
  throw setupError(
5130
5540
  "--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
5131
5541
  );
@@ -5151,8 +5561,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
5151
5561
  if (options.assetsDir !== void 0) {
5152
5562
  try {
5153
5563
  const [compose2, postgresInit2] = await Promise.all([
5154
- readFile(path10.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
5155
- readFile(path10.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
5564
+ readFile(path11.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
5565
+ readFile(path11.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
5156
5566
  ]);
5157
5567
  validateComposeAsset(compose2);
5158
5568
  validatePostgresAsset(postgresInit2);
@@ -5344,6 +5754,19 @@ var setupServer = async (args, runtime, store) => {
5344
5754
  };
5345
5755
  const assets = await resolveAssets(runtime, serverVersion, existing, store, options);
5346
5756
  const bucket = composeBucket(assets.compose);
5757
+ if (bucket !== void 0 && runtime.dockerDaemonFacts !== void 0) {
5758
+ const info = await runtime.run("docker", [
5759
+ "--context",
5760
+ selectedContext.name,
5761
+ "info",
5762
+ "--format",
5763
+ "{{json .}}"
5764
+ ]);
5765
+ const line = dockerShutdownSetupLine(
5766
+ readShutdownTimeout(runtime.dockerDaemonFacts(info.status === 0 ? info.stdout : null))
5767
+ );
5768
+ if (line !== null) runtime.writeLine(line);
5769
+ }
5347
5770
  const config = {
5348
5771
  ...configWithoutBucket,
5349
5772
  ...bucket === void 0 ? {} : { bucket }
@@ -5665,10 +6088,10 @@ var manageServer = async (command, args, runtime, store) => {
5665
6088
  };
5666
6089
  var nodeServerRuntime = () => {
5667
6090
  const environment = { ...process.env };
5668
- const home = os6.homedir();
5669
- const configHome = environment["XDG_CONFIG_HOME"] ?? path10.join(home, ".config");
6091
+ const home = os7.homedir();
6092
+ const configHome = environment["XDG_CONFIG_HOME"] ?? path11.join(home, ".config");
5670
6093
  return {
5671
- configDir: path10.join(configHome, "mend"),
6094
+ configDir: path11.join(configHome, "mend"),
5672
6095
  platform: process.platform,
5673
6096
  cliVersion: cliVersion(),
5674
6097
  run: (command, args, options) => runServerProcess(command, args, environment, options),
@@ -5687,7 +6110,8 @@ var nodeServerRuntime = () => {
5687
6110
  randomBytes,
5688
6111
  sleep: (milliseconds) => new Promise((resolve7) => setTimeout(resolve7, milliseconds)),
5689
6112
  writeLine: (line) => process.stdout.write(`${line}
5690
- `)
6113
+ `),
6114
+ dockerDaemonFacts: hostDockerDaemonFacts
5691
6115
  };
5692
6116
  };
5693
6117
  var serverCommand = async (args, runtime = nodeServerRuntime()) => {
@@ -6120,8 +6544,8 @@ var createServiceTunnels = (options) => {
6120
6544
  };
6121
6545
 
6122
6546
  // src/skills.ts
6123
- import * as fs11 from "node:fs";
6124
- import * as path11 from "node:path";
6547
+ import * as fs12 from "node:fs";
6548
+ import * as path12 from "node:path";
6125
6549
  var DEFAULT_SKILLS_DIR = ".agents/skills";
6126
6550
  var MAX_SKILL_FILES = 64;
6127
6551
  var MAX_SKILL_FILE_BYTES = 512 * 1024;
@@ -6144,12 +6568,12 @@ var readFrontmatterDescription = (contents) => {
6144
6568
  var walkFiles = (root) => {
6145
6569
  const found = [];
6146
6570
  const visit = (dir) => {
6147
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
6571
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
6148
6572
  if (IGNORED_DIRS.has(entry.name)) continue;
6149
- const absolute = path11.join(dir, entry.name);
6573
+ const absolute = path12.join(dir, entry.name);
6150
6574
  let stat;
6151
6575
  try {
6152
- stat = fs11.statSync(absolute);
6576
+ stat = fs12.statSync(absolute);
6153
6577
  } catch {
6154
6578
  continue;
6155
6579
  }
@@ -6163,7 +6587,7 @@ var walkFiles = (root) => {
6163
6587
  var scanSkillLibrary = (root) => {
6164
6588
  let entries;
6165
6589
  try {
6166
- entries = fs11.readdirSync(root, { withFileTypes: true });
6590
+ entries = fs12.readdirSync(root, { withFileTypes: true });
6167
6591
  } catch {
6168
6592
  return { error: `${root} is not a readable directory` };
6169
6593
  }
@@ -6171,23 +6595,23 @@ var scanSkillLibrary = (root) => {
6171
6595
  const notes = [];
6172
6596
  for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
6173
6597
  if (entry.name.startsWith(".")) continue;
6174
- const skillDir = path11.join(root, entry.name);
6598
+ const skillDir = path12.join(root, entry.name);
6175
6599
  let stat;
6176
6600
  try {
6177
- stat = fs11.statSync(skillDir);
6601
+ stat = fs12.statSync(skillDir);
6178
6602
  } catch {
6179
6603
  continue;
6180
6604
  }
6181
6605
  if (!stat.isDirectory()) continue;
6182
- if (!fs11.existsSync(path11.join(skillDir, "SKILL.md"))) {
6606
+ if (!fs12.existsSync(path12.join(skillDir, "SKILL.md"))) {
6183
6607
  notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
6184
6608
  continue;
6185
6609
  }
6186
6610
  const files = [];
6187
6611
  let bytes = 0;
6188
6612
  for (const absolute of walkFiles(skillDir)) {
6189
- const relative4 = path11.relative(skillDir, absolute).split(path11.sep).join("/");
6190
- const buffer = fs11.readFileSync(absolute);
6613
+ const relative4 = path12.relative(skillDir, absolute).split(path12.sep).join("/");
6614
+ const buffer = fs12.readFileSync(absolute);
6191
6615
  if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
6192
6616
  notes.push({ skill: entry.name, message: `${relative4} is over 512KB \u2014 skipped` });
6193
6617
  continue;
@@ -6222,16 +6646,16 @@ var scanSkillLibrary = (root) => {
6222
6646
  };
6223
6647
 
6224
6648
  // src/ssh-setup.ts
6225
- import * as os8 from "node:os";
6226
- import * as path13 from "node:path";
6649
+ import * as os9 from "node:os";
6650
+ import * as path14 from "node:path";
6227
6651
 
6228
6652
  // ../../packages/workspace-ssh/src/index.ts
6229
- import { spawnSync as spawnSync3 } from "node:child_process";
6653
+ import { spawnSync as spawnSync4 } from "node:child_process";
6230
6654
  import { createHash as createHash3 } from "node:crypto";
6231
- import * as fs12 from "node:fs";
6655
+ import * as fs13 from "node:fs";
6232
6656
  import { isIP as isIP2 } from "node:net";
6233
- import * as os7 from "node:os";
6234
- import * as path12 from "node:path";
6657
+ import * as os8 from "node:os";
6658
+ import * as path13 from "node:path";
6235
6659
  var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
6236
6660
  var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
6237
6661
  var WorkspaceSshError = class extends Error {
@@ -6318,8 +6742,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
6318
6742
  var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
6319
6743
  var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
6320
6744
  var parseIdentityPath = (value) => {
6321
- const resolved = path12.resolve(
6322
- value.startsWith("~/") ? path12.join(os7.homedir(), value.slice(2)) : value
6745
+ const resolved = path13.resolve(
6746
+ value.startsWith("~/") ? path13.join(os8.homedir(), value.slice(2)) : value
6323
6747
  );
6324
6748
  if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
6325
6749
  return failure(
@@ -6465,7 +6889,7 @@ var workspaceSshPublicKeyFingerprint = (publicKey) => {
6465
6889
  var publicKeyIdentity = (publicKey) => publicKey.trim().split(/\s+/).slice(0, 2).join(" ");
6466
6890
  var agentKeys = () => {
6467
6891
  if (!process.env["SSH_AUTH_SOCK"]) return [];
6468
- const listed = spawnSync3("ssh-add", ["-L"], {
6892
+ const listed = spawnSync4("ssh-add", ["-L"], {
6469
6893
  encoding: "utf8",
6470
6894
  timeout: 3e3,
6471
6895
  stdio: ["ignore", "pipe", "pipe"],
@@ -6475,7 +6899,7 @@ var agentKeys = () => {
6475
6899
  };
6476
6900
  var readText = (file) => {
6477
6901
  try {
6478
- return success(fs12.readFileSync(file, "utf8"));
6902
+ return success(fs13.readFileSync(file, "utf8"));
6479
6903
  } catch (cause) {
6480
6904
  const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
6481
6905
  return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
@@ -6502,7 +6926,7 @@ var readRequiredKey = (keyPath, source) => {
6502
6926
  const privatePath = publicPath.slice(0, -".pub".length);
6503
6927
  const read = readText(publicPath);
6504
6928
  if (!read.ok) return read;
6505
- const derived = spawnSync3("ssh-keygen", ["-y", "-P", "", "-f", privatePath], {
6929
+ const derived = spawnSync4("ssh-keygen", ["-y", "-P", "", "-f", privatePath], {
6506
6930
  encoding: "utf8",
6507
6931
  timeout: 3e3,
6508
6932
  stdio: ["ignore", "pipe", "pipe"],
@@ -6533,19 +6957,19 @@ var readRequiredKey = (keyPath, source) => {
6533
6957
  var readOptionalKey = (keyPath) => {
6534
6958
  const parsed = parseIdentityPath(keyPath);
6535
6959
  if (!parsed.ok) return parsed;
6536
- if (!fs12.existsSync(parsed.value) && !fs12.existsSync(publicKeyPath(parsed.value)))
6960
+ if (!fs13.existsSync(parsed.value) && !fs13.existsSync(publicKeyPath(parsed.value)))
6537
6961
  return success(null);
6538
6962
  return readRequiredKey(parsed.value, "existing");
6539
6963
  };
6540
6964
  var pinAgentKey = (configHome, publicKey) => {
6541
- const identityFile = path12.resolve(
6965
+ const identityFile = path13.resolve(
6542
6966
  configHome,
6543
6967
  "ssh",
6544
6968
  `agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
6545
6969
  );
6546
6970
  try {
6547
- fs12.mkdirSync(path12.dirname(identityFile), { recursive: true, mode: 448 });
6548
- fs12.writeFileSync(identityFile, `${publicKey}
6971
+ fs13.mkdirSync(path13.dirname(identityFile), { recursive: true, mode: 448 });
6972
+ fs13.writeFileSync(identityFile, `${publicKey}
6549
6973
  `, { mode: 384 });
6550
6974
  return success({ publicKey, identityFile, source: "agent" });
6551
6975
  } catch (cause) {
@@ -6556,15 +6980,15 @@ var pinAgentKey = (configHome, publicKey) => {
6556
6980
  };
6557
6981
  var generateDedicatedKey = (privatePath) => {
6558
6982
  try {
6559
- fs12.mkdirSync(path12.dirname(privatePath), { recursive: true, mode: 448 });
6983
+ fs13.mkdirSync(path13.dirname(privatePath), { recursive: true, mode: 448 });
6560
6984
  } catch (cause) {
6561
6985
  return failure(
6562
- new WorkspaceSshError("key", `Could not create ${path12.dirname(privatePath)}.`, cause)
6986
+ new WorkspaceSshError("key", `Could not create ${path13.dirname(privatePath)}.`, cause)
6563
6987
  );
6564
6988
  }
6565
- const generated = spawnSync3(
6989
+ const generated = spawnSync4(
6566
6990
  "ssh-keygen",
6567
- ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os7.hostname()}`],
6991
+ ["-t", "ed25519", "-f", privatePath, "-N", "", "-C", `mend-${os8.hostname()}`],
6568
6992
  { encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "pipe"] }
6569
6993
  );
6570
6994
  if (generated.status !== 0) {
@@ -6588,7 +7012,7 @@ var generateDedicatedKey = (privatePath) => {
6588
7012
  return fresh;
6589
7013
  };
6590
7014
  var dedicatedKey = (configHome, create) => {
6591
- const privatePath = path12.join(configHome, "ssh", "id_ed25519");
7015
+ const privatePath = path13.join(configHome, "ssh", "id_ed25519");
6592
7016
  const existing = readOptionalKey(privatePath);
6593
7017
  if (existing.ok === false) return failure(existing.error);
6594
7018
  if (existing.value !== null || !create) return existing;
@@ -6636,10 +7060,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
6636
7060
  const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
6637
7061
  if (reconciled.ok === false) return failure(reconciled.error);
6638
7062
  try {
6639
- fs12.mkdirSync(path12.dirname(configFile), { recursive: true, mode: 448 });
6640
- fs12.chmodSync(path12.dirname(configFile), 448);
6641
- fs12.writeFileSync(configFile, reconciled.value, { mode: 384 });
6642
- fs12.chmodSync(configFile, 384);
7063
+ fs13.mkdirSync(path13.dirname(configFile), { recursive: true, mode: 448 });
7064
+ fs13.chmodSync(path13.dirname(configFile), 448);
7065
+ fs13.writeFileSync(configFile, reconciled.value, { mode: 384 });
7066
+ fs13.chmodSync(configFile, 384);
6643
7067
  return success(void 0);
6644
7068
  } catch (cause) {
6645
7069
  return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
@@ -6652,8 +7076,8 @@ var dim5 = ansi("2");
6652
7076
  var green5 = ansi("32");
6653
7077
  var warn = ansi("33");
6654
7078
  var say5 = (line) => console.log(line);
6655
- var sshConfigPath = () => path13.join(os8.homedir(), ".ssh", "config");
6656
- var flagValue = (args, flag) => {
7079
+ var sshConfigPath = () => path14.join(os9.homedir(), ".ssh", "config");
7080
+ var flagValue2 = (args, flag) => {
6657
7081
  const index = args.indexOf(flag);
6658
7082
  return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
6659
7083
  };
@@ -6670,7 +7094,7 @@ var showStatus = async (api2, cliHome, serverUrl, args) => {
6670
7094
  const parsedTarget = parseWorkspaceSshTarget({
6671
7095
  serverUrl,
6672
7096
  publishedPort: view.gateway.port,
6673
- hostnameOverride: flagValue(args, "--host")
7097
+ hostnameOverride: flagValue2(args, "--host")
6674
7098
  });
6675
7099
  if (parsedTarget.ok === false) return showFailure(parsedTarget.error.message);
6676
7100
  const config = readWorkspaceSshConfig(sshConfigPath());
@@ -6707,14 +7131,14 @@ var setup = async (api2, cliHome, serverUrl, args) => {
6707
7131
  const parsedTarget = parseWorkspaceSshTarget({
6708
7132
  serverUrl,
6709
7133
  publishedPort: view.gateway.port,
6710
- hostnameOverride: flagValue(args, "--host")
7134
+ hostnameOverride: flagValue2(args, "--host")
6711
7135
  });
6712
7136
  if (parsedTarget.ok === false) return showFailure(parsedTarget.error.message);
6713
7137
  const config = readWorkspaceSshConfig(sshConfigPath());
6714
7138
  if (config.ok === false) return showFailure(config.error.message);
6715
7139
  const picked = pickWorkspaceSshKey({
6716
7140
  configHome: cliHome,
6717
- explicitKeyPath: flagValue(args, "--key"),
7141
+ explicitKeyPath: flagValue2(args, "--key"),
6718
7142
  configuredIdentityFile: configuredWorkspaceSshIdentityFile(config.value, parsedTarget.value),
6719
7143
  create: true
6720
7144
  });
@@ -6722,7 +7146,7 @@ var setup = async (api2, cliHome, serverUrl, args) => {
6722
7146
  if (picked.value === null) return showFailure("No workspace SSH key is available.");
6723
7147
  const registered = await api2("POST", "/workspace-ssh/keys", {
6724
7148
  publicKey: picked.value.publicKey,
6725
- name: os8.hostname()
7149
+ name: os9.hostname()
6726
7150
  });
6727
7151
  const sourceLabel = {
6728
7152
  explicit: "from --key",
@@ -6764,8 +7188,8 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
6764
7188
  };
6765
7189
 
6766
7190
  // src/uninstall.ts
6767
- import * as fs13 from "node:fs";
6768
- import * as path14 from "node:path";
7191
+ import * as fs14 from "node:fs";
7192
+ import * as path15 from "node:path";
6769
7193
  var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
6770
7194
  var parseUninstallArgs = (args) => {
6771
7195
  let scope = null;
@@ -6787,14 +7211,14 @@ var parseUninstallArgs = (args) => {
6787
7211
  var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
6788
7212
  var countEntries = (directory) => {
6789
7213
  try {
6790
- return fs13.readdirSync(directory).length;
7214
+ return fs14.readdirSync(directory).length;
6791
7215
  } catch {
6792
7216
  return 0;
6793
7217
  }
6794
7218
  };
6795
7219
  var exists = (file) => {
6796
7220
  try {
6797
- fs13.lstatSync(file);
7221
+ fs14.lstatSync(file);
6798
7222
  return true;
6799
7223
  } catch {
6800
7224
  return false;
@@ -6803,7 +7227,7 @@ var exists = (file) => {
6803
7227
  var managedBlockCount = (sshConfigFile) => {
6804
7228
  let config;
6805
7229
  try {
6806
- config = fs13.readFileSync(sshConfigFile, "utf8");
7230
+ config = fs14.readFileSync(sshConfigFile, "utf8");
6807
7231
  } catch {
6808
7232
  return 0;
6809
7233
  }
@@ -6827,27 +7251,27 @@ var describeUninstall = async (runtime, scope) => {
6827
7251
  if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
6828
7252
  server = "none";
6829
7253
  } else if (read.value === null) {
6830
- server = exists(path14.join(configDir, "identity.env")) ? {
7254
+ server = exists(path15.join(configDir, "identity.env")) ? {
6831
7255
  version: "(no active generation)",
6832
7256
  appUrl: "",
6833
7257
  dockerContext: "",
6834
- generations: countEntries(path14.join(configDir, "generations")),
6835
- backups: countEntries(path14.join(configDir, "backups"))
7258
+ generations: countEntries(path15.join(configDir, "generations")),
7259
+ backups: countEntries(path15.join(configDir, "backups"))
6836
7260
  } : "none";
6837
7261
  } else {
6838
7262
  server = {
6839
7263
  version: read.value.config.serverVersion,
6840
7264
  appUrl: read.value.config.appUrl,
6841
7265
  dockerContext: read.value.config.dockerContext,
6842
- generations: countEntries(path14.join(configDir, "generations")),
6843
- backups: countEntries(path14.join(configDir, "backups"))
7266
+ generations: countEntries(path15.join(configDir, "generations")),
7267
+ backups: countEntries(path15.join(configDir, "backups"))
6844
7268
  };
6845
7269
  }
6846
7270
  }
6847
7271
  let home = null;
6848
7272
  if (scope !== "server") {
6849
- const cliConfig = path14.join(runtime.cliHome, "cli.json");
6850
- const sshDirectory = path14.join(runtime.cliHome, "ssh");
7273
+ const cliConfig = path15.join(runtime.cliHome, "cli.json");
7274
+ const sshDirectory = path15.join(runtime.cliHome, "ssh");
6851
7275
  home = {
6852
7276
  cliConfig: exists(cliConfig) ? cliConfig : null,
6853
7277
  sshDirectory: exists(sshDirectory) ? sshDirectory : null,
@@ -6979,7 +7403,7 @@ var removeServer = async (runtime, failures, leftovers) => {
6979
7403
  }
6980
7404
  }
6981
7405
  for (const name of SERVER_FILES) {
6982
- fs13.rmSync(path14.join(configDir, name), { recursive: true, force: true });
7406
+ fs14.rmSync(path15.join(configDir, name), { recursive: true, force: true });
6983
7407
  }
6984
7408
  server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
6985
7409
  },
@@ -6990,20 +7414,20 @@ var removeServer = async (runtime, failures, leftovers) => {
6990
7414
  var removeHome = async (runtime, plan, failures, leftovers) => {
6991
7415
  const { server } = runtime;
6992
7416
  if (plan.cliConfig !== null) {
6993
- fs13.rmSync(plan.cliConfig, { force: true });
7417
+ fs14.rmSync(plan.cliConfig, { force: true });
6994
7418
  server.writeLine(`removed ${plan.cliConfig}`);
6995
7419
  }
6996
7420
  if (plan.sshDirectory !== null) {
6997
- fs13.rmSync(plan.sshDirectory, { recursive: true, force: true });
7421
+ fs14.rmSync(plan.sshDirectory, { recursive: true, force: true });
6998
7422
  server.writeLine(`removed ${plan.sshDirectory}`);
6999
7423
  }
7000
7424
  if (plan.managedSshBlocks > 0) {
7001
7425
  try {
7002
- const before = fs13.readFileSync(runtime.sshConfigFile, "utf8");
7426
+ const before = fs14.readFileSync(runtime.sshConfigFile, "utf8");
7003
7427
  const stripped = stripManagedWorkspaceSshBlocks(before);
7004
7428
  if (!stripped.ok) throw stripped.error;
7005
7429
  if (stripped.value.removed > 0) {
7006
- fs13.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
7430
+ fs14.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
7007
7431
  server.writeLine(
7008
7432
  `removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
7009
7433
  );
@@ -7016,12 +7440,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
7016
7440
  }
7017
7441
  let remaining = [];
7018
7442
  try {
7019
- remaining = fs13.readdirSync(runtime.cliHome);
7443
+ remaining = fs14.readdirSync(runtime.cliHome);
7020
7444
  } catch {
7021
7445
  return;
7022
7446
  }
7023
7447
  if (remaining.length === 0) {
7024
- fs13.rmdirSync(runtime.cliHome);
7448
+ fs14.rmdirSync(runtime.cliHome);
7025
7449
  server.writeLine(`removed ${runtime.cliHome}`);
7026
7450
  } else {
7027
7451
  leftovers.push(
@@ -7085,19 +7509,19 @@ var upgradeUrl = async (input) => {
7085
7509
  // src/main.ts
7086
7510
  var mendCliHome = () => {
7087
7511
  const xdg = process.env["XDG_CONFIG_HOME"];
7088
- const preferred = path15.join(
7089
- xdg === void 0 || xdg === "" ? path15.join(os9.homedir(), ".config") : xdg,
7512
+ const preferred = path16.join(
7513
+ xdg === void 0 || xdg === "" ? path16.join(os10.homedir(), ".config") : xdg,
7090
7514
  "mend"
7091
7515
  );
7092
- const legacy = path15.join(os9.homedir(), ".mend");
7093
- return !fs14.existsSync(preferred) && fs14.existsSync(legacy) ? legacy : preferred;
7516
+ const legacy = path16.join(os10.homedir(), ".mend");
7517
+ return !fs15.existsSync(preferred) && fs15.existsSync(legacy) ? legacy : preferred;
7094
7518
  };
7095
- var CONFIG_PATH = path15.join(mendCliHome(), "cli.json");
7519
+ var CONFIG_PATH = path16.join(mendCliHome(), "cli.json");
7096
7520
  var loadConfig = () => {
7097
7521
  let fileConfig = {};
7098
- if (fs14.existsSync(CONFIG_PATH)) {
7522
+ if (fs15.existsSync(CONFIG_PATH)) {
7099
7523
  try {
7100
- fileConfig = JSON.parse(fs14.readFileSync(CONFIG_PATH, "utf8"));
7524
+ fileConfig = JSON.parse(fs15.readFileSync(CONFIG_PATH, "utf8"));
7101
7525
  } catch {
7102
7526
  fail6(`could not parse ${CONFIG_PATH}`);
7103
7527
  }
@@ -7135,9 +7559,10 @@ var herdrAgentOf = (harness) => {
7135
7559
  if (harness === "codex" || harness === "claude" || harness === "opencode") return harness;
7136
7560
  return null;
7137
7561
  };
7562
+ var noHerdrHint = () => void 0;
7138
7563
  var hintHerdrAttachment = (harness) => {
7139
7564
  const agent = herdrAgentOf(harness);
7140
- if (process.env["HERDR_ENV"] !== "1" || agent === null) return () => void 0;
7565
+ if (process.env["HERDR_ENV"] !== "1" || agent === null) return noHerdrHint;
7141
7566
  const carrier = spawn3(
7142
7567
  process.execPath,
7143
7568
  ["-e", "process.stdin.resume();process.stdin.once('end',()=>process.exit(0))"],
@@ -7163,6 +7588,8 @@ var request = async (config, method, route, body) => {
7163
7588
  const headers = { "content-type": "application/json" };
7164
7589
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
7165
7590
  let response;
7591
+ const started = Date.now();
7592
+ const call = `${method} ${route}`;
7166
7593
  try {
7167
7594
  response = await fetch(`${config.url}/api${route}`, {
7168
7595
  method,
@@ -7170,23 +7597,30 @@ var request = async (config, method, route, body) => {
7170
7597
  // Spread, not `body: null` — fresh oxlint rejects a body key on GETs.
7171
7598
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
7172
7599
  });
7173
- } catch {
7174
- throw new Error(`cannot reach the Mend server at ${config.url} \u2014 is it running?`);
7600
+ } catch (error) {
7601
+ throw noAnswerError(error, config.url, call, Date.now() - started);
7175
7602
  }
7176
7603
  if (response.status === 401) {
7177
- throw new Error(
7178
- config.token === null ? `not signed in to ${config.url} \u2014 run: mend login` : `unauthorized at ${config.url} \u2014 the saved token was rejected; run: mend login`
7604
+ throw new MendRequestError(
7605
+ "http",
7606
+ config.token === null ? `not signed in to ${config.url} \u2014 run: mend login` : `unauthorized at ${config.url} \u2014 the saved token was rejected; run: mend login`,
7607
+ 401
7179
7608
  );
7180
7609
  }
7181
- const text = await response.text();
7610
+ let text;
7611
+ try {
7612
+ text = await response.text();
7613
+ } catch (error) {
7614
+ throw noAnswerError(error, config.url, call, Date.now() - started);
7615
+ }
7182
7616
  if (!response.ok) {
7183
- let message = `${method} ${route} \u2192 ${response.status}`;
7617
+ let message = `${call} \u2192 ${response.status}`;
7184
7618
  try {
7185
7619
  const parsed = JSON.parse(text);
7186
7620
  message = parsed.message ?? message;
7187
7621
  } catch {
7188
7622
  }
7189
- throw new Error(message);
7623
+ throw new MendRequestError("http", message, response.status);
7190
7624
  }
7191
7625
  try {
7192
7626
  return JSON.parse(text);
@@ -7205,10 +7639,11 @@ var download = (config) => async (route) => {
7205
7639
  const headers = {};
7206
7640
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
7207
7641
  let response;
7642
+ const started = Date.now();
7208
7643
  try {
7209
7644
  response = await fetch(`${config.url}/api${route}`, { headers });
7210
- } catch {
7211
- return fail6(`cannot reach the Mend server at ${config.url} \u2014 is it running?`);
7645
+ } catch (error) {
7646
+ return fail6(noAnswerError(error, config.url, `GET ${route}`, Date.now() - started).message);
7212
7647
  }
7213
7648
  if (response.status === 401) {
7214
7649
  return fail6(
@@ -7219,13 +7654,14 @@ var download = (config) => async (route) => {
7219
7654
  return { status: response.status, header: (name) => response.headers.get(name), bytes };
7220
7655
  };
7221
7656
  var warnedLegacyBearer = false;
7222
- var mintTicket = (config) => async (target, params) => {
7657
+ var mintTicket = (config, signal) => async (target, params) => {
7223
7658
  const headers = { "content-type": "application/json" };
7224
7659
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
7225
7660
  const response = await fetch(`${config.url}/api/upgrade-tickets`, {
7226
7661
  method: "POST",
7227
7662
  headers,
7228
- body: JSON.stringify({ target, ...params })
7663
+ body: JSON.stringify({ target, ...params }),
7664
+ ...signal === void 0 ? {} : { signal }
7229
7665
  });
7230
7666
  if (response.status === 404) {
7231
7667
  if (await mintRefusedInTransit(config.url)) throw new Error(MINT_REFUSED_IN_TRANSIT);
@@ -7243,12 +7679,12 @@ var mintTicket = (config) => async (target, params) => {
7243
7679
  throw new Error("upgrade ticket missing from the answer");
7244
7680
  return { kind: "ticket", ticket: minted.ticket };
7245
7681
  };
7246
- var socketUrl = (config, target, params, extra) => upgradeUrl({
7682
+ var socketUrl = (config, target, params, extra, signal) => upgradeUrl({
7247
7683
  serverUrl: parseMendUrl(config.url).toString(),
7248
7684
  target,
7249
7685
  params,
7250
7686
  ...extra === void 0 ? {} : { extra },
7251
- mint: mintTicket(config),
7687
+ mint: mintTicket(config, signal),
7252
7688
  legacyToken: config.token
7253
7689
  });
7254
7690
  var boundApi = (config) => (method, route, body) => api(config, method, route, body);
@@ -7258,8 +7694,11 @@ var withSpinner = async (label, work) => {
7258
7694
  const started = Date.now();
7259
7695
  let frame = 0;
7260
7696
  const timer = setInterval(() => {
7261
- const seconds = Math.round((Date.now() - started) / 1e3);
7262
- process.stdout.write(`\r ${frames[frame % frames.length]} ${label} ${dim6(`${seconds}s`)} `);
7697
+ const seconds2 = Math.round((Date.now() - started) / 1e3);
7698
+ const text = typeof label === "string" ? label : label();
7699
+ process.stdout.write(
7700
+ `\r\x1B[2K ${frames[frame % frames.length]} ${text} ${dim6(`${seconds2}s`)} `
7701
+ );
7263
7702
  frame += 1;
7264
7703
  }, 120);
7265
7704
  try {
@@ -7269,6 +7708,52 @@ var withSpinner = async (label, work) => {
7269
7708
  process.stdout.write("\r\x1B[2K");
7270
7709
  }
7271
7710
  };
7711
+ var followStarting = async (config, sessionId, label, start) => {
7712
+ let line = label;
7713
+ const timers = /* @__PURE__ */ new Set();
7714
+ const sleep3 = (ms) => new Promise((resolve7) => {
7715
+ const timer = setTimeout(() => {
7716
+ timers.delete(timer);
7717
+ resolve7();
7718
+ }, ms);
7719
+ timers.add(timer);
7720
+ });
7721
+ try {
7722
+ return await withSpinner(
7723
+ () => line,
7724
+ followStart({
7725
+ start,
7726
+ read: () => request(config, "GET", `/sessions/${sessionId}`),
7727
+ onLine: (next) => {
7728
+ line = next === "starting" ? label : next;
7729
+ if (process.stdout.isTTY !== true) say6(dim6(` ${next}`));
7730
+ },
7731
+ sleep: sleep3,
7732
+ now: Date.now
7733
+ })
7734
+ );
7735
+ } finally {
7736
+ for (const timer of timers) clearTimeout(timer);
7737
+ }
7738
+ };
7739
+ var startedOrExit = (config, sessionId, outcome) => {
7740
+ const id8 = sessionId.slice(0, 8);
7741
+ switch (outcome.kind) {
7742
+ case "live":
7743
+ return outcome.session;
7744
+ case "refused":
7745
+ return fail6(outcome.message);
7746
+ case "settled":
7747
+ return fail6(
7748
+ `session ${id8} \xB7 ${startingLineOf(outcome.session)} before its agent ran \xB7 ${config.url}/sessions/${sessionId}`
7749
+ );
7750
+ case "unreachable":
7751
+ return fail6(
7752
+ `${outcome.message} \xB7 the session may still be starting \xB7 follow it: mend attach ${id8}`
7753
+ );
7754
+ }
7755
+ };
7756
+ var startAndFollow = async (config, sessionId, label, start) => startedOrExit(config, sessionId, await followStarting(config, sessionId, label, start));
7272
7757
  var adopt = async (config, args) => {
7273
7758
  const nameFlag = args.indexOf("--name");
7274
7759
  const name = nameFlag !== -1 && args[nameFlag + 1] !== void 0 ? String(args[nameFlag + 1]) : null;
@@ -7288,7 +7773,7 @@ var adopt = async (config, args) => {
7288
7773
  }
7289
7774
  const sourceIssue = repositoryCloneUrlIssue(source);
7290
7775
  if (sourceIssue !== null) return fail6(sourceIssue);
7291
- const projectName = name ?? normalizeProjectName(path15.basename(source, ".git"));
7776
+ const projectName = name ?? normalizeProjectName(path16.basename(source, ".git"));
7292
7777
  const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
7293
7778
  if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
7294
7779
  return fail6(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
@@ -7337,7 +7822,7 @@ var findProject = async (config, explicit, adoptCwd = false) => {
7337
7822
  const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
7338
7823
  if (sourceIssue !== null) return fail6(sourceIssue);
7339
7824
  const adopted = await api(config, "POST", "/projects", {
7340
- name: normalizeProjectName(path15.basename(facts.repoRoot)),
7825
+ name: normalizeProjectName(path16.basename(facts.repoRoot)),
7341
7826
  source: facts.originUrl
7342
7827
  });
7343
7828
  say6(`${green6("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim6(adopted.storePath)}`);
@@ -7470,9 +7955,11 @@ var launch = async (config, harness, args) => {
7470
7955
  ...parsed.ask ? { permissionMode: "ask" } : {},
7471
7956
  ...parsed.fast ? { speed: "fast" } : {}
7472
7957
  } : { argv };
7473
- await withSpinner(
7958
+ await startAndFollow(
7959
+ config,
7960
+ session.id,
7474
7961
  "provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
7475
- api(config, "POST", `/sessions/${session.id}/launch`, launchBody)
7962
+ request(config, "POST", `/sessions/${session.id}/launch`, launchBody)
7476
7963
  );
7477
7964
  if (lifecycle === "detach") {
7478
7965
  say6(`${green6("\u2713 recording")} \xB7 running detached`);
@@ -7494,58 +7981,67 @@ var autoLandLine = (override, project2) => {
7494
7981
  }
7495
7982
  return `${green6("\u2713")} automatic landing \xB7 ${override ? "on" : "off"} for this session ${dim6("\xB7 from this terminal, land with mend land")}`;
7496
7983
  };
7497
- var attachTty = async (config, sessionId, harness, from, processId, options) => {
7498
- let url;
7499
- try {
7500
- url = await socketUrl(
7501
- config,
7502
- "tty",
7503
- processId === void 0 ? { session: sessionId } : { process: processId },
7504
- { from: from.toString() }
7984
+ var ATTACH_CONNECT_TIMEOUT_MS = (() => {
7985
+ const configured = Number(process.env["MEND_ATTACH_TIMEOUT_MS"]);
7986
+ return Number.isFinite(configured) && configured > 0 ? configured : 3e4;
7987
+ })();
7988
+ var connectingLine = (sessionId, elapsedMs) => `\r\x1B[2K connecting to ${sessionId.slice(0, 8)} \xB7 ${Math.round(elapsedMs / 1e3)}s \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C cancels`;
7989
+ var takeTerminal = () => {
7990
+ if (process.stdin.isTTY !== true) {
7991
+ console.error(
7992
+ "mend: stdin is not a terminal, so it cannot be put in raw mode \u2014 keys reach the session a line at a time and echo here"
7505
7993
  );
7506
- } catch {
7507
- return "unavailable";
7994
+ return { raw: false };
7508
7995
  }
7509
- const ws = new WebSocket(url);
7510
- ws.binaryType = "arraybuffer";
7511
7996
  try {
7512
- await new Promise((resolve7, reject) => {
7513
- ws.addEventListener("open", () => resolve7(), { once: true });
7514
- ws.addEventListener("error", () => reject(new Error("could not connect")), { once: true });
7515
- });
7516
- } catch {
7517
- return "unavailable";
7997
+ process.stdin.setRawMode(true);
7998
+ } catch (error) {
7999
+ console.error(
8000
+ `mend: could not put this terminal in raw mode (${error instanceof Error ? error.message : String(error)}) \u2014 keys reach the session a line at a time and echo here`
8001
+ );
8002
+ return { raw: false };
7518
8003
  }
7519
- const stopHerdrHint = hintHerdrAttachment(harness);
7520
- let finishAttachment;
7521
- const finished = new Promise((resolve7) => {
7522
- finishAttachment = resolve7;
7523
- });
7524
- const onClose = () => finishAttachment?.();
7525
- if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
7526
- else ws.addEventListener("close", onClose, { once: true });
7527
- const sendResize = () => {
7528
- if (ws.readyState !== WebSocket.OPEN) return;
7529
- ws.send(
7530
- JSON.stringify({
7531
- t: "resize",
7532
- cols: process.stdout.columns ?? 80,
7533
- rows: process.stdout.rows ?? 24
7534
- })
8004
+ if (!process.stdin.isRaw) {
8005
+ console.error(
8006
+ "mend: this terminal did not accept raw mode \u2014 keys reach the session a line at a time and echo here"
7535
8007
  );
8008
+ return { raw: false };
8009
+ }
8010
+ return { raw: true };
8011
+ };
8012
+ var terminalSize = () => ({
8013
+ cols: process.stdout.columns || 80,
8014
+ rows: process.stdout.rows || 24
8015
+ });
8016
+ var attachTty = async (config, sessionId, harness, from, processId, options) => {
8017
+ const interactive = options?.readOnly !== true;
8018
+ const rawModeEnabled = interactive ? takeTerminal().raw : false;
8019
+ const connecting = new AbortController();
8020
+ let abortReason = null;
8021
+ const abort = (reason) => {
8022
+ abortReason ??= reason;
8023
+ connecting.abort();
7536
8024
  };
7537
- const onWinch = () => sendResize();
7538
- const rawTty = process.stdin.isTTY === true;
7539
- let rawModeEnabled = false;
8025
+ let ws = null;
7540
8026
  let detached = false;
7541
8027
  let sawEnd = false;
7542
8028
  let interrupted = false;
8029
+ let finishAttachment;
8030
+ const finished = new Promise((resolve7) => {
8031
+ finishAttachment = resolve7;
8032
+ });
7543
8033
  const onSignal = () => {
7544
8034
  interrupted = true;
7545
- ws.close();
8035
+ abort("interrupted");
8036
+ ws?.close();
7546
8037
  finishAttachment?.();
7547
8038
  };
7548
8039
  const signals = ["SIGHUP", "SIGINT", "SIGTERM"];
8040
+ const sendResize = (size = terminalSize()) => {
8041
+ if (ws === null || ws.readyState !== WebSocket.OPEN) return;
8042
+ ws.send(JSON.stringify({ t: "resize", cols: size.cols, rows: size.rows }));
8043
+ };
8044
+ const onWinch = () => sendResize();
7549
8045
  let bracketedPaste = false;
7550
8046
  const onTtyFrame = (event) => {
7551
8047
  if (typeof event.data !== "string") {
@@ -7560,15 +8056,24 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
7560
8056
  return;
7561
8057
  }
7562
8058
  sawEnd = true;
7563
- ws.close();
8059
+ ws?.close();
7564
8060
  finishAttachment?.();
7565
8061
  } catch {
7566
8062
  }
7567
8063
  };
8064
+ const forward = (bytes) => {
8065
+ if (ws !== null && ws.readyState === WebSocket.OPEN) ws.send(new Uint8Array(bytes).buffer);
8066
+ };
7568
8067
  const onKeys = (data) => {
8068
+ if (ws === null) {
8069
+ if (detachKeyEnabled && isDetachChunk(data)) abort("detached");
8070
+ else if (data.includes(3)) abort("interrupted");
8071
+ return;
8072
+ }
7569
8073
  if (detachKeyEnabled && isDetachChunk(data)) {
7570
8074
  detached = true;
7571
8075
  ws.close();
8076
+ finishAttachment?.();
7572
8077
  return;
7573
8078
  }
7574
8079
  if (ws.readyState !== WebSocket.OPEN) return;
@@ -7578,9 +8083,6 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
7578
8083
  }
7579
8084
  ws.send(new Uint8Array(data).buffer);
7580
8085
  };
7581
- const forward = (bytes) => {
7582
- if (ws.readyState === WebSocket.OPEN) ws.send(new Uint8Array(bytes).buffer);
7583
- };
7584
8086
  const pasteClipboardImage = async (keystroke) => {
7585
8087
  const image = await readClipboardImage();
7586
8088
  if (image === null) {
@@ -7599,36 +8101,88 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
7599
8101
  process.stdout.write("\x07");
7600
8102
  }
7601
8103
  };
8104
+ const connect2 = async () => {
8105
+ let url;
8106
+ try {
8107
+ url = await socketUrl(
8108
+ config,
8109
+ "tty",
8110
+ processId === void 0 ? { session: sessionId } : { process: processId },
8111
+ { from: from.toString() },
8112
+ connecting.signal
8113
+ );
8114
+ } catch {
8115
+ return connecting.signal.aborted ? "aborted" : "unavailable";
8116
+ }
8117
+ if (connecting.signal.aborted) return "aborted";
8118
+ const socket = new WebSocket(url);
8119
+ socket.binaryType = "arraybuffer";
8120
+ const opened = await new Promise((resolve7) => {
8121
+ const onAbort = () => resolve7(false);
8122
+ connecting.signal.addEventListener("abort", onAbort, { once: true });
8123
+ const settle = (value) => {
8124
+ connecting.signal.removeEventListener("abort", onAbort);
8125
+ resolve7(value);
8126
+ };
8127
+ socket.addEventListener("open", () => settle(true), { once: true });
8128
+ socket.addEventListener("error", () => settle(false), { once: true });
8129
+ });
8130
+ if (opened && !connecting.signal.aborted) return socket;
8131
+ socket.close();
8132
+ return connecting.signal.aborted ? "aborted" : "unavailable";
8133
+ };
8134
+ const started = Date.now();
8135
+ let connectingShown = false;
8136
+ const connectingTimer = setInterval(() => {
8137
+ if (process.stdout.isTTY !== true) return;
8138
+ connectingShown = true;
8139
+ process.stdout.write(connectingLine(sessionId, Date.now() - started));
8140
+ }, 1e3);
8141
+ const deadline = setTimeout(() => abort("no-answer"), ATTACH_CONNECT_TIMEOUT_MS);
8142
+ const stopConnecting = () => {
8143
+ for (const timer of [connectingTimer, deadline]) clearTimeout(timer);
8144
+ if (connectingShown) process.stdout.write("\r\x1B[2K");
8145
+ connectingShown = false;
8146
+ };
8147
+ let stopHerdrHint = noHerdrHint;
7602
8148
  try {
7603
- sendResize();
7604
- process.stdout.on("resize", onWinch);
7605
8149
  if (options?.handleSignals === true) for (const signal of signals) process.on(signal, onSignal);
7606
- if (options?.readOnly !== true) {
7607
- if (rawTty) {
7608
- process.stdin.setRawMode(true);
7609
- rawModeEnabled = true;
7610
- }
7611
- process.stdin.resume();
8150
+ if (interactive) {
7612
8151
  process.stdin.on("data", onKeys);
8152
+ process.stdin.resume();
7613
8153
  }
8154
+ const connected = await connect2();
8155
+ stopConnecting();
8156
+ if (connected === "unavailable") return "unavailable";
8157
+ if (connected === "aborted") return abortReason ?? "interrupted";
8158
+ ws = connected;
8159
+ stopHerdrHint = hintHerdrAttachment(harness);
8160
+ const onClose = () => finishAttachment?.();
8161
+ if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
8162
+ else ws.addEventListener("close", onClose, { once: true });
7614
8163
  ws.addEventListener("message", onTtyFrame);
8164
+ const size = terminalSize();
8165
+ if (size.rows > 1) sendResize({ cols: size.cols, rows: size.rows - 1 });
8166
+ setTimeout(() => sendResize(), 80);
8167
+ process.stdout.on("resize", onWinch);
7615
8168
  await finished;
7616
8169
  return detached ? "detached" : interrupted ? "interrupted" : sawEnd ? "ended" : "dropped";
7617
8170
  } finally {
8171
+ stopConnecting();
7618
8172
  for (const signal of signals) process.off(signal, onSignal);
7619
8173
  process.stdin.off("data", onKeys);
7620
8174
  process.stdout.off("resize", onWinch);
7621
- ws.removeEventListener("message", onTtyFrame);
7622
- ws.removeEventListener("close", onClose);
8175
+ ws?.removeEventListener("message", onTtyFrame);
7623
8176
  if (rawModeEnabled) process.stdin.setRawMode(false);
7624
- if (options?.readOnly !== true && process.stdout.isTTY === true) {
8177
+ if (interactive && ws !== null && process.stdout.isTTY === true) {
7625
8178
  process.stdout.write(
7626
8179
  "\x1B[<u\x1B[=0;1u\x1B[?2004l\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1049l\x1B[?25h"
7627
8180
  // show the cursor
7628
8181
  );
7629
8182
  }
7630
8183
  process.stdin.pause();
7631
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
8184
+ if (ws !== null && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING))
8185
+ ws.close();
7632
8186
  stopHerdrHint();
7633
8187
  }
7634
8188
  };
@@ -7656,11 +8210,18 @@ var stopAndExit = async (config, sessionId) => {
7656
8210
  say6(`${amber2("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
7657
8211
  process.exit(1);
7658
8212
  };
8213
+ var noAnswerLine = (config, id8) => `${amber2("no answer")} \u2014 ${config.url} did not open the terminal within ${spoken(ATTACH_CONNECT_TIMEOUT_MS)}; the session keeps running \xB7 try again: mend attach ${id8} \xB7 check the server: mend doctor`;
7659
8214
  var finishAttach = async (config, sessionId, outcome, mode = "background") => {
7660
8215
  const id8 = sessionId.slice(0, 8);
7661
8216
  if (outcome === "unavailable") {
7662
8217
  return fail6(`tty attach unavailable: could not connect to ${config.url}`);
7663
8218
  }
8219
+ if (outcome === "no-answer") {
8220
+ say6("");
8221
+ say6(noAnswerLine(config, id8));
8222
+ if (mode === "foreground") return stopAndExit(config, sessionId);
8223
+ process.exit(1);
8224
+ }
7664
8225
  if (outcome === "detached") {
7665
8226
  say6("");
7666
8227
  say6(`${amber2("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
@@ -7722,6 +8283,9 @@ var attachPicked = async (config, session, tunnels) => {
7722
8283
  say6(
7723
8284
  `${green6("\u2713")} attaching to ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim6(session.id.slice(0, 8))}${detachHint()}`
7724
8285
  );
8286
+ if (session.status === "starting") {
8287
+ await startAndFollow(config, session.id, "starting \u2014 attaching once the agent runs\u2026", null);
8288
+ }
7725
8289
  await tunnels?.start(session.id);
7726
8290
  say6("");
7727
8291
  const outcome = await attachWithTunnels(
@@ -7734,9 +8298,11 @@ var attachPicked = async (config, session, tunnels) => {
7734
8298
  const detail = await api(config, "GET", `/sessions/${session.id}`);
7735
8299
  if (detail.currentAgent?.kind === "agent-protocol" && agentIsLive(detail.session, detail.currentAgent)) {
7736
8300
  say6(`${amber2("taking over")} from the protocol session`);
7737
- await withSpinner(
8301
+ await startAndFollow(
8302
+ config,
8303
+ session.id,
7738
8304
  "reopening as a terminal \u2014 same conversation\u2026",
7739
- api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
8305
+ request(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
7740
8306
  );
7741
8307
  say6("");
7742
8308
  await attachOrExit(config, session.id, session.harness, "background", tunnels);
@@ -7805,6 +8371,10 @@ var stopSessions = async (config, targets, summarise) => {
7805
8371
  if (hold !== null) {
7806
8372
  say6(`${amber2(" " + hold)} \xB7 mend stop --services ${session.id.slice(0, 8)}`);
7807
8373
  }
8374
+ const capture = after === null ? null : captureLineOf(after.session);
8375
+ if (capture !== null) {
8376
+ say6(`${amber2(" " + capture)} \xB7 the workspace stops once nothing is pending`);
8377
+ }
7808
8378
  say6(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
7809
8379
  }
7810
8380
  if (summarise) {
@@ -7888,6 +8458,10 @@ var shellCommand = async (config, args) => {
7888
8458
  return fail6(`tty attach unavailable: could not connect to ${config.url}`);
7889
8459
  }
7890
8460
  say6("");
8461
+ if (outcome === "no-answer") {
8462
+ say6(noAnswerLine(config, session.id.slice(0, 8)));
8463
+ process.exit(1);
8464
+ }
7891
8465
  if (outcome === "detached" || outcome === "interrupted" || outcome === "dropped") {
7892
8466
  say6(`${amber2("detached")} \u2014 the shell keeps running and holds the workspace open`);
7893
8467
  process.exit(0);
@@ -8192,13 +8766,13 @@ var serviceRestart = async (config, args) => {
8192
8766
  };
8193
8767
  var serviceInit = async (args) => {
8194
8768
  const root = gitTopLevel(process.cwd()) ?? process.cwd();
8195
- const target = path15.join(root, "mend.toml");
8196
- if (fs14.existsSync(target)) {
8769
+ const target = path16.join(root, "mend.toml");
8770
+ if (fs15.existsSync(target)) {
8197
8771
  return fail6(`${target} already exists \u2014 edit it directly (init never merges)`);
8198
8772
  }
8199
- const rootFiles = fs14.readdirSync(root);
8773
+ const rootFiles = fs15.readdirSync(root);
8200
8774
  const proposals = [];
8201
- const readRoot = (name) => fs14.readFileSync(path15.join(root, name), "utf8");
8775
+ const readRoot = (name) => fs15.readFileSync(path16.join(root, name), "utf8");
8202
8776
  if (rootFiles.includes("package.json")) {
8203
8777
  proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
8204
8778
  }
@@ -8208,13 +8782,13 @@ var serviceInit = async (args) => {
8208
8782
  );
8209
8783
  for (const glob of globs) {
8210
8784
  const parent = glob.replace(/\/?\*+$/, "");
8211
- const dirs = glob.endsWith("*") ? fs14.existsSync(path15.join(root, parent)) ? fs14.readdirSync(path15.join(root, parent)).map((dir) => path15.join(parent, dir)) : [] : [glob];
8785
+ const dirs = glob.endsWith("*") ? fs15.existsSync(path16.join(root, parent)) ? fs15.readdirSync(path16.join(root, parent)).map((dir) => path16.join(parent, dir)) : [] : [glob];
8212
8786
  for (const dir of dirs) {
8213
- const manifest = path15.join(root, dir, "package.json");
8214
- if (!fs14.existsSync(manifest)) continue;
8787
+ const manifest = path16.join(root, dir, "package.json");
8788
+ if (!fs15.existsSync(manifest)) continue;
8215
8789
  for (const proposal of proposeFromWorkspacePackage(
8216
- path15.basename(dir),
8217
- fs14.readFileSync(manifest, "utf8"),
8790
+ path16.basename(dir),
8791
+ fs15.readFileSync(manifest, "utf8"),
8218
8792
  rootFiles
8219
8793
  )) {
8220
8794
  if (!proposals.some((existing) => existing.name === proposal.name)) {
@@ -8258,7 +8832,7 @@ var serviceInit = async (args) => {
8258
8832
  return;
8259
8833
  }
8260
8834
  }
8261
- fs14.writeFileSync(target, toml);
8835
+ fs15.writeFileSync(target, toml);
8262
8836
  say6(`${green6("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
8263
8837
  };
8264
8838
  var tunnelServices = async (config, services, portOverride) => {
@@ -8362,23 +8936,23 @@ var serviceCommand = async (config, args) => {
8362
8936
  }
8363
8937
  };
8364
8938
  var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
8365
- var readIfExists = (file) => fs14.existsSync(file) ? fs14.readFileSync(file, "utf8") : null;
8939
+ var readIfExists = (file) => fs15.existsSync(file) ? fs15.readFileSync(file, "utf8") : null;
8366
8940
  var localCredential = (provider) => {
8367
- const home = os9.homedir();
8941
+ const home = os10.homedir();
8368
8942
  switch (provider) {
8369
8943
  case "codex":
8370
8944
  return readIfExists(
8371
- path15.join(process.env["CODEX_HOME"] ?? path15.join(home, ".codex"), "auth.json")
8945
+ path16.join(process.env["CODEX_HOME"] ?? path16.join(home, ".codex"), "auth.json")
8372
8946
  );
8373
8947
  case "claude":
8374
8948
  return readIfExists(
8375
- path15.join(
8376
- process.env["CLAUDE_CONFIG_DIR"] ?? path15.join(home, ".claude"),
8949
+ path16.join(
8950
+ process.env["CLAUDE_CONFIG_DIR"] ?? path16.join(home, ".claude"),
8377
8951
  ".credentials.json"
8378
8952
  )
8379
8953
  );
8380
8954
  case "github": {
8381
- const result = spawnSync4("gh", ["auth", "token"], { encoding: "utf8" });
8955
+ const result = spawnSync5("gh", ["auth", "token"], { encoding: "utf8" });
8382
8956
  const token = result.status === 0 ? result.stdout.trim() : "";
8383
8957
  return token === "" ? null : token;
8384
8958
  }
@@ -8423,7 +8997,7 @@ var claudeGrantSecret = () => {
8423
8997
  return read.kind === "missing" ? null : read.secret;
8424
8998
  };
8425
8999
  var doctorBundle = (config, args) => doctorBundleCommand(args, {
8426
- defaultDir: path15.join(mendCliHome(), "bundles"),
9000
+ defaultDir: path16.join(mendCliHome(), "bundles"),
8427
9001
  now: () => /* @__PURE__ */ new Date(),
8428
9002
  say: say6,
8429
9003
  warn: (line) => process.stderr.write(`${line}
@@ -8487,7 +9061,7 @@ var claudeGrant = async () => {
8487
9061
  return null;
8488
9062
  }
8489
9063
  }
8490
- if (status.configDirectory !== null && path15.resolve(status.configDirectory) !== path15.resolve(dir)) {
9064
+ if (status.configDirectory !== null && path16.resolve(status.configDirectory) !== path16.resolve(dir)) {
8491
9065
  fail6(
8492
9066
  `claude: this Claude read ${status.configDirectory} instead of ${dir}, so a separate grant is not possible \u2014 connect the shared one deliberately with --use-my-login`
8493
9067
  );
@@ -8550,7 +9124,7 @@ var connectCommand = async (config, args) => {
8550
9124
  }
8551
9125
  let secret;
8552
9126
  if (flags.includes("--from-stdin")) {
8553
- secret = fs14.readFileSync(0, "utf8").trim();
9127
+ secret = fs15.readFileSync(0, "utf8").trim();
8554
9128
  if (secret === "") return fail6("nothing on stdin");
8555
9129
  } else if (provider === "claude" && !flags.includes("--use-my-login")) {
8556
9130
  secret = await claudeGrant();
@@ -8596,14 +9170,14 @@ var takeFlagValue4 = (args, flag) => {
8596
9170
  return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
8597
9171
  };
8598
9172
  var saveCliConfig = (next) => {
8599
- fs14.mkdirSync(path15.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
8600
- fs14.writeFileSync(
9173
+ fs15.mkdirSync(path16.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
9174
+ fs15.writeFileSync(
8601
9175
  CONFIG_PATH,
8602
9176
  `${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
8603
9177
  `,
8604
9178
  { mode: 384 }
8605
9179
  );
8606
- fs14.chmodSync(CONFIG_PATH, 384);
9180
+ fs15.chmodSync(CONFIG_PATH, 384);
8607
9181
  };
8608
9182
  var login = async (config, args) => {
8609
9183
  await loginCommand(args, {
@@ -8616,7 +9190,7 @@ var login = async (config, args) => {
8616
9190
  });
8617
9191
  };
8618
9192
  var logout = async (config) => {
8619
- if (!fs14.existsSync(CONFIG_PATH) && config.token === null) {
9193
+ if (!fs15.existsSync(CONFIG_PATH) && config.token === null) {
8620
9194
  say6(dim6("nothing saved \u2014 already signed out"));
8621
9195
  return;
8622
9196
  }
@@ -8669,8 +9243,8 @@ var uninstallCommand = async (config, args) => {
8669
9243
  const runtime = {
8670
9244
  server,
8671
9245
  cliHome: mendCliHome(),
8672
- sshConfigFile: path15.join(os9.homedir(), ".ssh", "config"),
8673
- signedIn: config.token === null || !fs14.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
9246
+ sshConfigFile: path16.join(os10.homedir(), ".ssh", "config"),
9247
+ signedIn: config.token === null || !fs15.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
8674
9248
  revokeDevice: async () => {
8675
9249
  if (config.deviceId === null) return "no device id saved";
8676
9250
  try {
@@ -8734,7 +9308,7 @@ var keysShare = async (config) => {
8734
9308
  return fail6("SSH_AUTH_SOCK is not set \u2014 start (or plug in) your ssh-agent first");
8735
9309
  }
8736
9310
  await shareAgent({
8737
- url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
9311
+ url: () => socketUrl(config, "keys-bridge", { host: os10.hostname() }),
8738
9312
  agentSock,
8739
9313
  signal: new AbortController().signal,
8740
9314
  onEvent: (event) => {
@@ -8864,7 +9438,7 @@ ${usageOf("dotfiles repo")}`);
8864
9438
  var dotfilesSync = async (config, args) => {
8865
9439
  const all = args.includes("--all");
8866
9440
  const requestedPaths = args.filter((arg) => !arg.startsWith("--"));
8867
- const home = os9.homedir();
9441
+ const home = os10.homedir();
8868
9442
  if (!all && requestedPaths.length === 0) {
8869
9443
  const found = scanDotfileCandidates(home);
8870
9444
  if (found.length === 0) {
@@ -8891,13 +9465,13 @@ var dotfilesSync = async (config, args) => {
8891
9465
  if ("error" in read) return fail6(read.error);
8892
9466
  const result = await api(config, "POST", "/dotfiles/snapshot", {
8893
9467
  files: read.files,
8894
- source: os9.hostname(),
9468
+ source: os10.hostname(),
8895
9469
  merge: false
8896
9470
  });
8897
9471
  const snapshot = result.snapshot;
8898
9472
  if (snapshot === null) return fail6("the server accepted the sync but reports no snapshot");
8899
9473
  say6(
8900
- `${green6("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os9.hostname()} \xB7 ${dim6(snapshot.sha.slice(0, 7))} ${dim6("\u2014 applies from the next session launch")}`
9474
+ `${green6("synced")} ${snapshot.files.length} file${snapshot.files.length === 1 ? "" : "s"} from ${os10.hostname()} \xB7 ${dim6(snapshot.sha.slice(0, 7))} ${dim6("\u2014 applies from the next session launch")}`
8901
9475
  );
8902
9476
  };
8903
9477
  var envLoad = async (config, args) => {
@@ -8911,10 +9485,10 @@ var envLoad = async (config, args) => {
8911
9485
  const positional = args.filter(
8912
9486
  (arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
8913
9487
  );
8914
- const file = path15.resolve(positional[0] ?? ".env");
9488
+ const file = path16.resolve(positional[0] ?? ".env");
8915
9489
  let contents;
8916
9490
  try {
8917
- contents = fs14.readFileSync(file, "utf8");
9491
+ contents = fs15.readFileSync(file, "utf8");
8918
9492
  } catch {
8919
9493
  return fail6(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
8920
9494
  }
@@ -8930,10 +9504,10 @@ var envLoad = async (config, args) => {
8930
9504
  { contents, allSecret, secretNames }
8931
9505
  );
8932
9506
  if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
8933
- say6(dim6(`${path15.basename(file)} has no variables`));
9507
+ say6(dim6(`${path16.basename(file)} has no variables`));
8934
9508
  return;
8935
9509
  }
8936
- say6(`${green6("\u2713")} loaded ${path15.basename(file)} into ${project2.name}`);
9510
+ say6(`${green6("\u2713")} loaded ${path16.basename(file)} into ${project2.name}`);
8937
9511
  for (const line of formatLoadReport(report, { dim: dim6, warn: amber2 })) say6(line);
8938
9512
  const plaintextUrls = report.loaded.filter(
8939
9513
  (entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
@@ -9124,7 +9698,7 @@ var skillsList = async (config, args) => {
9124
9698
  };
9125
9699
  var skillsPush = async (config, args) => {
9126
9700
  const dirFlag = takeFlagValue4(args, "--dir");
9127
- const root = dirFlag === null ? path15.join(os9.homedir(), DEFAULT_SKILLS_DIR) : path15.resolve(dirFlag);
9701
+ const root = dirFlag === null ? path16.join(os10.homedir(), DEFAULT_SKILLS_DIR) : path16.resolve(dirFlag);
9128
9702
  const scanned = scanSkillLibrary(root);
9129
9703
  if ("error" in scanned) return fail6(scanned.error);
9130
9704
  for (const note of scanned.notes) say6(dim6(` ${note.skill}: ${note.message}`));
@@ -9275,9 +9849,11 @@ var completionsCommand = (args) => {
9275
9849
  }
9276
9850
  };
9277
9851
  var supervisedRun = async (config, session, argv) => {
9278
- const launched = await withSpinner(
9852
+ const launched = await startAndFollow(
9853
+ config,
9854
+ session.id,
9279
9855
  "provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
9280
- api(config, "POST", `/sessions/${session.id}/launch`, { argv })
9856
+ request(config, "POST", `/sessions/${session.id}/launch`, { argv })
9281
9857
  );
9282
9858
  say6(
9283
9859
  `${green6("\u2713 recording")} \xB7 run ${dim6(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
@@ -9413,9 +9989,13 @@ var resumeCommand = async (config, args) => {
9413
9989
  say6(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
9414
9990
  const priorAgent = detail.annotations.find((annotation) => annotation.sessionId === match.id)?.currentAgent ?? null;
9415
9991
  const protocolPrior = priorAgent?.kind === "agent-protocol" && withHarness === null;
9416
- await withSpinner(
9992
+ await startAndFollow(
9993
+ config,
9994
+ match.id,
9417
9995
  protocolPrior ? "reopening as a terminal \u2014 same conversation\u2026" : "resuming \u2014 a fresh workspace restores the saved session state\u2026",
9418
- protocolPrior ? api(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : api(config, "POST", `/sessions/${match.id}/resume`, { harness: withHarness })
9996
+ protocolPrior ? request(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : request(config, "POST", `/sessions/${match.id}/resume`, {
9997
+ harness: withHarness
9998
+ })
9419
9999
  );
9420
10000
  say6(`${green6("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
9421
10001
  say6("");
@@ -9423,17 +10003,18 @@ var resumeCommand = async (config, args) => {
9423
10003
  exitAfterSessionEnd(config, match.id);
9424
10004
  };
9425
10005
  var resumeForRejoin = async (config, sessionId, label) => {
9426
- try {
9427
- await withSpinner(
9428
- label,
9429
- request(config, "POST", `/sessions/${sessionId}/resume`, { harness: null })
9430
- );
9431
- return true;
9432
- } catch (error) {
10006
+ const outcome = await followStarting(
10007
+ config,
10008
+ sessionId,
10009
+ label,
10010
+ request(config, "POST", `/sessions/${sessionId}/resume`, { harness: null })
10011
+ );
10012
+ if (outcome.kind === "refused") {
9433
10013
  const refreshed = await api(config, "GET", `/sessions/${sessionId}`);
9434
10014
  if (agentIsLive(refreshed.session, refreshed.currentAgent)) return false;
9435
- return fail6(error instanceof Error ? error.message : String(error));
9436
10015
  }
10016
+ startedOrExit(config, sessionId, outcome);
10017
+ return true;
9437
10018
  };
9438
10019
  var rejoinCommand = async (config, args) => {
9439
10020
  const harnessFlag = args.indexOf("--harness");
@@ -9464,9 +10045,11 @@ var rejoinCommand = async (config, args) => {
9464
10045
  let restored = false;
9465
10046
  const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ?? null;
9466
10047
  if (currentAgent?.kind === "agent-protocol") {
9467
- await withSpinner(
10048
+ await startAndFollow(
10049
+ config,
10050
+ session.id,
9468
10051
  alreadyLive ? "taking over from the protocol session \u2014 same conversation\u2026" : "reopening as a terminal \u2014 same conversation\u2026",
9469
- api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
10052
+ request(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
9470
10053
  );
9471
10054
  restored = true;
9472
10055
  } else if (!alreadyLive) {
@@ -9549,7 +10132,10 @@ var refreshCommand = async (config, args) => {
9549
10132
  }
9550
10133
  if (branches.length > 12) say6(dim6(` \u2026 ${branches.length - 12} more`));
9551
10134
  };
9552
- var rowHold = (row) => row.annotation === void 0 ? null : servicesHoldOf(row.session, row.annotation.currentAgent, row.annotation.liveServices ?? 0);
10135
+ var rowHold = (row) => (
10136
+ // A workspace a stop is still saving (docs/adr/0002) reads what is left, first.
10137
+ captureLineOf(row.session) ?? (row.annotation === void 0 ? null : servicesHoldOf(row.session, row.annotation.currentAgent, row.annotation.liveServices ?? 0))
10138
+ );
9553
10139
  var printSessionRow = (row) => {
9554
10140
  const { session, annotation } = row;
9555
10141
  const live = ACTIVE_STATUSES.has(session.status);
@@ -9647,6 +10233,14 @@ var sessionsCommand = async (config, args) => {
9647
10233
  openComments: annotation.openComments,
9648
10234
  totalComments: annotation.totalComments,
9649
10235
  pendingFollowUp: annotation.pendingFollowUp
10236
+ },
10237
+ capture: session.capturePending === void 0 ? null : {
10238
+ pending: session.capturePending ?? null,
10239
+ pendingBytes: session.capturePendingBytes ?? null,
10240
+ refused: session.captureRefused ?? null,
10241
+ drain: session.captureDrain ?? null,
10242
+ notSaved: (session.captureNotSavedAt ?? null) !== null,
10243
+ line: captureLineOf(session)
9650
10244
  }
9651
10245
  }))
9652
10246
  };
@@ -9779,14 +10373,14 @@ var dashboard = async (config, options = {}) => {
9779
10373
  `the dashboard needs Node >= 26 (node:ffi) \u2014 this is ${process.version}; every other command still works`
9780
10374
  );
9781
10375
  }
9782
- const rerun = spawnSync4(
10376
+ const rerun = spawnSync5(
9783
10377
  process.execPath,
9784
10378
  ["--experimental-ffi", "--disable-warning=ExperimentalWarning", ...process.argv.slice(1)],
9785
10379
  { stdio: "inherit" }
9786
10380
  );
9787
10381
  process.exit(rerun.status ?? 0);
9788
10382
  }
9789
- const { runDashboard } = await import("./dashboard-NWHR7I5W.js");
10383
+ const { runDashboard } = await import("./dashboard-DWXXYFPE.js");
9790
10384
  const agentShare = await startShareIfBridge(config);
9791
10385
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
9792
10386
  listServices: () => fetchServices(config),
@@ -9820,7 +10414,7 @@ var startShareIfBridge = async (config) => {
9820
10414
  }
9821
10415
  if (access.mode !== "bridge") return null;
9822
10416
  return startAgentShare({
9823
- url: () => socketUrl(config, "keys-bridge", { host: os9.hostname() }),
10417
+ url: () => socketUrl(config, "keys-bridge", { host: os10.hostname() }),
9824
10418
  agentSock
9825
10419
  });
9826
10420
  };
@@ -9860,10 +10454,10 @@ var manCommand = (words) => {
9860
10454
  }
9861
10455
  const version = cliVersion();
9862
10456
  const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
9863
- const file = path15.join(os9.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
9864
- fs14.writeFileSync(file, page);
9865
- const result = spawnSync4("man", ["-l", file], { stdio: "inherit" });
9866
- fs14.rmSync(file, { force: true });
10457
+ const file = path16.join(os10.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
10458
+ fs15.writeFileSync(file, page);
10459
+ const result = spawnSync5("man", ["-l", file], { stdio: "inherit" });
10460
+ fs15.rmSync(file, { force: true });
9867
10461
  if (result.error !== void 0 || result.status !== 0) {
9868
10462
  say6(doc === null ? renderIndex() : renderCommand(doc));
9869
10463
  }