@sealant/mend 0.36.0-next.684 → 0.36.0-next.721

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 (94) hide show
  1. package/dist/{chunk-DHM7VXTR.js → chunk-GF5A6W2Y.js} +12 -1
  2. package/dist/{dashboard-OBFWHSPD.js → dashboard-MPO6H5YF.js} +823 -176
  3. package/dist/main.js +1221 -84
  4. package/man/mend-accounts.1 +1 -1
  5. package/man/mend-adopt.1 +1 -1
  6. package/man/mend-attach.1 +1 -1
  7. package/man/mend-codex.1 +1 -1
  8. package/man/mend-completions.1 +1 -1
  9. package/man/mend-connect-pi.1 +1 -1
  10. package/man/mend-connect.1 +1 -1
  11. package/man/mend-continue.1 +1 -1
  12. package/man/mend-doctor.1 +1 -1
  13. package/man/mend-dotfiles-repo.1 +1 -1
  14. package/man/mend-dotfiles-sync.1 +1 -1
  15. package/man/mend-dotfiles.1 +1 -1
  16. package/man/mend-env-cluster.1 +1 -1
  17. package/man/mend-env-load.1 +1 -1
  18. package/man/mend-env-show.1 +1 -1
  19. package/man/mend-folder-create.1 +1 -1
  20. package/man/mend-folder-list.1 +1 -1
  21. package/man/mend-folder-push.1 +1 -1
  22. package/man/mend-folder-rm.1 +1 -1
  23. package/man/mend-git-author.1 +1 -1
  24. package/man/mend-help.1 +1 -1
  25. package/man/mend-invite.1 +1 -1
  26. package/man/mend-keys-init.1 +1 -1
  27. package/man/mend-keys-mode.1 +1 -1
  28. package/man/mend-keys-share.1 +1 -1
  29. package/man/mend-keys-show.1 +1 -1
  30. package/man/mend-land.1 +1 -1
  31. package/man/mend-login.1 +1 -1
  32. package/man/mend-logout.1 +1 -1
  33. package/man/mend-logs.1 +1 -1
  34. package/man/mend-man.1 +1 -1
  35. package/man/mend-members.1 +1 -1
  36. package/man/mend-memory-import.1 +1 -1
  37. package/man/mend-memory-rm.1 +1 -1
  38. package/man/mend-memory-show.1 +1 -1
  39. package/man/mend-memory.1 +1 -1
  40. package/man/mend-models.1 +1 -1
  41. package/man/mend-operator-exposure.1 +2 -2
  42. package/man/mend-operator-gate.1 +1 -1
  43. package/man/mend-operator-grant-owner.1 +1 -1
  44. package/man/mend-operator-org-create.1 +1 -1
  45. package/man/mend-operator-org-invite-owner.1 +1 -1
  46. package/man/mend-operator-org-list.1 +1 -1
  47. package/man/mend-operator-org-rename.1 +1 -1
  48. package/man/mend-operator-reset-link.1 +1 -1
  49. package/man/mend-pair.1 +1 -1
  50. package/man/mend-projects.1 +1 -1
  51. package/man/mend-pull.1 +1 -1
  52. package/man/mend-refresh.1 +1 -1
  53. package/man/mend-rejoin.1 +1 -1
  54. package/man/mend-resume.1 +1 -1
  55. package/man/mend-run.1 +1 -1
  56. package/man/mend-secrets-add.1 +1 -1
  57. package/man/mend-secrets-rm.1 +1 -1
  58. package/man/mend-secrets.1 +1 -1
  59. package/man/mend-server-logs.1 +1 -1
  60. package/man/mend-server-restart.1 +1 -1
  61. package/man/mend-server-setup.1 +37 -6
  62. package/man/mend-server-start.1 +1 -1
  63. package/man/mend-server-status.1 +1 -1
  64. package/man/mend-server-stop.1 +1 -1
  65. package/man/mend-server-upgrade.1 +1 -1
  66. package/man/mend-server.1 +1 -1
  67. package/man/mend-service-add.1 +1 -1
  68. package/man/mend-service-connect.1 +1 -1
  69. package/man/mend-service-init.1 +1 -1
  70. package/man/mend-service-list.1 +1 -1
  71. package/man/mend-service-logs.1 +1 -1
  72. package/man/mend-service-restart.1 +1 -1
  73. package/man/mend-service-run.1 +1 -1
  74. package/man/mend-service-stop.1 +1 -1
  75. package/man/mend-session-share.1 +1 -1
  76. package/man/mend-sessions.1 +1 -1
  77. package/man/mend-shell.1 +1 -1
  78. package/man/mend-skills-push.1 +1 -1
  79. package/man/mend-skills.1 +1 -1
  80. package/man/mend-snake.1 +2 -2
  81. package/man/mend-ssh-keys-remove.1 +1 -1
  82. package/man/mend-ssh-keys.1 +1 -1
  83. package/man/mend-ssh-setup.1 +1 -1
  84. package/man/mend-ssh.1 +1 -1
  85. package/man/mend-stop.1 +1 -1
  86. package/man/mend-ui.1 +3 -1
  87. package/man/mend-uninstall.1 +1 -1
  88. package/man/mend-version.1 +1 -1
  89. package/man/mend-wait.1 +1 -1
  90. package/man/mend-workspace-replace.1 +1 -1
  91. package/man/mend-worktrees-rm.1 +1 -1
  92. package/man/mend-worktrees.1 +1 -1
  93. package/man/mend.1 +2 -2
  94. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -54,6 +54,8 @@ import {
54
54
  repositoryCloneUrlIssue,
55
55
  sameGrant,
56
56
  serviceStartCorrelation,
57
+ serviceViewAttempt,
58
+ serviceViewStatus,
57
59
  servicesHoldOf,
58
60
  sessionDisplayName,
59
61
  sessionWorkspaceLines,
@@ -71,14 +73,14 @@ import {
71
73
  workspaceRetirementLine,
72
74
  worktreeJoinLine,
73
75
  worktreeListsPeople
74
- } from "./chunk-DHM7VXTR.js";
76
+ } from "./chunk-GF5A6W2Y.js";
75
77
 
76
78
  // src/main.ts
77
79
  import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
78
80
  import * as crypto from "node:crypto";
79
81
  import * as fs21 from "node:fs";
80
82
  import { createRequire } from "node:module";
81
- import * as net5 from "node:net";
83
+ import * as net6 from "node:net";
82
84
  import * as os13 from "node:os";
83
85
  import * as path21 from "node:path";
84
86
 
@@ -102,12 +104,12 @@ var claudeMemoryDirFor = (repoRoot, home = claudeHome()) => {
102
104
  var scanClaudeMemory = (dir) => {
103
105
  const files = [];
104
106
  const notes = [];
105
- const walk2 = (abs, rel) => {
107
+ const walk3 = (abs, rel) => {
106
108
  for (const entry of fs.readdirSync(abs, { withFileTypes: true }).toSorted((a, b) => a.name < b.name ? -1 : 1)) {
107
109
  const at = rel === "" ? entry.name : `${rel}/${entry.name}`;
108
110
  const full = path.join(abs, entry.name);
109
111
  if (entry.isDirectory()) {
110
- walk2(full, at);
112
+ walk3(full, at);
111
113
  continue;
112
114
  }
113
115
  if (!entry.isFile()) continue;
@@ -126,7 +128,7 @@ var scanClaudeMemory = (dir) => {
126
128
  );
127
129
  }
128
130
  };
129
- walk2(dir, "");
131
+ walk3(dir, "");
130
132
  return { dir, files, notes };
131
133
  };
132
134
  var machineIdFor = (configDir, create) => {
@@ -1280,7 +1282,15 @@ var isTenancy = (value) => TENANCIES.some((tenancy) => tenancy === value);
1280
1282
  var EDGE_IMAGE = "caddy:2.10-alpine";
1281
1283
  var EDGE_COMPOSE_FILE = "compose.edge.yaml";
1282
1284
  var POSTURE_COMPOSE_FILE = "compose.posture.yaml";
1283
- var DECLARABLE_ITEMS = ["core-private", "edge-tls", "workspace-ssh"];
1285
+ var T3_GATEWAY_COMPOSE_FILE = "compose.t3.yaml";
1286
+ var T3_GATEWAY_CONTAINER_PORT = 3120;
1287
+ var DEFAULT_T3_GATEWAY_PORT = 3120;
1288
+ var DECLARABLE_ITEMS = [
1289
+ "core-private",
1290
+ "edge-tls",
1291
+ "workspace-ssh",
1292
+ "t3code-gateway"
1293
+ ];
1284
1294
  var isDeclarableItem = (value) => DECLARABLE_ITEMS.some((item) => item === value);
1285
1295
  var publishedAddress = (address, port) => `${net2.isIP(address) === 6 ? `[${address}]` : address}:${port}`;
1286
1296
  var parseEdgeHost = (value) => {
@@ -1307,6 +1317,7 @@ var postureEnvironment = (posture) => {
1307
1317
  };
1308
1318
  var postureEnvLines = (posture) => [
1309
1319
  ...posture.edgeHost === void 0 ? [] : [`MEND_EDGE_HOST=${posture.edgeHost}`],
1320
+ ...posture.t3GatewayPort === void 0 ? [] : [`MEND_T3_GATEWAY_PORT=${posture.t3GatewayPort}`],
1310
1321
  ...postureEnvironment(posture).map(([key, value]) => `${key}=${value}`)
1311
1322
  ];
1312
1323
  var renderPostureOverlay = (posture) => {
@@ -1326,10 +1337,33 @@ var renderPostureOverlay = (posture) => {
1326
1337
  ""
1327
1338
  ].join("\n");
1328
1339
  };
1340
+ var T3_GATEWAY_VOLUME = "mend-t3-gateway";
1341
+ var T3_GATEWAY_IMAGE_LABEL = "dev.sealant.mend.t3-gateway";
1342
+ var T3_GATEWAY_STATE_DIR = "/opt/mend-t3-gateway/state";
1343
+ var renderT3GatewayOverlay = (posture) => {
1344
+ if (posture.t3GatewayPort === void 0) return void 0;
1345
+ return [
1346
+ "# Written by mend server setup --t3-gateway: the t3code gateway (docs/adr/0012), its own",
1347
+ "# listener beside Mend's, published on loopback only. Reaching it from elsewhere is an",
1348
+ "# exposure the operator declares (docs/adr/0004). mend server setup --no-t3-gateway removes it.",
1349
+ "services:",
1350
+ " mend:",
1351
+ " environment:",
1352
+ ' MEND_T3_GATEWAY_ENABLED: "true"',
1353
+ " ports:",
1354
+ ` - "127.0.0.1:\${MEND_T3_GATEWAY_PORT:?set MEND_T3_GATEWAY_PORT in server.env}:${T3_GATEWAY_CONTAINER_PORT}"`,
1355
+ " volumes:",
1356
+ ` - ${T3_GATEWAY_VOLUME}:${T3_GATEWAY_STATE_DIR}`,
1357
+ "volumes:",
1358
+ ` ${T3_GATEWAY_VOLUME}:`,
1359
+ ""
1360
+ ].join("\n");
1361
+ };
1329
1362
  var composeOverlays = (posture) => [
1330
1363
  ...posture.edgeHost === void 0 ? [] : [EDGE_COMPOSE_FILE],
1331
1364
  ...renderPostureOverlay(posture) === void 0 ? [] : [POSTURE_COMPOSE_FILE],
1332
- ...runsMirrors(posture.mirrors) ? [MIRRORS_COMPOSE_FILE] : []
1365
+ ...runsMirrors(posture.mirrors) ? [MIRRORS_COMPOSE_FILE] : [],
1366
+ ...posture.t3GatewayPort === void 0 ? [] : [T3_GATEWAY_COMPOSE_FILE]
1333
1367
  ];
1334
1368
  var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? new Map(Object.entries(value)) : null;
1335
1369
  var healthPosture = (body) => {
@@ -1353,6 +1387,9 @@ var healthPosture = (body) => {
1353
1387
  };
1354
1388
  var declaredPostureLines = (posture) => [
1355
1389
  ...posture.edgeHost === void 0 ? [] : [`edge \xB7 ${posture.edgeHost} \xB7 ${EDGE_IMAGE} on 80 and 443 \xB7 Mend's own port on loopback`],
1390
+ ...posture.t3GatewayPort === void 0 ? [] : [
1391
+ `t3code gateway \xB7 on \xB7 127.0.0.1:${posture.t3GatewayPort} \xB7 loopback only \xB7 reaching it from elsewhere is an exposure you declare`
1392
+ ],
1356
1393
  `exposure \xB7 declared ${posture.exposure ?? "private"}${posture.exposure === void 0 ? " \xB7 the default, not set on this install" : ""}`,
1357
1394
  `tenancy \xB7 declared ${posture.tenancy ?? "single"}${posture.tenancy === void 0 ? " \xB7 the default, not set on this install" : ""}`,
1358
1395
  ...posture.sshBind === void 0 || posture.sshPort === void 0 ? [] : [
@@ -2328,6 +2365,7 @@ var COMMANDS = [
2328
2365
  synopsis: ["[--no-tunnel]"],
2329
2366
  description: [
2330
2367
  "A full-screen view of every project and session, updating live. The session pane takes three quarters of the screen: a read-only detail for the selected session with the conversation record it has written so far. The remaining quarter is a sidebar of three stacked sections, projects then worktrees then sessions, where the section you are in stands open and the other two fold to the line that says what is selected. Bare mend with no command opens the same thing.",
2368
+ "Every pane carries its number in its title, as lazygit's do: [1] projects, [2] worktrees, [3] sessions, and [0] the session pane. A digit jumps to its pane, tab and Shift+Tab cycle through them and come back round, and esc goes back to where you came from. ? lists every key, / filters the focused list (enter keeps the filter, esc clears it), and + and _ cycle the screen mode: normal, half (the sidebar takes half the width) and full (the side you are on takes the screen).",
2331
2369
  "Moving the selection only ever navigates: arrows or j/k move inside the open section, enter and the arrows move between the sidebar and the session pane, and nothing takes this terminal until you ask. Reading the record leaves the sidebar as it was. The verbs are a to attach a live session, r to resume a settled one, n for another session in the selected worktree, w for a new worktree, e to rename, v to review the change, o to open it in the browser, Shift+K to stop (on a row whose agent has stopped while its Services keep the workspace up, Shift+K stops those Services), Shift+D to remove, and Shift+R to refresh.",
2332
2370
  "Nothing is ever squeezed. A terminal too narrow for both gives the whole width to the side you are on, a terminal too short for three drawn panes shows the open section alone, and a one-line breadcrumb states whatever did not fit.",
2333
2371
  "On a server that is not this machine, the selected session's live Services declared --http or --https are tunneled to this machine's loopback while it stays selected, and the session pane shows where each one opens, for example web \u2192 http://localhost:5173.",
@@ -2342,7 +2380,7 @@ var COMMANDS = [
2342
2380
  summary: "the dashboard, with snake over it",
2343
2381
  synopsis: [],
2344
2382
  description: [
2345
- "Opens the dashboard with a game of snake floating over it. The arrows steer, space pauses, and esc or q closes the game and leaves you in the dashboard, which has been live underneath the whole time. The same game appears on its own in the session pane while a session is starting: a first session on a new project setup builds its image before it boots, about seven minutes on some families.",
2383
+ "Opens the dashboard with a game of snake floating over it. It counts down 3, 2, 1, go before the snake moves. The arrows or h j k l steer, space or p pauses (and counts down again to resume), and esc or q closes the game and leaves you in the dashboard, which has been live underneath the whole time. The same game appears in the session pane while a session is starting: a first session on a new project setup builds its image before it boots, about seven minutes on some families. A start or resume from the dashboard hands the game the keyboard, as does enter or \u2192 into that pane; esc or q gives it back to the list you were in.",
2346
2384
  "Needs Node 26 or newer, like the dashboard."
2347
2385
  ],
2348
2386
  see: ["ui"]
@@ -3161,7 +3199,7 @@ var COMMANDS = [
3161
3199
  summary: "how this instance is exposed, as declared and as observed",
3162
3200
  synopsis: [],
3163
3201
  description: [
3164
- "Operator only. States MEND_EXPOSURE as declared (loopback, private or public) and one line per item of the public exposure gate, with how it was established: observed (this server read it), carried (this build contains it, and the server cannot see it in effect), declared (you stated it and the server cannot check it), or open. MEND_EXPOSURE=public refuses to start while an item that blocks a start is open: every item the server can observe, and workspace-ssh until you declare it. Items it cannot observe say what would verify them; once you have verified core-private, edge-tls or workspace-ssh (SSH published apart from the web port) from outside, name it in MEND_EXPOSURE_DECLARED, or with mend server setup --declare. The report is what was observed; it is not a statement that the instance is fit to expose."
3202
+ "Operator only. States MEND_EXPOSURE as declared (loopback, private or public) and one line per item of the public exposure gate, with how it was established: observed (this server read it), carried (this build contains it, and the server cannot see it in effect), declared (you stated it and the server cannot check it), or open. MEND_EXPOSURE=public refuses to start while an item that blocks a start is open: every item the server can observe, and workspace-ssh until you declare it. Items it cannot observe say what would verify them; once you have verified core-private, edge-tls, workspace-ssh (SSH published apart from the web port) or (while it is enabled) t3code-gateway from outside, name it in MEND_EXPOSURE_DECLARED, or with mend server setup --declare. The report is what was observed; it is not a statement that the instance is fit to expose."
3165
3203
  ],
3166
3204
  see: ["operator gate", "doctor"]
3167
3205
  },
@@ -3245,20 +3283,28 @@ var COMMANDS = [
3245
3283
  {
3246
3284
  name: "server setup",
3247
3285
  section: "this machine",
3248
- summary: "install or repair the local Mend server",
3286
+ summary: "install or change the local Mend server, guided",
3249
3287
  synopsis: [
3250
- "[--context <name>] [--version <version|latest>] [--bind <ip>] [--ssh-bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--edge <host> | --no-edge] [--exposure <loopback|private|public>] [--tenancy <single|multi>] [--declare <item>...] [--npm-mirror | --no-npm-mirror] [--npm-mirror-max-size <size>] [--docker-mirror | --no-docker-mirror] [--docker-mirror-max-size <size>] [--docker-hub-username <name> --docker-hub-token-stdin --docker-hub-public-only | --no-docker-hub-login] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
3288
+ "",
3289
+ "[--yes] [--context <name>] [--version <version|latest>] [--bind <ip>] [--ssh-bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--edge <host> | --no-edge] [--exposure <loopback|private|public>] [--tenancy <single|multi>] [--declare <item>...] [--undeclare <item>...] [--t3-gateway [--t3-gateway-port <n>] | --no-t3-gateway] [--npm-mirror | --no-npm-mirror] [--npm-mirror-max-size <size>] [--docker-mirror | --no-docker-mirror] [--docker-mirror-max-size <size>] [--docker-hub-username <name> --docker-hub-token-stdin --docker-hub-public-only | --no-docker-hub-login] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
3251
3290
  ],
3252
3291
  description: [
3292
+ "On a terminal with no flags, setup asks its questions in plain words, one at a time: how people reach this Mend (just this machine, your private network or Tailscale, or the public internet with HTTPS), whether VS Code Remote-SSH reaches it from other machines, the T3 Code gateway, the mirrors, and one organization or several. It looks before it asks and says what it observed: Tailscale's name and address and what Tailscale Serve forwards, where your domain resolves, and whether ports 80 and 443 are taken. It never asks for an address to bind: that follows from the answers. On an existing install it shows what is saved and lets you keep it, change one thing, or go through every question. It ends with what changes, in words, and the same command with flags; nothing applies until you say yes.",
3293
+ "Flags are for scripts and CI, and teach what the questions do. Each changes only what it names; everything else is kept from the saved install, so mend server setup --t3-gateway turns the gateway on and nothing more. When there is an install, a run with flags says what it changes. With no terminal and no flags, a fresh install is refused rather than guessed: --yes takes the defaults.",
3253
3294
  "Checks a local Unix-socket Docker context and the Compose plugin, downloads the compose and Postgres initialization assets for one Mend release, preserves existing data and secrets, and starts the server. Re-running repairs the same pinned version. A changed --version is refused; use mend server upgrade. Updating this CLI never updates an existing server pin.",
3254
3295
  "The default listens only on localhost at http://localhost:3105. Non-local access requires both --bind and --url. Every extra browser origin must be named with --origin; setup never guesses from the request Host header or network interfaces.",
3255
3296
  "--edge <host> runs a TLS edge in front of Mend: Caddy on ports 80 and 443 of every interface, which obtains and renews a certificate for the host and proxies to Mend's web tier. Mend's own port stays on loopback and the browser origin is https://<host>. The edge's compose overlay and Caddyfile are written into the generation beside compose.yaml, so start, restart and upgrade run them every time. --no-edge takes it away again, and the edge's container with it. A fresh install cannot start with the edge: until the first account exists, registration is open to whoever reaches the origin first, so set up on localhost, create the account, then add the edge.",
3297
+ "--t3-gateway turns on the t3code gateway (docs/adr/0012): t3code's desktop, mobile and web clients add it as an environment, pair with a code from mend pair, and see this Mend's projects and sessions. It runs in the Mend container, confined to a root and a uid of its own, on a listener of its own published on 127.0.0.1 only (port 3120, or --t3-gateway-port): reaching it from another machine is an exposure you put in front of it and declare. Setup refuses it on a Mend image that has no gateway, and says whether it answered. It is kept across reruns and upgrades; --no-t3-gateway turns it off, and its state stays in its own volume, mend-t3-gateway. Off, nothing of it runs.",
3256
3298
  "--exposure declares how the instance is reached, and --tenancy whether one organization or many use it. Both are written into the generation and kept across reruns and upgrades. public needs the edge and an existing first account. With multi, or with public, the multi mode gate's settings follow: MEND_SOURCE_POLICY=tenant and MEND_CAPTURE_REQUIRE_SIZES=true, and public sets MEND_URL_BEARERS=refuse. The server still decides whether it starts, and mend server status shows what it reports.",
3257
3299
  "Two mirrors run beside Mend unless turned off: an npm mirror (nginx caching npmjs.org, capped at 10g by default, least recently used out) and a Docker mirror (a pull-through cache of Docker Hub, capped at 20g by default: over the cap its cache is cleared). Both leave 5g free on their disk: below that, nginx evicts and the Docker mirror pauses, and sessions pull from Docker Hub directly. Neither publishes a host port. Sessions install npm packages and pull Docker Hub images through them; a project's own npm settings win, and a mirror that is down sends sessions upstream instead. Both are kept across reruns and upgrades, and an upgrade adds them to an install from before them. The Docker mirror pulls anonymously unless given a Docker Hub login: --docker-hub-username with the access token piped on standard input, kept in server.env only. The mirror has no login of its own, so every session that reaches it can pull whatever that token can read: setup takes a login only with --docker-hub-public-only, your statement that the token's access permission is Public Repo Read-only. Mend cannot check a token's scope.",
3258
3300
  "Docker Desktop on Linux and macOS, and OrbStack on macOS, expose client-side proxy sockets. Containers use the daemon-side /var/run/docker.sock. --docker-socket overrides detection and is retained on reruns.",
3259
3301
  "Setup holds an exclusive process lock through startup and health checks. A busy lock reports its owner and manual recovery steps. Never remove a live lock. Private configuration uses immutable generations and an atomic active pointer; failed attempts retain credentials and never delete Docker volumes."
3260
3302
  ],
3261
3303
  options: [
3304
+ {
3305
+ flag: "--yes",
3306
+ text: "no questions: a fresh install takes the defaults (this machine only, http://localhost:3105), an existing one keeps what is saved"
3307
+ },
3262
3308
  {
3263
3309
  flag: "--context <name>",
3264
3310
  text: "local Docker context to persist; the global context is unchanged"
@@ -3275,7 +3321,7 @@ var COMMANDS = [
3275
3321
  { flag: "--url <origin>", text: "advertised browser URL, required with a non-loopback bind" },
3276
3322
  {
3277
3323
  flag: "--origin <origin>",
3278
- text: "additional exact browser origin; repeat for more than one"
3324
+ text: "additional exact browser origin; repeat for more than one. The list given replaces the saved one; none clears it"
3279
3325
  },
3280
3326
  { flag: "--port <n>", text: "external web port. Default: 3105" },
3281
3327
  {
@@ -3287,6 +3333,15 @@ var COMMANDS = [
3287
3333
  text: "run the Caddy TLS edge for this DNS name on 80 and 443; the origin becomes https://<host>, --bind stays on loopback, and the first account must already exist"
3288
3334
  },
3289
3335
  { flag: "--no-edge", text: "take a saved edge away; the origin returns to http://localhost" },
3336
+ {
3337
+ flag: "--t3-gateway",
3338
+ text: "run the t3code gateway, published on 127.0.0.1:3120 only; kept across reruns and upgrades"
3339
+ },
3340
+ {
3341
+ flag: "--t3-gateway-port <n>",
3342
+ text: "the gateway's port on 127.0.0.1; implies --t3-gateway"
3343
+ },
3344
+ { flag: "--no-t3-gateway", text: "turn the t3code gateway off; its state stays" },
3290
3345
  {
3291
3346
  flag: "--exposure <v>",
3292
3347
  text: "declare loopback, private or public; kept across reruns and upgrades. public needs --edge and an existing first account"
@@ -3297,7 +3352,11 @@ var COMMANDS = [
3297
3352
  },
3298
3353
  {
3299
3354
  flag: "--declare <item>",
3300
- text: "state a gate item you verified from outside: core-private, edge-tls or workspace-ssh; repeat for more, none clears. Kept across reruns. public with --ssh-bind beyond loopback needs workspace-ssh"
3355
+ text: "state a gate item you verified from outside: core-private, edge-tls, workspace-ssh or t3code-gateway. Adds to what is saved; repeat for more, none clears. public with --ssh-bind beyond loopback needs workspace-ssh"
3356
+ },
3357
+ {
3358
+ flag: "--undeclare <item>",
3359
+ text: "take one statement back; the others stay. Repeat for more"
3301
3360
  },
3302
3361
  {
3303
3362
  flag: "--npm-mirror, --no-npm-mirror",
@@ -3339,7 +3398,18 @@ var COMMANDS = [
3339
3398
  { flag: "--docker-socket <path>", text: "daemon-side socket mount override for diagnostics" }
3340
3399
  ],
3341
3400
  examples: [
3342
- { command: "mend server setup", text: "localhost, using the current local Docker context" },
3401
+ {
3402
+ command: "mend server setup",
3403
+ text: "on a terminal: the questions, then what changes and the same command with flags"
3404
+ },
3405
+ {
3406
+ command: "mend server setup --t3-gateway",
3407
+ text: "on an existing install: turn the T3 Code gateway on, and keep everything else"
3408
+ },
3409
+ {
3410
+ command: "mend server setup --yes",
3411
+ text: "no questions: localhost on a fresh install, using the current local Docker context"
3412
+ },
3343
3413
  {
3344
3414
  command: "mend server setup --context orbstack --bind 0.0.0.0 --url http://100.70.80.90:3105",
3345
3415
  text: "explicit private-network exposure (a tailnet, a LAN, a VPN)"
@@ -5445,9 +5515,9 @@ var resolveServerUrl = async (args, deps) => {
5445
5515
  }
5446
5516
  const readline = await import("node:readline/promises");
5447
5517
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5448
- const typed = (await rl.question(`mend server url ${dim3(`[${deps.defaultUrl}]`)}: `)).trim();
5518
+ const typed2 = (await rl.question(`mend server url ${dim3(`[${deps.defaultUrl}]`)}: `)).trim();
5449
5519
  rl.close();
5450
- const chosen = typed === "" ? deps.defaultUrl : typed;
5520
+ const chosen = typed2 === "" ? deps.defaultUrl : typed2;
5451
5521
  return normalizeServerUrl(chosen) ?? fail3(`"${chosen}" is not a URL the CLI can dial`);
5452
5522
  };
5453
5523
  var loginCommand = async (args, deps) => {
@@ -5603,7 +5673,7 @@ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
5603
5673
  var scanFolder = (dir) => {
5604
5674
  const files = [];
5605
5675
  const skipped = [];
5606
- const walk2 = (relative5) => {
5676
+ const walk3 = (relative5) => {
5607
5677
  const entries = fs12.readdirSync(path12.join(dir, relative5), { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
5608
5678
  for (const entry of entries) {
5609
5679
  const child = relative5 === "" ? entry.name : `${relative5}/${entry.name}`;
@@ -5611,7 +5681,7 @@ var scanFolder = (dir) => {
5611
5681
  skipped.push({ path: child, reason: "symlink" });
5612
5682
  } else if (entry.isDirectory()) {
5613
5683
  if (SKIPPED_DIRECTORIES.has(entry.name)) skipped.push({ path: child, reason: "skipped" });
5614
- else walk2(child);
5684
+ else walk3(child);
5615
5685
  } else if (entry.isFile()) {
5616
5686
  const size = fs12.statSync(path12.join(dir, child)).size;
5617
5687
  if (size > FOLDER_FILE_BYTES) skipped.push({ path: child, reason: "over 1 MiB" });
@@ -5619,7 +5689,7 @@ var scanFolder = (dir) => {
5619
5689
  }
5620
5690
  }
5621
5691
  };
5622
- walk2("");
5692
+ walk3("");
5623
5693
  return { files, skipped };
5624
5694
  };
5625
5695
  var uploadBatches = (files, cap = FOLDER_REQUEST_BYTES) => {
@@ -6233,13 +6303,13 @@ var processRowOf = (detail, processId) => {
6233
6303
  return rows.find((row) => row.exitedAt !== null) ?? rows[0];
6234
6304
  };
6235
6305
  var commandEndOf = (detail, processId = null) => {
6236
- const settled = SETTLED2.has(detail.session.status);
6306
+ const settled2 = SETTLED2.has(detail.session.status);
6237
6307
  if (processId !== null) {
6238
6308
  const target = processRowOf(detail, processId);
6239
6309
  if (target !== void 0 && target.exitedAt !== null) {
6240
6310
  return { processId, status: target.status, exitCode: target.exitCode };
6241
6311
  }
6242
- if (settled) {
6312
+ if (settled2) {
6243
6313
  return { processId, status: detail.session.status, exitCode: target?.exitCode ?? null };
6244
6314
  }
6245
6315
  return null;
@@ -6249,7 +6319,7 @@ var commandEndOf = (detail, processId = null) => {
6249
6319
  if (agent !== null && agent.exitedAt !== null) {
6250
6320
  return { processId: agent.id ?? null, status: agent.status, exitCode: agent.exitCode };
6251
6321
  }
6252
- if (settled) {
6322
+ if (settled2) {
6253
6323
  return {
6254
6324
  processId: agent?.id ?? null,
6255
6325
  status: detail.session.status,
@@ -6559,9 +6629,10 @@ var secretFileLines = (files) => {
6559
6629
 
6560
6630
  // src/server-setup.ts
6561
6631
  import { createHmac, randomBytes } from "node:crypto";
6632
+ import * as dns from "node:dns/promises";
6562
6633
  import * as fs16 from "node:fs";
6563
6634
  import { readFile } from "node:fs/promises";
6564
- import * as net3 from "node:net";
6635
+ import * as net4 from "node:net";
6565
6636
  import * as os10 from "node:os";
6566
6637
  import * as path16 from "node:path";
6567
6638
 
@@ -6779,6 +6850,739 @@ var ownership = async (runtime, input, mode) => {
6779
6850
  var claimServerDockerVolumes = (runtime, input) => ownership(runtime, input, "claim");
6780
6851
  var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "verify");
6781
6852
 
6853
+ // src/server-setup-guide.ts
6854
+ import * as net3 from "node:net";
6855
+ var LOOPBACK = "127.0.0.1";
6856
+ var isLoopbackAddress = (address) => address === "::1" || address.startsWith("127.");
6857
+ var isLoopbackName = (hostname8) => hostname8 === "localhost" || hostname8 === "[::1]" || isLoopbackAddress(hostname8);
6858
+ var sameSet = (left, right) => left.length === right.length && left.every((item) => right.includes(item));
6859
+ var sameMirrors = (left, right) => left.npm?.maxSize === right.npm?.maxSize && left.npm === null === (right.npm === null) && left.docker === null === (right.docker === null) && left.docker?.maxSize === right.docker?.maxSize && dockerMirrorLogin(left.docker) === dockerMirrorLogin(right.docker);
6860
+ var sameSettings = (left, right) => left.bind === right.bind && left.sshBind === right.sshBind && left.appUrl === right.appUrl && sameSet(left.allowedOrigins, right.allowedOrigins) && left.appPort === right.appPort && left.sshPort === right.sshPort && left.edgeHost === right.edgeHost && left.exposure === right.exposure && left.tenancy === right.tenancy && sameSet(left.declared, right.declared) && left.t3GatewayPort === right.t3GatewayPort && sameMirrors(left.mirrors, right.mirrors);
6861
+ var flagsFor = (before, after) => {
6862
+ const flags = [];
6863
+ if (after.edgeHost !== before.edgeHost)
6864
+ flags.push(...after.edgeHost === void 0 ? ["--no-edge"] : ["--edge", after.edgeHost]);
6865
+ if (after.bind !== before.bind) flags.push("--bind", after.bind);
6866
+ if (after.edgeHost === void 0 && after.appUrl !== before.appUrl)
6867
+ flags.push("--url", after.appUrl);
6868
+ if (after.appPort !== before.appPort) flags.push("--port", String(after.appPort));
6869
+ if (after.sshPort !== before.sshPort) flags.push("--ssh-port", String(after.sshPort));
6870
+ if (after.sshBind !== before.sshBind) flags.push("--ssh-bind", after.sshBind ?? after.bind);
6871
+ if (!sameSet(after.allowedOrigins, before.allowedOrigins))
6872
+ flags.push(
6873
+ ...after.allowedOrigins.length === 0 ? ["--origin", "none"] : after.allowedOrigins.flatMap((origin) => ["--origin", origin])
6874
+ );
6875
+ if (after.exposure !== void 0 && after.exposure !== before.exposure)
6876
+ flags.push("--exposure", after.exposure);
6877
+ if (after.tenancy !== void 0 && after.tenancy !== before.tenancy)
6878
+ flags.push("--tenancy", after.tenancy);
6879
+ const added = after.declared.filter((item) => !before.declared.includes(item));
6880
+ const removed = before.declared.filter((item) => !after.declared.includes(item));
6881
+ if (after.declared.length === 0 && removed.length > 0) flags.push("--declare", "none");
6882
+ else {
6883
+ for (const item of added) flags.push("--declare", item);
6884
+ for (const item of removed) flags.push("--undeclare", item);
6885
+ }
6886
+ if (after.t3GatewayPort !== before.t3GatewayPort) {
6887
+ if (after.t3GatewayPort === void 0) flags.push("--no-t3-gateway");
6888
+ else if (before.t3GatewayPort === void 0 && after.t3GatewayPort === DEFAULT_T3_GATEWAY_PORT)
6889
+ flags.push("--t3-gateway");
6890
+ else flags.push("--t3-gateway-port", String(after.t3GatewayPort));
6891
+ }
6892
+ const npmBefore = before.mirrors.npm;
6893
+ const npmAfter = after.mirrors.npm;
6894
+ if (npmAfter === null && npmBefore !== null) flags.push("--no-npm-mirror");
6895
+ if (npmAfter !== null) {
6896
+ const sized = npmAfter.maxSize !== (npmBefore?.maxSize ?? DEFAULT_NPM_MIRROR_MAX_SIZE);
6897
+ if (sized) flags.push("--npm-mirror-max-size", npmAfter.maxSize);
6898
+ else if (npmBefore === null) flags.push("--npm-mirror");
6899
+ }
6900
+ const dockerBefore = before.mirrors.docker;
6901
+ const dockerAfter = after.mirrors.docker;
6902
+ if (dockerAfter === null && dockerBefore !== null) flags.push("--no-docker-mirror");
6903
+ if (dockerAfter !== null) {
6904
+ const sized = dockerAfter.maxSize !== (dockerBefore?.maxSize ?? DEFAULT_DOCKER_MIRROR_MAX_SIZE);
6905
+ if (sized) flags.push("--docker-mirror-max-size", dockerAfter.maxSize);
6906
+ else if (dockerBefore === null) flags.push("--docker-mirror");
6907
+ if (dockerMirrorLogin(dockerBefore) !== void 0 && dockerMirrorLogin(dockerAfter) === void 0)
6908
+ flags.push("--no-docker-hub-login");
6909
+ }
6910
+ return flags;
6911
+ };
6912
+ var shellWord = (word) => /^[A-Za-z0-9_./:@=,+-]+$/.test(word) ? word : `'${word.replaceAll("'", `'\\''`)}'`;
6913
+ var setupCommandOf = (flags) => ["mend", "server", "setup", ...flags.length === 0 ? ["--yes"] : flags.map(shellWord)].join(" ");
6914
+ var sshAt = (settings) => settings.sshBind ?? settings.bind;
6915
+ var sshBeyondThisMachine = (settings) => !isLoopbackAddress(sshAt(settings));
6916
+ var reachOf = (settings) => settings.edgeHost !== void 0 ? `the public internet, over HTTPS at ${settings.edgeHost} (the edge, Caddy, on 80 and 443)` : isLoopbackAddress(settings.bind) ? `this machine only, at ${settings.appUrl}` : `your network, at ${settings.appUrl} (listening on ${settings.bind})`;
6917
+ var exposureOf = (settings) => settings.exposure === void 0 ? "none declared (the server reads private)" : `declared ${settings.exposure}`;
6918
+ var sshOf = (settings) => {
6919
+ const at = publishedAddress(sshAt(settings), settings.sshPort);
6920
+ return sshBeyondThisMachine(settings) ? `published on ${at}, beyond this machine` : `this machine only (${at})`;
6921
+ };
6922
+ var t3Of = (settings) => settings.t3GatewayPort === void 0 ? "off" : `on, at 127.0.0.1:${settings.t3GatewayPort}`;
6923
+ var tenancyOf = (settings) => settings.tenancy === "multi" ? "several organizations (multi)" : "one organization (single)";
6924
+ var mirrorsOf = (mirrors) => {
6925
+ const login2 = dockerMirrorLogin(mirrors.docker);
6926
+ return [
6927
+ `npm ${mirrors.npm === null ? "off" : `on, ${mirrors.npm.maxSize}`}`,
6928
+ `Docker Hub ${mirrors.docker === null ? "off" : `on, ${mirrors.docker.maxSize}${login2 === void 0 ? "" : `, pulls as ${login2}`}`}`
6929
+ ].join(" \xB7 ");
6930
+ };
6931
+ var declaredOf = (settings) => settings.declared.length === 0 ? "nothing" : settings.declared.join(", ");
6932
+ var originsOf = (settings) => settings.allowedOrigins.length === 0 ? "none" : settings.allowedOrigins.join(", ");
6933
+ var rowsOf = (settings) => [
6934
+ ["reached", reachOf(settings)],
6935
+ ["exposure", exposureOf(settings)],
6936
+ ["workspace SSH", sshOf(settings)],
6937
+ ["T3 Code gateway", t3Of(settings)],
6938
+ ["organizations", tenancyOf(settings)],
6939
+ ["mirrors", mirrorsOf(settings.mirrors)],
6940
+ ["you declared", declaredOf(settings)],
6941
+ ["extra origins", originsOf(settings)]
6942
+ ];
6943
+ var LABEL_WIDTH2 = 17;
6944
+ var settingsLines = (settings) => rowsOf(settings).filter(
6945
+ ([label]) => (label !== "you declared" || settings.exposure === "public" || settings.declared.length > 0) && (label !== "extra origins" || settings.allowedOrigins.length > 0)
6946
+ ).map(([label, value]) => ` ${label.padEnd(LABEL_WIDTH2)}${value}`);
6947
+ var headlineOf = (settings) => [
6948
+ settings.edgeHost !== void 0 ? `public HTTPS at ${settings.edgeHost}` : isLoopbackAddress(settings.bind) ? "this machine only" : `your network at ${settings.appUrl}`,
6949
+ `VS Code SSH from other machines ${sshBeyondThisMachine(settings) ? "on" : "off"}`,
6950
+ `T3 gateway ${settings.t3GatewayPort === void 0 ? "off" : "on"}`
6951
+ ].join(", ");
6952
+ var changeLines = (before, after) => {
6953
+ const was = new Map(rowsOf(before));
6954
+ return rowsOf(after).flatMap(([label, value]) => {
6955
+ const previous = was.get(label);
6956
+ return previous === value ? [] : [` ${label}: ${previous ?? ""} \u2192 ${value}`];
6957
+ });
6958
+ };
6959
+ var fieldsOf = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? new Map(Object.entries(value)) : /* @__PURE__ */ new Map();
6960
+ var parseJson2 = (text) => {
6961
+ try {
6962
+ return JSON.parse(text);
6963
+ } catch {
6964
+ return null;
6965
+ }
6966
+ };
6967
+ var loopbackPortOf = (target) => {
6968
+ if (/^\d+$/.test(target)) return Number(target);
6969
+ try {
6970
+ const url = new URL(/^[a-z+]+:\/\//.test(target) ? target : `http://${target}`);
6971
+ if (!isLoopbackName(url.hostname) || url.port === "") return null;
6972
+ return Number(url.port);
6973
+ } catch {
6974
+ return null;
6975
+ }
6976
+ };
6977
+ var tailscaleFactsOf = (status, serve) => {
6978
+ const fields = fieldsOf(parseJson2(status));
6979
+ if (!fields.has("BackendState")) return null;
6980
+ const self = fieldsOf(fields.get("Self"));
6981
+ const dns2 = self.get("DNSName");
6982
+ const ips = self.get("TailscaleIPs");
6983
+ const addresses = Array.isArray(ips) ? ips.filter((ip) => typeof ip === "string") : [];
6984
+ const ipv4 = addresses.find((ip) => net3.isIPv4(ip));
6985
+ const ipv6 = addresses.find((ip) => net3.isIPv6(ip));
6986
+ const serveFields = fieldsOf(parseJson2(serve));
6987
+ const funnel = fieldsOf(serveFields.get("AllowFunnel"));
6988
+ const routes = [...fieldsOf(serveFields.get("Web")).entries()].flatMap(([hostPort, web]) => {
6989
+ const match = /^(.+):(\d+)$/.exec(hostPort);
6990
+ if (match === null) return [];
6991
+ const [, host = "", port = ""] = match;
6992
+ const root = fieldsOf(fieldsOf(fieldsOf(web).get("Handlers")).get("/"));
6993
+ const proxy = root.get("Proxy");
6994
+ return [
6995
+ {
6996
+ origin: port === "443" ? `https://${host}` : `https://${host}:${port}`,
6997
+ loopbackPort: typeof proxy === "string" ? loopbackPortOf(proxy) : null,
6998
+ funnel: funnel.get(hostPort) === true
6999
+ }
7000
+ ];
7001
+ });
7002
+ return {
7003
+ running: fields.get("BackendState") === "Running",
7004
+ dnsName: typeof dns2 === "string" && dns2 !== "" ? dns2.replace(/\.$/, "") : null,
7005
+ ipv4: ipv4 ?? null,
7006
+ ipv6: ipv6 ?? null,
7007
+ serve: routes
7008
+ };
7009
+ };
7010
+ var GuideStopped = class extends Error {
7011
+ _tag = "GuideStopped";
7012
+ };
7013
+ var answer = async (io, prompt) => {
7014
+ const line = await io.ask(prompt);
7015
+ if (line === null) throw new GuideStopped();
7016
+ return line.trim();
7017
+ };
7018
+ var choose = async (io, question, choices, preferred) => {
7019
+ io.write("");
7020
+ io.write(question);
7021
+ choices.forEach(
7022
+ (choice, index) => io.write(` ${index + 1}. ${choice.label}${choice.detail === "" ? "" : ` \xB7 ${choice.detail}`}`)
7023
+ );
7024
+ const range = choices.length === 2 ? "1 or 2" : `1-${choices.length}`;
7025
+ const fallback = choices[preferred] ?? choices[0];
7026
+ for (; ; ) {
7027
+ const given = await answer(io, ` ${range} [${preferred + 1}]: `);
7028
+ const picked = given === "" ? fallback : /^\d+$/.test(given) ? choices[Number(given) - 1] : void 0;
7029
+ if (picked !== void 0) return picked.value;
7030
+ io.write(` "${given}" is not one of the choices.`);
7031
+ }
7032
+ };
7033
+ var yesNo = async (io, question, preferred) => {
7034
+ for (; ; ) {
7035
+ const given = (await answer(io, `${question} ${preferred ? "[Y/n]" : "[y/N]"} `)).toLowerCase();
7036
+ if (given === "") return preferred;
7037
+ if (given === "y" || given === "yes") return true;
7038
+ if (given === "n" || given === "no") return false;
7039
+ io.write(" y or n.");
7040
+ }
7041
+ };
7042
+ var typed = async (io, question, preferred, parse) => {
7043
+ for (; ; ) {
7044
+ const given = await answer(
7045
+ io,
7046
+ `${question}${preferred === void 0 ? "" : ` [${preferred}]`}: `
7047
+ );
7048
+ const input = given === "" ? preferred : given;
7049
+ if (input === void 0) {
7050
+ io.write(" An answer is needed here.");
7051
+ continue;
7052
+ }
7053
+ const parsed = parse(input);
7054
+ if ("value" in parsed) return parsed.value;
7055
+ io.write(` ${parsed.refused}`);
7056
+ }
7057
+ };
7058
+ var reachModeOf = (settings) => settings.edgeHost !== void 0 ? "public" : isLoopbackAddress(settings.bind) ? "machine" : "network";
7059
+ var without = (declared, item) => declared.filter((entry) => entry !== item);
7060
+ var withItem = (declared, item) => declared.includes(item) ? declared : [...declared, item];
7061
+ var originOf = (input, port) => {
7062
+ const withScheme = /^https?:\/\//.test(input) ? input : `http://${input}`;
7063
+ try {
7064
+ const url = new URL(withScheme);
7065
+ if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "")
7066
+ return null;
7067
+ if (url.pathname !== "/") return null;
7068
+ if (!/^https?:\/\//.test(input) && url.port === "") url.port = String(port);
7069
+ return url.origin;
7070
+ } catch {
7071
+ return null;
7072
+ }
7073
+ };
7074
+ var tailscaleOnce = (observe) => {
7075
+ let facts;
7076
+ return () => facts ??= observe.tailscale();
7077
+ };
7078
+ var tailnetAddress = (tailscale) => tailscale?.running === true ? tailscale.ipv4 ?? tailscale.ipv6 : null;
7079
+ var urlHost = (address) => net3.isIPv6(address) ? `[${address}]` : address;
7080
+ var settled = (settings) => ({
7081
+ ...settings,
7082
+ sshBind: settings.sshBind === settings.bind ? void 0 : settings.sshBind,
7083
+ allowedOrigins: [...new Set(settings.allowedOrigins)].filter(
7084
+ (origin) => origin !== settings.appUrl
7085
+ )
7086
+ });
7087
+ var toThisMachine = (settings) => ({
7088
+ ...settings,
7089
+ bind: isLoopbackAddress(settings.bind) ? settings.bind : LOOPBACK,
7090
+ sshBind: void 0,
7091
+ edgeHost: void 0,
7092
+ appUrl: settings.edgeHost === void 0 && isLoopbackName(new URL(settings.appUrl).hostname) ? settings.appUrl : `http://localhost:${settings.appPort}`,
7093
+ exposure: "loopback",
7094
+ declared: without(settings.declared, "workspace-ssh")
7095
+ });
7096
+ var askNetwork = async (asked, settings) => {
7097
+ const { io } = asked;
7098
+ const tailscale = await asked.tailscale();
7099
+ io.write("");
7100
+ if (tailscale === null) io.write("Observed: tailscale did not answer on this machine.");
7101
+ else if (!tailscale.running)
7102
+ io.write("Observed: Tailscale is installed here and not running (tailscale up starts it).");
7103
+ else
7104
+ io.write(
7105
+ `Observed: Tailscale is up; this machine is ${tailscale.dnsName ?? "unnamed"} at ${[tailscale.ipv4, tailscale.ipv6].filter((ip) => ip !== null).join(" and ") || "no address"} on your tailnet.`
7106
+ );
7107
+ const tailnet = tailnetAddress(tailscale);
7108
+ const others = asked.context.observe.localAddresses().filter((address) => net3.isIPv4(address) && address !== tailnet);
7109
+ const port = settings.appPort;
7110
+ const listening = reachModeOf(settings) === "network" && settings.bind !== "0.0.0.0" && settings.bind !== tailnet && !others.includes(settings.bind) ? settings.bind : null;
7111
+ const choices = [
7112
+ ...listening === null ? [] : [
7113
+ {
7114
+ label: `where it listens now, ${listening}`,
7115
+ detail: "keeps the address it is published on",
7116
+ value: { bind: listening, host: urlHost(listening) }
7117
+ }
7118
+ ],
7119
+ ...tailnet === null ? [] : [
7120
+ {
7121
+ label: `over Tailscale, as ${tailscale?.dnsName ?? tailnet}`,
7122
+ detail: `listens on ${tailnet}, this machine's tailnet address, and is published there only`,
7123
+ value: { bind: tailnet, host: tailscale?.dnsName ?? urlHost(tailnet) }
7124
+ }
7125
+ ],
7126
+ ...others.map((address) => ({
7127
+ label: `on this network address, ${address}`,
7128
+ detail: `listens on ${address} only, and is published on that address`,
7129
+ value: { bind: address, host: address }
7130
+ })),
7131
+ {
7132
+ label: "on every address of this machine",
7133
+ detail: "listens on 0.0.0.0, every address: who reaches it is up to your network and firewall",
7134
+ value: { bind: "0.0.0.0", host: null }
7135
+ }
7136
+ ];
7137
+ const current = choices.findIndex((choice) => choice.value.bind === settings.bind);
7138
+ const picked = await choose(
7139
+ io,
7140
+ "Which network do people reach it on?",
7141
+ choices,
7142
+ current < 0 ? 0 : current
7143
+ );
7144
+ const keepsUrl = picked.bind === settings.bind && reachModeOf(settings) === "network";
7145
+ const preferred = keepsUrl ? settings.appUrl : picked.host !== null ? `http://${picked.host}:${port}` : others[0] === void 0 ? void 0 : `http://${others[0]}:${port}`;
7146
+ const appUrl = await typed(
7147
+ io,
7148
+ "The address people open in a browser",
7149
+ preferred,
7150
+ (input) => {
7151
+ const origin = originOf(input, port);
7152
+ if (origin === null)
7153
+ return { refused: `"${input}" is not a name, an address or an http(s) origin.` };
7154
+ if (isLoopbackName(new URL(origin).hostname))
7155
+ return {
7156
+ refused: "That address names this machine itself; give a name or an address other machines use."
7157
+ };
7158
+ return { value: origin };
7159
+ }
7160
+ );
7161
+ const followsWeb = settings.sshBind === void 0 && !isLoopbackAddress(settings.bind);
7162
+ return settled({
7163
+ ...settings,
7164
+ bind: picked.bind,
7165
+ sshBind: followsWeb || reachModeOf(settings) === "machine" ? void 0 : settings.sshBind ?? settings.bind,
7166
+ edgeHost: void 0,
7167
+ appUrl,
7168
+ exposure: "private"
7169
+ });
7170
+ };
7171
+ var FRESH_PUBLIC = [
7172
+ "A fresh install starts on this machine first. Until the first account exists, whoever reaches",
7173
+ "the address first can register it, and the server refuses to start as public without one.",
7174
+ "Setup installs Mend on this machine now. Create the first account at the address it prints,",
7175
+ "then run mend server setup again and choose the public internet."
7176
+ ];
7177
+ var askPublic = async (asked, settings) => {
7178
+ const { io } = asked;
7179
+ io.write("");
7180
+ io.write(
7181
+ "Mend runs an edge, Caddy, on ports 80 and 443 of every address. It gets and renews a certificate for your domain and forwards to Mend, which stays on 127.0.0.1."
7182
+ );
7183
+ const domain = await typed(io, "Your domain for Mend", settings.edgeHost, (input) => {
7184
+ const host = parseEdgeHost(input);
7185
+ return host === null ? {
7186
+ refused: `"${input}" is not a DNS name a certificate can be issued for, such as mend.example.com.`
7187
+ } : { value: host };
7188
+ });
7189
+ const resolved = await asked.context.observe.lookupHost(domain);
7190
+ const own = asked.context.observe.localAddresses();
7191
+ if (resolved === null || resolved.length === 0)
7192
+ io.write(
7193
+ `Observed: ${domain} does not resolve from this machine. Caddy cannot get a certificate until its DNS points here.`
7194
+ );
7195
+ else if (resolved.some((address) => own.includes(address)))
7196
+ io.write(`Observed: ${domain} resolves to ${resolved.join(", ")}, an address of this machine.`);
7197
+ else
7198
+ io.write(
7199
+ `Observed: ${domain} resolves to ${resolved.join(", ")}; this machine's own addresses are ${own.length === 0 ? "none" : own.join(", ")}. Setup cannot see a NAT or a load balancer in front of this machine; Caddy gets its certificate once 80 and 443 on this machine answer from the internet.`
7200
+ );
7201
+ if (settings.edgeHost === void 0) {
7202
+ const taken = [];
7203
+ for (const port of [80, 443]) if (await asked.context.observe.portTaken(port)) taken.push(port);
7204
+ io.write(
7205
+ taken.length === 0 ? "Observed: nothing on this machine listens on 80 or 443 yet." : `Observed: something on this machine already listens on ${taken.join(" and ")}; the edge needs ${taken.length === 1 ? "it" : "them"}.`
7206
+ );
7207
+ }
7208
+ const publicSettings = settled({
7209
+ ...settings,
7210
+ edgeHost: domain,
7211
+ appUrl: `https://${domain}`,
7212
+ bind: isLoopbackAddress(settings.bind) ? settings.bind : LOOPBACK,
7213
+ sshBind: reachModeOf(settings) === "machine" ? void 0 : sshAt(settings),
7214
+ exposure: "public"
7215
+ });
7216
+ return askSsh(asked, publicSettings);
7217
+ };
7218
+ var askSsh = async (asked, settings) => {
7219
+ const { io } = asked;
7220
+ const tailnet = tailnetAddress(await asked.tailscale());
7221
+ const port = settings.sshPort;
7222
+ const web = isLoopbackAddress(settings.bind) ? null : settings.bind;
7223
+ const candidates = [
7224
+ {
7225
+ label: "no, this machine only",
7226
+ detail: `SSH on ${publishedAddress(LOOPBACK, port)}`,
7227
+ value: LOOPBACK
7228
+ },
7229
+ ...web === null || web === "0.0.0.0" || web === tailnet ? [] : [
7230
+ {
7231
+ label: "yes, on the same address as the web",
7232
+ detail: `SSH on ${publishedAddress(web, port)}`,
7233
+ value: web
7234
+ }
7235
+ ],
7236
+ ...tailnet === null ? [] : [
7237
+ {
7238
+ label: "yes, over Tailscale only",
7239
+ detail: `SSH on ${publishedAddress(tailnet, port)}, the tailnet address`,
7240
+ value: tailnet
7241
+ }
7242
+ ],
7243
+ {
7244
+ label: "yes, from any network",
7245
+ detail: `SSH on ${publishedAddress("0.0.0.0", port)}, every address: who reaches it is up to your network and firewall`,
7246
+ value: "0.0.0.0"
7247
+ }
7248
+ ];
7249
+ const at = sshAt(settings);
7250
+ const choices = isLoopbackAddress(at) || candidates.some((choice) => choice.value === at) ? candidates : [
7251
+ ...candidates,
7252
+ {
7253
+ label: "yes, where it is published now",
7254
+ detail: `SSH on ${publishedAddress(at, port)}`,
7255
+ value: at
7256
+ }
7257
+ ];
7258
+ const current = isLoopbackAddress(at) ? 0 : choices.findIndex((choice) => choice.value === at);
7259
+ const chosen = await choose(
7260
+ io,
7261
+ `Should VS Code Remote-SSH and mend ssh reach sessions from other machines?${settings.edgeHost === void 0 ? " Workspace SSH" : " The edge carries HTTPS only; workspace SSH"} is a port of its own.`,
7262
+ choices,
7263
+ current < 0 ? 0 : current
7264
+ );
7265
+ const onLoopback = () => settled({
7266
+ ...settings,
7267
+ sshBind: isLoopbackAddress(settings.bind) ? void 0 : LOOPBACK,
7268
+ declared: without(settings.declared, "workspace-ssh")
7269
+ });
7270
+ if (isLoopbackAddress(chosen)) return onLoopback();
7271
+ if (settings.exposure !== "public") return settled({ ...settings, sshBind: chosen });
7272
+ const declared = settings.declared.includes("workspace-ssh") && at === chosen;
7273
+ const checked = await choose(
7274
+ io,
7275
+ `Mend cannot see who reaches ${publishedAddress(chosen, port)} from outside, and a public server does not start until you state that you checked (the exposure gate's workspace-ssh item).`,
7276
+ [
7277
+ {
7278
+ label: "I checked it from a network that should not reach it",
7279
+ detail: "declares workspace-ssh",
7280
+ value: true
7281
+ },
7282
+ { label: "not yet", detail: "SSH stays on this machine for now", value: false }
7283
+ ],
7284
+ declared ? 0 : 1
7285
+ );
7286
+ return checked ? settled({
7287
+ ...settings,
7288
+ sshBind: chosen,
7289
+ declared: withItem(settings.declared, "workspace-ssh")
7290
+ }) : onLoopback();
7291
+ };
7292
+ var offerServeOrigins = async (asked, settings) => {
7293
+ const tailscale = await asked.tailscale();
7294
+ let next = settings;
7295
+ for (const route of tailscale?.serve ?? []) {
7296
+ if (route.loopbackPort !== settings.appPort || route.origin === next.appUrl || next.allowedOrigins.includes(route.origin))
7297
+ continue;
7298
+ asked.io.write("");
7299
+ asked.io.write(
7300
+ `Observed: Tailscale Serve forwards ${route.origin} to Mend's port here${route.funnel ? "; Tailscale's settings have Funnel on for it, which publishes it to the public internet as well as your tailnet" : ""}.`
7301
+ );
7302
+ if (await yesNo(asked.io, `Allow ${route.origin} as a browser origin too?`, true))
7303
+ next = { ...next, allowedOrigins: [...next.allowedOrigins, route.origin] };
7304
+ }
7305
+ return next;
7306
+ };
7307
+ var askReach = async (asked, settings) => {
7308
+ const reach = await choose(
7309
+ asked.io,
7310
+ "How will people reach this Mend?",
7311
+ [
7312
+ {
7313
+ label: "just this machine",
7314
+ detail: `a browser here opens http://localhost:${settings.appPort}`,
7315
+ value: "machine"
7316
+ },
7317
+ {
7318
+ label: "my private network or Tailscale",
7319
+ detail: "a tailnet, a LAN or a VPN you control who joins",
7320
+ value: "network"
7321
+ },
7322
+ {
7323
+ label: "the public internet, with HTTPS",
7324
+ detail: "your domain, and an edge that gets its certificate",
7325
+ value: "public"
7326
+ }
7327
+ ],
7328
+ asked.fresh ? 0 : ["machine", "network", "public"].indexOf(reachModeOf(settings))
7329
+ );
7330
+ let next;
7331
+ if (reach === "public" && asked.fresh) {
7332
+ asked.io.write("");
7333
+ for (const line of FRESH_PUBLIC) asked.io.write(line);
7334
+ next = toThisMachine(settings);
7335
+ } else if (reach === "public") next = await askPublic(asked, settings);
7336
+ else if (reach === "network") next = await askSsh(asked, await askNetwork(asked, settings));
7337
+ else next = toThisMachine(settings);
7338
+ return settled(await offerServeOrigins(asked, settled(next)));
7339
+ };
7340
+ var DECLARATIONS = [
7341
+ {
7342
+ item: "core-private",
7343
+ text: () => "Sealant, the database and the mirrors do not answer from the internet"
7344
+ },
7345
+ {
7346
+ item: "edge-tls",
7347
+ text: (settings) => `the certificate for ${settings.edgeHost ?? "the edge"} chains to a public root and renews, and port 80 redirects to https`
7348
+ },
7349
+ {
7350
+ item: "t3code-gateway",
7351
+ text: (settings) => `port ${settings.t3GatewayPort ?? DEFAULT_T3_GATEWAY_PORT} answers nothing from another machine, or only through what you put in front of it`
7352
+ }
7353
+ ];
7354
+ var askDeclarations = async (asked, settings) => {
7355
+ const { io } = asked;
7356
+ io.write("");
7357
+ io.write(
7358
+ "Mend cannot observe these from inside. mend operator exposure lists each as open until you state you checked it from outside; none of them stops a start. Say yes only to what you checked."
7359
+ );
7360
+ let declared = settings.declared;
7361
+ for (const { item, text } of DECLARATIONS) {
7362
+ if (item === "t3code-gateway" && settings.t3GatewayPort === void 0) continue;
7363
+ const checked = await yesNo(
7364
+ io,
7365
+ ` ${item}: ${text(settings)}. Checked?`,
7366
+ declared.includes(item)
7367
+ );
7368
+ declared = checked ? withItem(declared, item) : without(declared, item);
7369
+ }
7370
+ return { ...settings, declared: DECLARABLE_ITEMS.filter((item) => declared.includes(item)) };
7371
+ };
7372
+ var askT3 = async (asked, settings) => {
7373
+ const port = settings.t3GatewayPort ?? DEFAULT_T3_GATEWAY_PORT;
7374
+ asked.io.write("");
7375
+ asked.io.write(
7376
+ `The T3 Code gateway lets t3code's desktop, mobile and web apps pair with this Mend (mend pair). It listens on 127.0.0.1:${port} only; from another machine, use a tunnel you run, such as ssh -L ${port}:127.0.0.1:${port} <this machine>, or tailscale serve.`
7377
+ );
7378
+ const on = await yesNo(
7379
+ asked.io,
7380
+ "Turn on the T3 Code gateway?",
7381
+ settings.t3GatewayPort !== void 0
7382
+ );
7383
+ return on ? { ...settings, t3GatewayPort: port } : {
7384
+ ...settings,
7385
+ t3GatewayPort: void 0,
7386
+ declared: without(settings.declared, "t3code-gateway")
7387
+ };
7388
+ };
7389
+ var askSize = (asked, question, preferred) => typed(asked.io, question, preferred, (input) => {
7390
+ const size = parseMirrorSize(input);
7391
+ return size === null ? { refused: "A whole number of gibibytes or mebibytes, at least 1g, such as 20g or 1536m." } : { value: size };
7392
+ });
7393
+ var askMirrors = async (asked, settings, offerKeep) => {
7394
+ const { io } = asked;
7395
+ const { npm, docker: docker2 } = settings.mirrors;
7396
+ io.write("");
7397
+ io.write(
7398
+ `Mirrors: ${mirrorsOf(settings.mirrors)}. Sessions fetch npm packages and Docker Hub images through them, so each is downloaded once; neither publishes a port.`
7399
+ );
7400
+ if (offerKeep && await yesNo(io, "Keep the mirrors as they are?", true)) return settings;
7401
+ const npmOn = await yesNo(io, "Run the npm mirror?", npm !== null);
7402
+ const npmSize = npmOn ? await askSize(asked, " its disk cap", npm?.maxSize ?? DEFAULT_NPM_MIRROR_MAX_SIZE) : null;
7403
+ const dockerOn = await yesNo(io, "Run the Docker Hub mirror?", docker2 !== null);
7404
+ const dockerSize = dockerOn ? await askSize(asked, " its disk cap", docker2?.maxSize ?? DEFAULT_DOCKER_MIRROR_MAX_SIZE) : null;
7405
+ const login2 = dockerMirrorLogin(docker2);
7406
+ if (dockerOn && login2 !== void 0)
7407
+ io.write(` It keeps pulling as ${login2}; flags change the login (mend help server setup).`);
7408
+ return {
7409
+ ...settings,
7410
+ mirrors: {
7411
+ npm: npmSize === null ? null : { maxSize: npmSize },
7412
+ docker: dockerSize === null ? null : login2 === void 0 ? { maxSize: dockerSize } : { maxSize: dockerSize, upstreamUser: login2, upstreamPublicOnly: true }
7413
+ }
7414
+ };
7415
+ };
7416
+ var askTenancy = async (asked, settings) => {
7417
+ const tenancy = await choose(
7418
+ asked.io,
7419
+ "One organization on this server, or several?",
7420
+ [
7421
+ {
7422
+ label: "one organization",
7423
+ detail: "everyone who signs in shares its projects, folders and audit log",
7424
+ value: "single"
7425
+ },
7426
+ {
7427
+ label: "several organizations",
7428
+ detail: "each with its own members and projects; also sets the multi mode gate's settings",
7429
+ value: "multi"
7430
+ }
7431
+ ],
7432
+ settings.tenancy === "multi" ? 1 : 0
7433
+ );
7434
+ return tenancy === "single" && settings.tenancy === void 0 ? settings : { ...settings, tenancy };
7435
+ };
7436
+ var askOrigins = async (asked, settings) => {
7437
+ let next = await offerServeOrigins(asked, settings);
7438
+ for (; ; ) {
7439
+ asked.io.write("");
7440
+ asked.io.write(
7441
+ `Browser origins besides ${next.appUrl}: ${next.allowedOrigins.length === 0 ? "none" : next.allowedOrigins.join(", ")}.`
7442
+ );
7443
+ const action = await choose(
7444
+ asked.io,
7445
+ "Allow another origin, or take one away?",
7446
+ [
7447
+ { label: "keep these", detail: "", value: "keep" },
7448
+ { label: "add one", detail: "such as a Tailscale Serve name", value: "add" },
7449
+ ...next.allowedOrigins.length === 0 ? [] : [{ label: "remove one", detail: "", value: "remove" }]
7450
+ ],
7451
+ 0
7452
+ );
7453
+ if (action === "keep") return next;
7454
+ if (action === "add") {
7455
+ const origin = await typed(asked.io, "The origin", void 0, (input) => {
7456
+ const parsed = /^https?:\/\//.test(input) ? originOf(input, next.appPort) : null;
7457
+ return parsed === null ? { refused: "An http:// or https:// origin, such as https://mend.tail1234.ts.net:8443." } : { value: parsed };
7458
+ });
7459
+ if (origin !== next.appUrl && !next.allowedOrigins.includes(origin))
7460
+ next = { ...next, allowedOrigins: [...next.allowedOrigins, origin] };
7461
+ continue;
7462
+ }
7463
+ const gone = await choose(
7464
+ asked.io,
7465
+ "Which one?",
7466
+ next.allowedOrigins.map((origin) => ({ label: origin, detail: "", value: origin })),
7467
+ 0
7468
+ );
7469
+ next = { ...next, allowedOrigins: next.allowedOrigins.filter((origin) => origin !== gone) };
7470
+ }
7471
+ };
7472
+ var walk2 = async (asked, settings) => {
7473
+ let next = await askReach(asked, settings);
7474
+ next = await askT3(asked, next);
7475
+ if (next.exposure === "public" && await yesNo(asked.io, "State what you have checked from outside the network now?", false))
7476
+ next = await askDeclarations(asked, next);
7477
+ next = await askMirrors(asked, next, true);
7478
+ return askTenancy(asked, next);
7479
+ };
7480
+ var changeOne = async (asked, settings) => {
7481
+ let next = settings;
7482
+ for (; ; ) {
7483
+ const topic = await choose(
7484
+ asked.io,
7485
+ "What should change?",
7486
+ [
7487
+ { label: "how people reach it", detail: reachOf(next), value: "reach" },
7488
+ ...reachModeOf(next) === "machine" ? [] : [
7489
+ {
7490
+ label: "VS Code Remote-SSH from other machines",
7491
+ detail: sshOf(next),
7492
+ value: "ssh"
7493
+ }
7494
+ ],
7495
+ { label: "the T3 Code gateway", detail: t3Of(next), value: "t3" },
7496
+ ...next.exposure === "public" ? [
7497
+ {
7498
+ label: "what you checked from outside",
7499
+ detail: declaredOf(next),
7500
+ value: "declared"
7501
+ }
7502
+ ] : [],
7503
+ { label: "the mirrors", detail: mirrorsOf(next.mirrors), value: "mirrors" },
7504
+ { label: "one organization or several", detail: tenancyOf(next), value: "tenancy" },
7505
+ { label: "extra browser origins", detail: originsOf(next), value: "origins" }
7506
+ ],
7507
+ 0
7508
+ );
7509
+ if (topic === "reach") next = await askReach(asked, next);
7510
+ else if (topic === "ssh") next = await askSsh(asked, next);
7511
+ else if (topic === "t3") next = await askT3(asked, next);
7512
+ else if (topic === "declared") next = await askDeclarations(asked, next);
7513
+ else if (topic === "mirrors") next = await askMirrors(asked, next, false);
7514
+ else if (topic === "tenancy") next = await askTenancy(asked, next);
7515
+ else next = await askOrigins(asked, next);
7516
+ asked.io.write("");
7517
+ if (!await yesNo(asked.io, "Change something else?", false)) return next;
7518
+ }
7519
+ };
7520
+ var runGuide = async (io, context) => {
7521
+ const base = context.saved ?? context.defaults;
7522
+ const asked = {
7523
+ io,
7524
+ context,
7525
+ tailscale: tailscaleOnce(context.observe),
7526
+ fresh: context.saved === null
7527
+ };
7528
+ try {
7529
+ let target;
7530
+ if (context.saved === null) {
7531
+ io.write("Mend server setup. A few questions; enter takes the answer in brackets.");
7532
+ target = await walk2(asked, base);
7533
+ } else {
7534
+ io.write(`Currently: ${headlineOf(base)}.`);
7535
+ for (const line of settingsLines(base)) io.write(line);
7536
+ const how = await choose(
7537
+ io,
7538
+ "What would you like to do?",
7539
+ [
7540
+ {
7541
+ label: "keep it as it is",
7542
+ detail: "setup checks this install and starts it again",
7543
+ value: "keep"
7544
+ },
7545
+ { label: "change something", detail: "pick it, answer, done", value: "change" },
7546
+ { label: "go through every question", detail: "", value: "walk" }
7547
+ ],
7548
+ 0
7549
+ );
7550
+ target = how === "keep" ? base : how === "change" ? await changeOne(asked, base) : await walk2(asked, base);
7551
+ }
7552
+ target = settled(target);
7553
+ const flags = flagsFor(base, target);
7554
+ let resolved;
7555
+ try {
7556
+ resolved = context.resolve(flags);
7557
+ } catch (cause) {
7558
+ return { _tag: "refused", message: cause instanceof Error ? cause.message : String(cause) };
7559
+ }
7560
+ if (!sameSettings(resolved, target))
7561
+ return {
7562
+ _tag: "refused",
7563
+ message: `Setup could not say these answers as flags: ${setupCommandOf(flags)} would set something else. Nothing changed; please report this.`
7564
+ };
7565
+ io.write("");
7566
+ if (context.saved === null) {
7567
+ io.write("Setup will install:");
7568
+ for (const line of settingsLines(target)) io.write(line);
7569
+ } else {
7570
+ const changes = changeLines(base, target);
7571
+ if (changes.length === 0)
7572
+ io.write("Nothing changes: setup checks this install and starts it as it is.");
7573
+ else {
7574
+ io.write("What changes:");
7575
+ for (const line of changes) io.write(line);
7576
+ }
7577
+ }
7578
+ io.write(`Same as: ${setupCommandOf(flags)}`);
7579
+ return await yesNo(io, "Apply?", true) ? { _tag: "apply", flags } : { _tag: "stopped" };
7580
+ } catch (cause) {
7581
+ if (cause instanceof GuideStopped) return { _tag: "stopped" };
7582
+ throw cause;
7583
+ }
7584
+ };
7585
+
6782
7586
  // src/server-store.ts
6783
7587
  import { randomUUID as randomUUID2 } from "node:crypto";
6784
7588
  import * as fs14 from "node:fs";
@@ -6835,7 +7639,8 @@ var optionalFileKeys = [
6835
7639
  "caddyfile",
6836
7640
  "mirrors",
6837
7641
  "npmMirrorConf",
6838
- "dockerMirrorGuard"
7642
+ "dockerMirrorGuard",
7643
+ "t3Gateway"
6839
7644
  ];
6840
7645
  var fileNames = {
6841
7646
  identity: "identity.env",
@@ -6848,7 +7653,8 @@ var fileNames = {
6848
7653
  caddyfile: "Caddyfile",
6849
7654
  mirrors: "compose.mirrors.yaml",
6850
7655
  npmMirrorConf: "npm-mirror.conf",
6851
- dockerMirrorGuard: "docker-mirror-guard.sh"
7656
+ dockerMirrorGuard: "docker-mirror-guard.sh",
7657
+ t3Gateway: "compose.t3.yaml"
6852
7658
  };
6853
7659
  var fileModes = {
6854
7660
  identity: 384,
@@ -6861,7 +7667,8 @@ var fileModes = {
6861
7667
  caddyfile: 420,
6862
7668
  mirrors: 384,
6863
7669
  npmMirrorConf: 420,
6864
- dockerMirrorGuard: 420
7670
+ dockerMirrorGuard: 420,
7671
+ t3Gateway: 384
6865
7672
  };
6866
7673
  var allFiles = (files) => [...fileKeys, ...optionalFileKeys].map((key) => [key, files[key]]);
6867
7674
  var parseLockOwner = (raw) => {
@@ -6968,6 +7775,7 @@ var readActive = (paths) => {
6968
7775
  const mirrors = optional("mirrors");
6969
7776
  const npmMirrorConf = optional("npmMirrorConf");
6970
7777
  const dockerMirrorGuard = optional("dockerMirrorGuard");
7778
+ const t3Gateway = optional("t3Gateway");
6971
7779
  const files = {
6972
7780
  identity: fs14.readFileSync(path15.join(directory, fileNames.identity), "utf8"),
6973
7781
  config: fs14.readFileSync(path15.join(directory, fileNames.config), "utf8"),
@@ -6979,7 +7787,8 @@ var readActive = (paths) => {
6979
7787
  ...caddyfile === void 0 ? {} : { caddyfile },
6980
7788
  ...mirrors === void 0 ? {} : { mirrors },
6981
7789
  ...npmMirrorConf === void 0 ? {} : { npmMirrorConf },
6982
- ...dockerMirrorGuard === void 0 ? {} : { dockerMirrorGuard }
7790
+ ...dockerMirrorGuard === void 0 ? {} : { dockerMirrorGuard },
7791
+ ...t3Gateway === void 0 ? {} : { t3Gateway }
6983
7792
  };
6984
7793
  if (readIdentity(paths) !== files.identity) {
6985
7794
  throw new ServerStoreError(
@@ -7539,7 +8348,12 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
7539
8348
  "--docker-hub-username",
7540
8349
  "--docker-hub-token-stdin",
7541
8350
  "--docker-hub-public-only",
7542
- "--no-docker-hub-login"
8351
+ "--no-docker-hub-login",
8352
+ "--t3-gateway",
8353
+ "--t3-gateway-port",
8354
+ "--no-t3-gateway",
8355
+ "--undeclare",
8356
+ "--yes"
7543
8357
  ]);
7544
8358
  var SWITCHES = /* @__PURE__ */ new Set([
7545
8359
  "--offline",
@@ -7550,8 +8364,12 @@ var SWITCHES = /* @__PURE__ */ new Set([
7550
8364
  "--no-docker-mirror",
7551
8365
  "--docker-hub-token-stdin",
7552
8366
  "--docker-hub-public-only",
7553
- "--no-docker-hub-login"
8367
+ "--no-docker-hub-login",
8368
+ "--t3-gateway",
8369
+ "--no-t3-gateway",
8370
+ "--yes"
7554
8371
  ]);
8372
+ var REPEATABLE = /* @__PURE__ */ new Set(["--origin", "--declare", "--undeclare"]);
7555
8373
  var parseExposure = (value) => {
7556
8374
  if (!isExposure(value))
7557
8375
  throw setupError(`--exposure must be one of ${EXPOSURES.join(", ")}, not "${value}".`);
@@ -7562,14 +8380,14 @@ var parseTenancy = (value) => {
7562
8380
  throw setupError(`--tenancy must be one of ${TENANCIES.join(", ")}, not "${value}".`);
7563
8381
  return value;
7564
8382
  };
7565
- var parseDeclared = (values) => {
7566
- if (values.length === 1 && values[0] === "none") return [];
8383
+ var parseDeclared = (values, flag) => {
8384
+ if (flag === "--declare" && values.length === 1 && values[0] === "none") return [];
7567
8385
  return [
7568
8386
  ...new Set(
7569
8387
  values.map((value) => {
7570
8388
  if (!isDeclarableItem(value)) {
7571
8389
  throw setupError(
7572
- `--declare takes ${DECLARABLE_ITEMS.join(", ")} or none, not "${value}". Every other gate item is observed by the server, never stated.`
8390
+ `${flag} takes ${DECLARABLE_ITEMS.join(", ")}${flag === "--declare" ? " or none" : ""}, not "${value}". Every other gate item is observed by the server, never stated.`
7573
8391
  );
7574
8392
  }
7575
8393
  return value;
@@ -7601,7 +8419,7 @@ var parseSetupOptions = (args) => {
7601
8419
  const [value, valueIndex] = nextFlagValue(args, index, flag);
7602
8420
  index = valueIndex;
7603
8421
  const previous = values.get(flag) ?? [];
7604
- if (flag !== "--origin" && flag !== "--declare" && previous.length > 0) {
8422
+ if (!REPEATABLE.has(flag) && previous.length > 0) {
7605
8423
  throw setupError(`${flag} may be supplied only once.`);
7606
8424
  }
7607
8425
  previous.push(value);
@@ -7616,6 +8434,14 @@ var parseSetupOptions = (args) => {
7616
8434
  const exposure = flagValue3("--exposure");
7617
8435
  const tenancy = flagValue3("--tenancy");
7618
8436
  const declared = values.get("--declare");
8437
+ const declaredItems = declared === void 0 ? void 0 : parseDeclared(declared, "--declare");
8438
+ const undeclared = parseDeclared(values.get("--undeclare") ?? [], "--undeclare");
8439
+ const both = undeclared.filter((item) => declaredItems?.includes(item) === true);
8440
+ if (both.length > 0) throw setupError(`--declare and --undeclare both name ${both.join(", ")}.`);
8441
+ if (declaredItems?.length === 0 && undeclared.length > 0)
8442
+ throw setupError("--declare none already clears every statement; drop --undeclare.");
8443
+ if (origins !== void 0 && origins.includes("none") && origins.length > 1)
8444
+ throw setupError("--origin none clears the extra origins and goes alone.");
7619
8445
  if (edge !== void 0 && values.has("--no-edge"))
7620
8446
  throw setupError("--edge and --no-edge contradict each other.");
7621
8447
  const pair = (on, off) => {
@@ -7663,13 +8489,18 @@ var parseSetupOptions = (args) => {
7663
8489
  throw setupError(
7664
8490
  `--docker-hub-username and ${noDockerHubLogin ? "--no-docker-hub-login" : "--no-docker-mirror"} contradict each other.`
7665
8491
  );
8492
+ const t3GatewayPort = flagValue3("--t3-gateway-port");
8493
+ if (values.has("--no-t3-gateway") && (values.has("--t3-gateway") || t3GatewayPort !== void 0)) {
8494
+ throw setupError("--no-t3-gateway contradicts --t3-gateway and --t3-gateway-port.");
8495
+ }
7666
8496
  return {
7667
8497
  context: flagValue3("--context"),
7668
8498
  version: version === void 0 ? void 0 : parseVersion(version),
7669
8499
  bind: flagValue3("--bind"),
7670
8500
  sshBind: flagValue3("--ssh-bind"),
7671
8501
  url: flagValue3("--url"),
7672
- origins: origins === void 0 ? void 0 : origins,
8502
+ // `--origin none`: no extra origins at all.
8503
+ origins: origins === void 0 ? void 0 : origins[0] === "none" ? [] : origins,
7673
8504
  appPort: appPort === void 0 ? void 0 : parsePort(appPort, "--port"),
7674
8505
  sshPort: sshPort === void 0 ? void 0 : parsePort(sshPort, "--ssh-port"),
7675
8506
  dockerSocket: flagValue3("--docker-socket"),
@@ -7679,14 +8510,19 @@ var parseSetupOptions = (args) => {
7679
8510
  noEdge: values.has("--no-edge"),
7680
8511
  exposure: exposure === void 0 ? void 0 : parseExposure(exposure),
7681
8512
  tenancy: tenancy === void 0 ? void 0 : parseTenancy(tenancy),
7682
- declared: declared === void 0 ? void 0 : parseDeclared(declared),
8513
+ declared: declaredItems,
8514
+ undeclared,
8515
+ yes: values.has("--yes"),
7683
8516
  npmMirror,
7684
8517
  npmMirrorMaxSize,
7685
8518
  dockerMirror,
7686
8519
  dockerMirrorMaxSize,
7687
8520
  dockerHubUsername,
7688
8521
  dockerHubTokenStdin,
7689
- noDockerHubLogin
8522
+ noDockerHubLogin,
8523
+ t3Gateway: values.has("--t3-gateway"),
8524
+ t3GatewayPort: t3GatewayPort === void 0 ? void 0 : parsePort(t3GatewayPort, "--t3-gateway-port"),
8525
+ noT3Gateway: values.has("--no-t3-gateway")
7690
8526
  };
7691
8527
  };
7692
8528
  var resolveMirrors = (saved, options) => {
@@ -7829,13 +8665,13 @@ var validateExposure = (existing, options) => {
7829
8665
  throw setupError("--port and --ssh-port must use different ports.");
7830
8666
  }
7831
8667
  const bind = options.bind ?? existing?.bind ?? DEFAULT_BIND;
7832
- if (net3.isIP(bind) === 0) {
8668
+ if (net4.isIP(bind) === 0) {
7833
8669
  throw setupError(
7834
8670
  "--bind must be a literal IPv4 or IPv6 address, such as 127.0.0.1 or 0.0.0.0."
7835
8671
  );
7836
8672
  }
7837
8673
  const requestedSshBind = options.sshBind ?? existing?.sshBind;
7838
- if (requestedSshBind !== void 0 && net3.isIP(requestedSshBind) === 0) {
8674
+ if (requestedSshBind !== void 0 && net4.isIP(requestedSshBind) === 0) {
7839
8675
  throw setupError(
7840
8676
  "--ssh-bind must be a literal IPv4 or IPv6 address, such as 127.0.0.1 or 0.0.0.0."
7841
8677
  );
@@ -7844,7 +8680,18 @@ var validateExposure = (existing, options) => {
7844
8680
  const edgeHost = options.noEdge ? void 0 : options.edge ?? existing?.edgeHost;
7845
8681
  const exposure = options.exposure ?? existing?.exposure;
7846
8682
  const tenancy = options.tenancy ?? existing?.tenancy;
7847
- const declared = options.declared ?? existing?.declared ?? [];
8683
+ const t3GatewayPort = options.noT3Gateway ? void 0 : options.t3GatewayPort ?? existing?.t3GatewayPort ?? (options.t3Gateway ? DEFAULT_T3_GATEWAY_PORT : void 0);
8684
+ if (t3GatewayPort !== void 0 && [appPort, sshPort].includes(t3GatewayPort)) {
8685
+ throw setupError(
8686
+ `--t3-gateway-port ${t3GatewayPort} is Mend's own --port or --ssh-port: the gateway needs a port of its own.`
8687
+ );
8688
+ }
8689
+ if (t3GatewayPort !== void 0 && edgeHost !== void 0 && [80, 443].includes(t3GatewayPort)) {
8690
+ throw setupError("With an edge, --t3-gateway-port must not be 80 or 443: the edge has them.");
8691
+ }
8692
+ const saved = existing?.declared ?? [];
8693
+ const stated = options.declared === void 0 ? saved : options.declared.length === 0 ? [] : [.../* @__PURE__ */ new Set([...saved, ...options.declared])];
8694
+ const declared = stated.filter((item) => !options.undeclared.includes(item));
7848
8695
  checkPosture(bind, sshBind ?? bind, { appPort, sshPort }, edgeHost, exposure, declared);
7849
8696
  const appUrl = resolveAppUrl(existing, options, appPort, edgeHost);
7850
8697
  if (edgeHost === void 0)
@@ -7864,9 +8711,31 @@ var validateExposure = (existing, options) => {
7864
8711
  ...edgeHost === void 0 ? {} : { edgeHost },
7865
8712
  ...exposure === void 0 ? {} : { exposure },
7866
8713
  ...tenancy === void 0 ? {} : { tenancy },
7867
- ...declared.length === 0 ? {} : { declared }
8714
+ ...declared.length === 0 ? {} : { declared },
8715
+ ...t3GatewayPort === void 0 ? {} : { t3GatewayPort }
7868
8716
  };
7869
8717
  };
8718
+ var settingsOf = (config) => ({
8719
+ bind: config.bind,
8720
+ sshBind: config.sshBind,
8721
+ appUrl: config.appUrl,
8722
+ allowedOrigins: config.allowedOrigins,
8723
+ appPort: config.appPort,
8724
+ sshPort: config.sshPort,
8725
+ edgeHost: config.edgeHost,
8726
+ exposure: config.exposure,
8727
+ tenancy: config.tenancy,
8728
+ declared: config.declared ?? [],
8729
+ t3GatewayPort: config.t3GatewayPort,
8730
+ mirrors: config.mirrors
8731
+ });
8732
+ var resolveSetupSettings = (existing, flags) => {
8733
+ const options = parseSetupOptions(flags);
8734
+ return settingsOf({
8735
+ ...validateExposure(existing, options),
8736
+ mirrors: resolveMirrors(existing?.mirrors, options)
8737
+ });
8738
+ };
7870
8739
  var parseServerConfig = (raw) => {
7871
8740
  let decoded;
7872
8741
  try {
@@ -7910,6 +8779,7 @@ var parseServerConfig = (raw) => {
7910
8779
  );
7911
8780
  }
7912
8781
  const declared = Array.isArray(declaredField) ? declaredField.filter((item) => isDeclarableItem(String(item))) : [];
8782
+ const t3GatewayPort = fields.has("t3GatewayPort") ? requiredInteger(fields, "t3GatewayPort") : void 0;
7913
8783
  const config = {
7914
8784
  schemaVersion: requiredInteger(fields, "schemaVersion"),
7915
8785
  assetContract: requiredString(fields, "assetContract"),
@@ -7932,7 +8802,8 @@ var parseServerConfig = (raw) => {
7932
8802
  ...exposure === void 0 ? {} : { exposure },
7933
8803
  ...tenancy === void 0 ? {} : { tenancy },
7934
8804
  ...declared.length === 0 ? {} : { declared },
7935
- ...fields.has("mirrors") ? { mirrors: parseMirrorsField(fields.get("mirrors")) } : {}
8805
+ ...fields.has("mirrors") ? { mirrors: parseMirrorsField(fields.get("mirrors")) } : {},
8806
+ ...t3GatewayPort === void 0 ? {} : { t3GatewayPort }
7936
8807
  };
7937
8808
  if (config.schemaVersion !== CONFIG_SCHEMA_VERSION || config.assetContract !== ASSET_CONTRACT && !LEGACY_ASSET_CONTRACTS.has(config.assetContract)) {
7938
8809
  throw setupError(
@@ -7943,6 +8814,8 @@ var parseServerConfig = (raw) => {
7943
8814
  parsePort(String(config.sshPort), "Server config sshPort");
7944
8815
  if (config.registryPort !== void 0)
7945
8816
  parsePort(String(config.registryPort), "Server config registryPort");
8817
+ if (config.t3GatewayPort !== void 0)
8818
+ parsePort(String(config.t3GatewayPort), "Server config t3GatewayPort");
7946
8819
  validateExposure(null, {
7947
8820
  context: void 0,
7948
8821
  version: void 0,
@@ -7960,13 +8833,18 @@ var parseServerConfig = (raw) => {
7960
8833
  exposure: config.exposure,
7961
8834
  tenancy: config.tenancy,
7962
8835
  declared: config.declared,
8836
+ undeclared: [],
8837
+ yes: false,
7963
8838
  npmMirror: void 0,
7964
8839
  npmMirrorMaxSize: void 0,
7965
8840
  dockerMirror: void 0,
7966
8841
  dockerMirrorMaxSize: void 0,
7967
8842
  dockerHubUsername: void 0,
7968
8843
  dockerHubTokenStdin: false,
7969
- noDockerHubLogin: false
8844
+ noDockerHubLogin: false,
8845
+ t3Gateway: false,
8846
+ t3GatewayPort: config.t3GatewayPort,
8847
+ noT3Gateway: false
7970
8848
  });
7971
8849
  return config;
7972
8850
  };
@@ -8307,7 +9185,7 @@ var renderGarage = (secrets, config) => {
8307
9185
  ];
8308
9186
  };
8309
9187
  var namespaceOf = (config) => config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
8310
- var composeHost = (address) => net3.isIP(address) === 6 ? `[${address}]` : address;
9188
+ var composeHost = (address) => net4.isIP(address) === 6 ? `[${address}]` : address;
8311
9189
  var renderSecrets = (secrets, config) => {
8312
9190
  const sshHost = parseUrl(config.appUrl, "Server config appUrl").hostname.replace(/^\[|\]$/g, "");
8313
9191
  const composeBind = composeHost(config.bind);
@@ -8360,12 +9238,22 @@ var validateOverlays = (config, files) => {
8360
9238
  }
8361
9239
  for (const line of postureEnvLines(config)) {
8362
9240
  const key = line.slice(0, line.indexOf("="));
8363
- if (key !== "MEND_EDGE_HOST" && !(files.posture ?? "").includes(` ${key}: \${${key}`)) {
9241
+ if (key !== "MEND_EDGE_HOST" && key !== "MEND_T3_GATEWAY_PORT" && !(files.posture ?? "").includes(` ${key}: \${${key}`)) {
8364
9242
  throw setupError(
8365
9243
  `Server generation is corrupt: compose.posture.yaml does not hand ${key} to Mend.`
8366
9244
  );
8367
9245
  }
8368
9246
  }
9247
+ if (config.t3GatewayPort === void 0 !== (files.t3Gateway === void 0)) {
9248
+ throw setupError(
9249
+ "Server generation is corrupt: compose.t3.yaml does not match the persisted server config."
9250
+ );
9251
+ }
9252
+ if (files.t3Gateway !== void 0 && (!files.t3Gateway.includes('MEND_T3_GATEWAY_ENABLED: "true"') || !files.t3Gateway.includes('"127.0.0.1:${MEND_T3_GATEWAY_PORT'))) {
9253
+ throw setupError(
9254
+ "Server generation is corrupt: compose.t3.yaml is not the gateway overlay on loopback."
9255
+ );
9256
+ }
8369
9257
  if (config.edgeHost === void 0 !== (files.edge === void 0) || config.edgeHost === void 0 !== (files.caddyfile === void 0)) {
8370
9258
  throw setupError(
8371
9259
  "Server generation is corrupt: compose.edge.yaml and Caddyfile do not match the persisted server config."
@@ -8478,7 +9366,8 @@ var readServerInstallationFacts = async (configDir) => {
8478
9366
  ...generation.files.posture === void 0 ? [] : [{ name: "compose.posture.yaml", content: generation.files.posture }],
8479
9367
  ...generation.files.mirrors === void 0 ? [] : [{ name: MIRRORS_COMPOSE_FILE, content: generation.files.mirrors }],
8480
9368
  ...generation.files.npmMirrorConf === void 0 ? [] : [{ name: NPM_MIRROR_CONF_NAME, content: generation.files.npmMirrorConf }],
8481
- ...generation.files.dockerMirrorGuard === void 0 ? [] : [{ name: DOCKER_MIRROR_GUARD_NAME, content: generation.files.dockerMirrorGuard }]
9369
+ ...generation.files.dockerMirrorGuard === void 0 ? [] : [{ name: DOCKER_MIRROR_GUARD_NAME, content: generation.files.dockerMirrorGuard }],
9370
+ ...generation.files.t3Gateway === void 0 ? [] : [{ name: "compose.t3.yaml", content: generation.files.t3Gateway }]
8482
9371
  ],
8483
9372
  envKeys: envKeyNames(generation.files.env)
8484
9373
  };
@@ -8491,6 +9380,7 @@ var readServerInstallationFacts = async (configDir) => {
8491
9380
  var generationFiles = (config, secrets, assets, identity = renderIdentity(secrets)) => {
8492
9381
  const posture = renderPostureOverlay(config);
8493
9382
  const mirrors = renderMirrorsOverlay(config.mirrors);
9383
+ const t3Gateway = renderT3GatewayOverlay(config);
8494
9384
  return {
8495
9385
  identity,
8496
9386
  config: `${JSON.stringify(config, null, 2)}
@@ -8498,6 +9388,7 @@ var generationFiles = (config, secrets, assets, identity = renderIdentity(secret
8498
9388
  env: renderSecrets(secrets, config),
8499
9389
  ...assets,
8500
9390
  ...posture === void 0 ? {} : { posture },
9391
+ ...t3Gateway === void 0 ? {} : { t3Gateway },
8501
9392
  ...config.edgeHost === void 0 ? {} : { edge: EDGE_COMPOSE_OVERLAY, caddyfile: EDGE_CADDYFILE },
8502
9393
  ...mirrors === void 0 ? {} : { mirrors },
8503
9394
  ...config.mirrors === void 0 || config.mirrors.npm === null ? {} : { npmMirrorConf: NPM_MIRROR_CONF },
@@ -8507,10 +9398,22 @@ var generationFiles = (config, secrets, assets, identity = renderIdentity(secret
8507
9398
  var persistSetup = (store, config, secrets, assets) => storeValue(store.prepare(generationFiles(config, secrets, assets)));
8508
9399
  var healthOrigin = (config) => {
8509
9400
  if (config.edgeHost === void 0) return config.appUrl;
8510
- const host = net3.isIP(config.bind) === 6 ? `[${config.bind}]` : config.bind;
9401
+ const host = net4.isIP(config.bind) === 6 ? `[${config.bind}]` : config.bind;
8511
9402
  return `http://${host}:${config.appPort}`;
8512
9403
  };
8513
9404
  var reachableLine = (config) => config.edgeHost === void 0 ? `Mend ${config.serverVersion} is reachable at ${config.appUrl}` : `Mend ${config.serverVersion} answers at ${healthOrigin(config)} on this machine \xB7 the edge is set up for ${config.appUrl}`;
9405
+ var T3_GATEWAY_OBSERVE_ATTEMPTS = 15;
9406
+ var observeT3Gateway = async (runtime, port) => {
9407
+ const url = `http://127.0.0.1:${port}/.well-known/t3/environment`;
9408
+ for (let attempt2 = 0; attempt2 < T3_GATEWAY_OBSERVE_ATTEMPTS; attempt2 += 1) {
9409
+ const response = await runtime.fetchText(url, 2e3);
9410
+ if (response.error === void 0 && response.status === 200) {
9411
+ return `The t3code gateway answered at 127.0.0.1:${port}, observed from this machine; it is published there only. In t3code, add it as an environment at http://127.0.0.1:${port} and pair with a code from mend pair; reaching it from elsewhere is an exposure you put in front of it and declare (docs/adr/0004).`;
9412
+ }
9413
+ if (attempt2 < T3_GATEWAY_OBSERVE_ATTEMPTS - 1) await runtime.sleep(2e3);
9414
+ }
9415
+ return `The t3code gateway did not answer at 127.0.0.1:${port} from this machine within about a minute. Mend runs without it; mend server logs shows what it said, and mend server status looks again.`;
9416
+ };
8514
9417
  var probeHealth = async (runtime, appUrl, expectedVersion) => {
8515
9418
  const healthUrl = `${appUrl}/api/health`;
8516
9419
  let lastFailure = "request did not complete";
@@ -8639,6 +9542,22 @@ var checkLocalImages = async (runtime, installation, policy = "local") => {
8639
9542
  `Image ${image} must carry org.opencontainers.image.version=${config.serverVersion}.`
8640
9543
  );
8641
9544
  }
9545
+ if (config.t3GatewayPort !== void 0) {
9546
+ const gateway = await runtime.run("docker", [
9547
+ "--context",
9548
+ config.dockerContext,
9549
+ "image",
9550
+ "inspect",
9551
+ image,
9552
+ "--format",
9553
+ `{{index .Config.Labels "${T3_GATEWAY_IMAGE_LABEL}"}}`
9554
+ ]);
9555
+ if (gateway.status !== 0 || gateway.stdout.trim() !== "1") {
9556
+ throw setupError(
9557
+ `Mend ${config.serverVersion} has no t3code gateway (its image carries no ${T3_GATEWAY_IMAGE_LABEL} label). Upgrade to a version that has one with mend server upgrade --version <version>, then turn it on; or run mend server setup --no-t3-gateway`
9558
+ );
9559
+ }
9560
+ }
8642
9561
  const postgres = await inspectImage(
8643
9562
  runtime,
8644
9563
  config.dockerContext,
@@ -8896,12 +9815,90 @@ var mirrorsChangedLines = (before, config) => {
8896
9815
  );
8897
9816
  return lines;
8898
9817
  };
8899
- var setupServer = async (args, runtime, store) => {
9818
+ var UNASKED_FRESH_SETUP = "No terminal to ask on and no flags, so setup does not guess how this Mend is reached. Run mend server setup in a terminal to answer its questions, or say it with flags: --yes takes the defaults (this machine only, http://localhost:3105); --bind <address> --url <origin> serves a private network (a tailnet, a LAN, a VPN); for public HTTPS, set up on this machine first, create the first account, then run mend server setup --edge <host> --exposure public. mend help server setup lists every flag.";
9819
+ var refuseLegacyContract = (existing) => {
9820
+ if (existing !== null && existing.config.assetContract !== ASSET_CONTRACT) {
9821
+ throw setupError(
9822
+ `Mend ${existing.config.serverVersion} was installed under the ${existing.config.assetContract} bundle contract. Use mend server upgrade --version latest to move it to ${ASSET_CONTRACT}; setup cannot repair it in place.`
9823
+ );
9824
+ }
9825
+ };
9826
+ var guideObservations = (runtime) => ({
9827
+ tailscale: async () => {
9828
+ const status = await runtime.run("tailscale", ["status", "--json"], { timeoutMs: 5e3 });
9829
+ if (status.stdout.trim() === "") return null;
9830
+ const serve = await runtime.run("tailscale", ["serve", "status", "--json"], {
9831
+ timeoutMs: 5e3
9832
+ });
9833
+ return tailscaleFactsOf(status.stdout, serve.status === 0 ? serve.stdout : "");
9834
+ },
9835
+ lookupHost: runtime.lookupHost ?? (async () => null),
9836
+ localAddresses: runtime.localAddresses ?? (() => []),
9837
+ portTaken: runtime.portTaken ?? (async () => false)
9838
+ });
9839
+ var guidedSetup = async (runtime, prompter) => {
9840
+ if (runtime.platform !== "linux" && runtime.platform !== "darwin") {
9841
+ throw setupError(`mend server setup supports Linux and macOS, not ${runtime.platform}.`);
9842
+ }
9843
+ const read = await withServerStore(
9844
+ runtime.configDir,
9845
+ async (store) => storeValue(readServerInstallation(store)),
9846
+ { create: true }
9847
+ );
9848
+ if (read._tag === "error") return { _tag: "error", message: read.error.message };
9849
+ const existing = read.value;
9850
+ refuseLegacyContract(existing);
9851
+ const outcome = await runGuide(
9852
+ { write: runtime.writeLine, ask: prompter },
9853
+ {
9854
+ saved: existing === null ? null : resolveSetupSettings(existing.config, []),
9855
+ defaults: resolveSetupSettings(null, []),
9856
+ observe: guideObservations(runtime),
9857
+ resolve: (flags) => resolveSetupSettings(existing?.config ?? null, flags)
9858
+ }
9859
+ );
9860
+ if (outcome._tag === "refused") return { _tag: "error", message: outcome.message };
9861
+ if (outcome._tag === "stopped") {
9862
+ runtime.writeLine("Nothing changed.");
9863
+ return { _tag: "ok" };
9864
+ }
9865
+ return underLock(
9866
+ runtime.configDir,
9867
+ (store) => setupServer(outcome.flags, runtime, store, { askedAgainst: existing?.directory ?? null }),
9868
+ { create: true }
9869
+ );
9870
+ };
9871
+ var underLock = async (configDir, operation, options) => {
9872
+ let refused;
9873
+ const result = await withServerStore(
9874
+ configDir,
9875
+ async (store) => {
9876
+ try {
9877
+ await operation(store);
9878
+ } catch (cause) {
9879
+ if (!(cause instanceof ServerSetupError)) throw cause;
9880
+ refused = cause;
9881
+ }
9882
+ },
9883
+ options
9884
+ );
9885
+ if (result._tag === "error") return { _tag: "error", message: result.error.message };
9886
+ return refused === void 0 ? { _tag: "ok" } : { _tag: "error", message: refused.message };
9887
+ };
9888
+ var setupServer = async (args, runtime, store, run2 = {}) => {
8900
9889
  if (runtime.platform !== "linux" && runtime.platform !== "darwin") {
8901
9890
  throw setupError(`mend server setup supports Linux and macOS, not ${runtime.platform}.`);
8902
9891
  }
8903
9892
  const options = parseSetupOptions(args);
8904
9893
  const existing = storeValue(readServerInstallation(store));
9894
+ if (run2.askedAgainst !== void 0 && (existing?.directory ?? null) !== run2.askedAgainst) {
9895
+ throw setupError(
9896
+ "The install changed while you answered: another mend server setup or upgrade ran. This one changed nothing; run mend server setup again to see the install as it is now."
9897
+ );
9898
+ }
9899
+ if (run2.askedAgainst === void 0 && args.length === 0 && existing === null) {
9900
+ throw setupError(UNASKED_FRESH_SETUP);
9901
+ }
8905
9902
  const savedIdentity = storeValue(store.readIdentity());
8906
9903
  const savedSecrets = savedIdentity === null ? null : parseSecrets(savedIdentity);
8907
9904
  if (existing !== null && options.version !== void 0 && options.version !== existing.config.serverVersion) {
@@ -8909,11 +9906,7 @@ var setupServer = async (args, runtime, store) => {
8909
9906
  `Setup retains Mend ${existing.config.serverVersion}. Use mend server upgrade --version ${options.version} to change the server pin.`
8910
9907
  );
8911
9908
  }
8912
- if (existing !== null && existing.config.assetContract !== ASSET_CONTRACT) {
8913
- throw setupError(
8914
- `Mend ${existing.config.serverVersion} was installed under the ${existing.config.assetContract} bundle contract. Use mend server upgrade --version latest to move it to ${ASSET_CONTRACT}; setup cannot repair it in place.`
8915
- );
8916
- }
9909
+ refuseLegacyContract(existing);
8917
9910
  const selectedContext = await selectDockerContext(
8918
9911
  runtime,
8919
9912
  options.context ?? existing?.config.dockerContext
@@ -8956,6 +9949,16 @@ var setupServer = async (args, runtime, store) => {
8956
9949
  ...configWithoutBucket,
8957
9950
  ...bucket === void 0 ? {} : { bucket }
8958
9951
  };
9952
+ if (existing !== null && run2.askedAgainst === void 0) {
9953
+ const changes = changeLines(
9954
+ resolveSetupSettings(existing.config, []),
9955
+ settingsOf({ ...configWithoutBucket, mirrors })
9956
+ );
9957
+ if (changes.length > 0) {
9958
+ runtime.writeLine("This run changes:");
9959
+ for (const line of changes) runtime.writeLine(line);
9960
+ }
9961
+ }
8959
9962
  if (existing === null && (config.edgeHost !== void 0 || config.exposure === "public")) {
8960
9963
  throw setupError(
8961
9964
  `A fresh install cannot start with the edge or as public: until the first account exists, registration is open to whoever reaches the origin first, and the server refuses MEND_EXPOSURE=public without an operator account. Run mend server setup without --edge and --exposure public, create the first account at http://localhost:${config.appPort}, then run mend server setup --edge <host> and declare the posture.`
@@ -8965,6 +9968,11 @@ var setupServer = async (args, runtime, store) => {
8965
9968
  ...savedSecrets ?? createSecrets(runtime),
8966
9969
  ...dockerHubToken === void 0 ? {} : { dockerHubToken }
8967
9970
  };
9971
+ if (config.t3GatewayPort !== void 0 && config.t3GatewayPort !== existing?.config.t3GatewayPort && config.dockerEndpoint.startsWith("unix://") && runtime.portTaken !== void 0 && await runtime.portTaken(config.t3GatewayPort)) {
9972
+ throw setupError(
9973
+ `127.0.0.1:${config.t3GatewayPort} is already in use on this machine, so the t3code gateway cannot be published there and Mend would not start. Free it, or pick another with --t3-gateway-port <n>`
9974
+ );
9975
+ }
8968
9976
  const generation = persistSetup(store, config, secrets, assets);
8969
9977
  const installation = {
8970
9978
  directory: generation.directory,
@@ -8994,6 +10002,14 @@ var setupServer = async (args, runtime, store) => {
8994
10002
  if (edgeStarted !== null) runtime.writeLine(edgeStarted);
8995
10003
  const sshPublication = await sshPublicationLine(runtime, config);
8996
10004
  if (sshPublication !== null) runtime.writeLine(sshPublication);
10005
+ if (config.t3GatewayPort !== void 0) {
10006
+ runtime.writeLine(await observeT3Gateway(runtime, config.t3GatewayPort));
10007
+ }
10008
+ if (existing?.config.t3GatewayPort !== void 0 && config.t3GatewayPort === void 0) {
10009
+ runtime.writeLine(
10010
+ `The t3code gateway is off. Its state (pairings and the device tokens they hold, queued messages) stays in its volume and comes back if you turn it on again; to remove it: docker --context ${config.dockerContext} volume rm mend_${T3_GATEWAY_VOLUME}`
10011
+ );
10012
+ }
8997
10013
  if (existing?.config.edgeHost !== void 0 && config.edgeHost === void 0) {
8998
10014
  runtime.writeLine(
8999
10015
  `The edge for ${existing.config.edgeHost} is gone. Its certificate volumes stay until you remove them: docker --context ${config.dockerContext} volume rm mend_mend-edge-data mend_mend-edge-config`
@@ -9627,6 +10643,15 @@ var serverStatus = async (runtime, installation) => {
9627
10643
  }
9628
10644
  for (const line of await mirrorStatusLines(runtime, installation, runningServices))
9629
10645
  runtime.writeLine(line);
10646
+ if (config.t3GatewayPort !== void 0 && runningServices.includes("mend")) {
10647
+ const gateway = await runtime.fetchText(
10648
+ `http://127.0.0.1:${config.t3GatewayPort}/.well-known/t3/environment`,
10649
+ 2e3
10650
+ );
10651
+ runtime.writeLine(
10652
+ gateway.error === void 0 && gateway.status === 200 ? `t3code gateway \xB7 observed answering at 127.0.0.1:${config.t3GatewayPort} from this machine` : `t3code gateway \xB7 not observed at 127.0.0.1:${config.t3GatewayPort} from this machine \xB7 mend server logs shows what it said`
10653
+ );
10654
+ }
9630
10655
  if (!runningServices.includes("mend")) {
9631
10656
  runtime.writeLine("Mend is stopped. No health claim was made.");
9632
10657
  return;
@@ -9743,6 +10768,17 @@ var nodeServerRuntime = () => {
9743
10768
  dockerDaemonFacts: hostDockerDaemonFacts,
9744
10769
  readLogin: (configDir) => savedLogin(configDir, environment),
9745
10770
  probeSsh: probeSshFromHere,
10771
+ portTaken: portTakenHere,
10772
+ ...process.stdin.isTTY === true && process.stdout.isTTY === true ? { prompter: askOnTerminal } : {},
10773
+ lookupHost: async (host) => {
10774
+ try {
10775
+ return (await dns.lookup(host, { all: true })).map((entry) => entry.address);
10776
+ } catch {
10777
+ return null;
10778
+ }
10779
+ },
10780
+ // Container and VM bridges (Docker's own among them) are never where people reach Mend.
10781
+ localAddresses: () => Object.entries(os10.networkInterfaces()).filter(([name]) => !/^(docker|br-|veth|virbr|cni|flannel|vmnet|vboxnet)/.test(name)).flatMap(([, entries]) => entries ?? []).filter((entry) => !entry.internal).map((entry) => entry.address),
9746
10782
  readStdin: async () => {
9747
10783
  const chunks = [];
9748
10784
  for await (const chunk of process.stdin)
@@ -9751,8 +10787,21 @@ var nodeServerRuntime = () => {
9751
10787
  }
9752
10788
  };
9753
10789
  };
10790
+ var askOnTerminal = async (prompt) => {
10791
+ const readline = await import("node:readline/promises");
10792
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10793
+ try {
10794
+ return await new Promise((resolve8) => {
10795
+ rl.once("close", () => resolve8(null));
10796
+ rl.once("SIGINT", () => rl.close());
10797
+ rl.question(prompt).then(resolve8, () => resolve8(null));
10798
+ });
10799
+ } finally {
10800
+ rl.close();
10801
+ }
10802
+ };
9754
10803
  var sshBannerAt = async (host, port, timeoutMs) => {
9755
- const socket = net3.connect({ host, port });
10804
+ const socket = net4.connect({ host, port });
9756
10805
  let deadline;
9757
10806
  try {
9758
10807
  return await Promise.race([
@@ -9774,6 +10823,11 @@ var sshBannerAt = async (host, port, timeoutMs) => {
9774
10823
  socket.destroy();
9775
10824
  }
9776
10825
  };
10826
+ var portTakenHere = (port) => new Promise((resolve8) => {
10827
+ const server = net4.createServer();
10828
+ server.once("error", (error) => resolve8(error.code === "EADDRINUSE"));
10829
+ server.listen(port, "127.0.0.1", () => server.close(() => resolve8(false)));
10830
+ });
9777
10831
  var probeSshFromHere = async (bind, port) => {
9778
10832
  const unspecified = bind === "0.0.0.0" || bind === "::";
9779
10833
  const addresses = unspecified ? Object.values(os10.networkInterfaces()).flatMap((entries) => entries ?? []).filter(
@@ -9808,15 +10862,14 @@ var serverCommand = async (args, runtime = nodeServerRuntime()) => {
9808
10862
  };
9809
10863
  }
9810
10864
  try {
9811
- const result = await withServerStore(
10865
+ if (command === "setup" && rest.length === 0 && runtime.prompter !== void 0) {
10866
+ return await guidedSetup(runtime, runtime.prompter);
10867
+ }
10868
+ return await underLock(
9812
10869
  runtime.configDir,
9813
- async (store) => {
9814
- if (command === "setup") await setupServer(rest, runtime, store);
9815
- else await manageServer(command, rest, runtime, store);
9816
- },
10870
+ (store) => command === "setup" ? setupServer(rest, runtime, store) : manageServer(command, rest, runtime, store),
9817
10871
  { create: command === "setup" }
9818
10872
  );
9819
- return result._tag === "error" ? { _tag: "error", message: result.error.message } : { _tag: "ok" };
9820
10873
  } catch (cause) {
9821
10874
  if (cause instanceof ServerSetupError) return { _tag: "error", message: cause.message };
9822
10875
  const detail = cause instanceof Error ? cause.message : String(cause);
@@ -10003,7 +11056,7 @@ var renderMendToml = (proposals) => {
10003
11056
  };
10004
11057
 
10005
11058
  // src/service-tunnels.ts
10006
- import * as net4 from "node:net";
11059
+ import * as net5 from "node:net";
10007
11060
  var attachTunnelTargets = (services, sessionId) => services.filter(
10008
11061
  (service) => service.sessionId === sessionId && service.protocol === "tcp" && service.browserScheme !== null
10009
11062
  );
@@ -10115,7 +11168,7 @@ var createServiceTunnels = (options) => {
10115
11168
  };
10116
11169
  const openFor = async (service) => {
10117
11170
  const sockets = /* @__PURE__ */ new Set();
10118
- const server = net4.createServer((socket) => {
11171
+ const server = net5.createServer((socket) => {
10119
11172
  sockets.add(socket);
10120
11173
  socket.on("close", () => sockets.delete(socket));
10121
11174
  pumpConnection(socket, options.tunnelUrl(service.id));
@@ -10748,11 +11801,65 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
10748
11801
  try {
10749
11802
  fs18.mkdirSync(path18.dirname(configFile), { recursive: true, mode: 448 });
10750
11803
  fs18.chmodSync(path18.dirname(configFile), 448);
10751
- fs18.writeFileSync(configFile, reconciled.value, { mode: 384 });
10752
- fs18.chmodSync(configFile, 384);
11804
+ replaceFileAtomically(configFile, reconciled.value, 384);
10753
11805
  return success(void 0);
10754
11806
  } catch (cause) {
10755
- return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
11807
+ return failure(
11808
+ new WorkspaceSshError(
11809
+ "config",
11810
+ `Could not write ${configFile}${cause instanceof Error ? `: ${cause.message}` : "."}`,
11811
+ cause
11812
+ )
11813
+ );
11814
+ }
11815
+ };
11816
+ var linkTarget = (file) => {
11817
+ let current = file;
11818
+ for (let hops = 0; hops < 40; hops += 1) {
11819
+ let stat;
11820
+ try {
11821
+ stat = fs18.lstatSync(current);
11822
+ } catch {
11823
+ if (current !== file && !fs18.existsSync(path18.dirname(current))) {
11824
+ throw new Error(
11825
+ `${file} is a symlink to ${current}, whose directory does not exist. Create it, or replace the link with a file; Mend left the link as it is.`
11826
+ );
11827
+ }
11828
+ return current;
11829
+ }
11830
+ if (!stat.isSymbolicLink()) return current;
11831
+ current = path18.resolve(path18.dirname(current), fs18.readlinkSync(current));
11832
+ }
11833
+ throw new Error(`${file} is a chain of symlinks too long to follow; Mend left it as it is.`);
11834
+ };
11835
+ var replaceFileAtomically = (file, contents, mode) => {
11836
+ const destination = linkTarget(file);
11837
+ const temporary = path18.join(
11838
+ path18.dirname(destination),
11839
+ `.${path18.basename(destination)}.mend-${process.pid}-${createHash3("sha256").update(`${Date.now()}:${Math.random()}`).digest("hex").slice(0, 12)}`
11840
+ );
11841
+ let descriptor = null;
11842
+ try {
11843
+ descriptor = fs18.openSync(temporary, "wx", mode);
11844
+ fs18.writeFileSync(descriptor, contents);
11845
+ fs18.fsyncSync(descriptor);
11846
+ fs18.closeSync(descriptor);
11847
+ descriptor = null;
11848
+ fs18.chmodSync(temporary, mode);
11849
+ fs18.renameSync(temporary, destination);
11850
+ } catch (cause) {
11851
+ if (descriptor !== null) fs18.closeSync(descriptor);
11852
+ fs18.rmSync(temporary, { force: true });
11853
+ throw cause;
11854
+ }
11855
+ try {
11856
+ const directory = fs18.openSync(path18.dirname(destination), "r");
11857
+ try {
11858
+ fs18.fsyncSync(directory);
11859
+ } finally {
11860
+ fs18.closeSync(directory);
11861
+ }
11862
+ } catch {
10756
11863
  }
10757
11864
  };
10758
11865
 
@@ -11112,6 +12219,17 @@ var describeUninstall = async (runtime, scope) => {
11112
12219
  dockerContext: read.value.config.dockerContext,
11113
12220
  edgeHost: read.value.config.edgeHost ?? null,
11114
12221
  mirrors: mirrorServices(read.value.config.mirrors),
12222
+ t3GatewayVolume: read.value.config.t3GatewayPort !== void 0 || (await runtime.server.run(
12223
+ "docker",
12224
+ dockerArgs(
12225
+ read.value.config.dockerContext,
12226
+ "volume",
12227
+ "inspect",
12228
+ `mend_${T3_GATEWAY_VOLUME}`,
12229
+ "--format",
12230
+ "{{json .}}"
12231
+ )
12232
+ )).status === 0,
11115
12233
  generations: countEntries(path20.join(configDir, "generations")),
11116
12234
  backups: countEntries(path20.join(configDir, "backups"))
11117
12235
  };
@@ -11145,7 +12263,9 @@ var planLines = (plan, configDir) => {
11145
12263
  const edge = [...edgeHost === null ? [] : ["edge"], ...mirrors];
11146
12264
  const edgeVolumes = [
11147
12265
  ...edgeHost === null ? [] : ["mend-edge-data", "mend-edge-config"],
11148
- ...mirrors.map((service) => `mend-${service}`)
12266
+ ...mirrors.map((service) => `mend-${service}`),
12267
+ // The t3code gateway's state, with paired people's device tokens, when it is there.
12268
+ ...plan.server.t3GatewayVolume === true ? [T3_GATEWAY_VOLUME] : []
11149
12269
  ];
11150
12270
  lines.push(
11151
12271
  ` containers ${["mend", "postgres", "garage", ...edge].join(", ")} \xB7 volumes ${[MEND_DOCKER_NAMESPACE_WITH_GARAGE.store, ...secondaryVolumesOf(MEND_DOCKER_NAMESPACE_WITH_GARAGE), "mend-config", "mend-ssh", "mend-postgres", ...edgeVolumes].join(", ")} \xB7 image ghcr.io/sealant-sh/mend:${version}${edgeHost === null ? "" : ` \xB7 the edge for ${edgeHost}`}`
@@ -11211,6 +12331,24 @@ var removeServer = async (runtime, failures, leftovers) => {
11211
12331
  server.writeLine(
11212
12332
  `removed containers ${["mend", "postgres", "garage", ...mirrorServices(installation.config.mirrors)].join(", ")} and the Compose-owned volumes`
11213
12333
  );
12334
+ const gatewayVolume = `mend_${T3_GATEWAY_VOLUME}`;
12335
+ const gatewayLeft = await server.run(
12336
+ "docker",
12337
+ dockerArgs(context, "volume", "inspect", gatewayVolume, "--format", "{{json .}}")
12338
+ );
12339
+ if (gatewayLeft.status === 0) {
12340
+ const gone = await server.run(
12341
+ "docker",
12342
+ dockerArgs(context, "volume", "rm", gatewayVolume)
12343
+ );
12344
+ if (gone.status === 0) {
12345
+ server.writeLine(`removed volume ${gatewayVolume}, the t3code gateway's state`);
12346
+ } else {
12347
+ failures.push(
12348
+ `could not remove volume ${gatewayVolume} (the t3code gateway's state, with paired device tokens): ${(gone.error ?? gone.stderr.trim()) || "no output"}`
12349
+ );
12350
+ }
12351
+ }
11214
12352
  const namespace = installation.config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
11215
12353
  const dataVolumes = [namespace.store, ...secondaryVolumesOf(namespace)];
11216
12354
  const identity = store.readIdentity();
@@ -11881,8 +13019,8 @@ var askWorktreeName = async () => {
11881
13019
  const readline = await import("node:readline/promises");
11882
13020
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
11883
13021
  try {
11884
- const answer = (await rl.question(` worktree name ${dim7("(enter for auto)")}: `)).trim();
11885
- return answer === "" ? null : normalizeProjectName(answer);
13022
+ const answer2 = (await rl.question(` worktree name ${dim7("(enter for auto)")}: `)).trim();
13023
+ return answer2 === "" ? null : normalizeProjectName(answer2);
11886
13024
  } finally {
11887
13025
  rl.close();
11888
13026
  }
@@ -12534,10 +13672,10 @@ var pickSessionInteractively = async (rows) => {
12534
13672
  });
12535
13673
  const readline = await import("node:readline/promises");
12536
13674
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12537
- const answer = await rl.question(" session #: ");
13675
+ const answer2 = await rl.question(" session #: ");
12538
13676
  rl.close();
12539
- const chosen = rows[Number(answer.trim()) - 1];
12540
- if (chosen === void 0) return fail7(`"${answer.trim()}" is not one of the choices`);
13677
+ const chosen = rows[Number(answer2.trim()) - 1];
13678
+ if (chosen === void 0) return fail7(`"${answer2.trim()}" is not one of the choices`);
12541
13679
  return chosen.session;
12542
13680
  };
12543
13681
  var resolveLiveSession = async (config, prefix, command) => {
@@ -12600,8 +13738,7 @@ var shellCommand = async (config, args) => {
12600
13738
  process.exit(0);
12601
13739
  };
12602
13740
  var flattenService = (view) => {
12603
- const attempt2 = view.service.currentAttemptId === null ? null : view.attempts.find((candidate) => candidate.id === view.service.currentAttemptId) ?? null;
12604
- const observation = view.currentForward !== null && view.latestObservation?.forwardId === view.currentForward.id ? view.latestObservation : null;
13741
+ const attempt2 = serviceViewAttempt(view);
12605
13742
  const endpoint = view.endpoints.find((candidate) => candidate.scope === "private") ?? view.endpoints[0] ?? null;
12606
13743
  const browserUrl = view.endpoints.find((candidate) => candidate.browserUrl !== null)?.browserUrl ?? null;
12607
13744
  return {
@@ -12609,7 +13746,7 @@ var flattenService = (view) => {
12609
13746
  processId: attempt2?.id ?? null,
12610
13747
  sessionId: view.service.sessionId,
12611
13748
  label: view.service.name,
12612
- status: observation?.state ?? view.currentForward?.state ?? attempt2?.status ?? "stopped",
13749
+ status: serviceViewStatus(view),
12613
13750
  workspacePort: view.service.workspacePort,
12614
13751
  hostPort: endpoint?.hostPort ?? null,
12615
13752
  authority: endpoint?.authority ?? null,
@@ -13171,9 +14308,9 @@ var serviceInit = async (args) => {
13171
14308
  }
13172
14309
  const readline = await import("node:readline/promises");
13173
14310
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13174
- const answer = await rl.question("write it? [y/N] ");
14311
+ const answer2 = await rl.question("write it? [y/N] ");
13175
14312
  rl.close();
13176
- if (answer.trim().toLowerCase() !== "y") {
14313
+ if (answer2.trim().toLowerCase() !== "y") {
13177
14314
  say7(dim7("nothing written"));
13178
14315
  return;
13179
14316
  }
@@ -13184,7 +14321,7 @@ var serviceInit = async (args) => {
13184
14321
  var tunnelServices = async (config, services, portOverride) => {
13185
14322
  for (const service of services) {
13186
14323
  const port = portOverride ?? service.hostPort ?? service.workspacePort;
13187
- const server = net5.createServer(
14324
+ const server = net6.createServer(
13188
14325
  (socket) => pumpConnection(socket, tunnelUrlFor(config, service.id))
13189
14326
  );
13190
14327
  await listenLocal(server, port, false).catch(
@@ -13756,19 +14893,19 @@ var askUninstallScope = async () => {
13756
14893
  UNINSTALL_CHOICES.forEach((choice, index) => say7(` ${index + 1}. ${choice.text}`));
13757
14894
  const readline = await import("node:readline/promises");
13758
14895
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13759
- const answer = (await rl.question(" 1, 2 or 3: ")).trim();
14896
+ const answer2 = (await rl.question(" 1, 2 or 3: ")).trim();
13760
14897
  rl.close();
13761
- const chosen = UNINSTALL_CHOICES[Number(answer) - 1];
13762
- if (chosen === void 0) return fail7(`"${answer}" is not one of the choices`);
14898
+ const chosen = UNINSTALL_CHOICES[Number(answer2) - 1];
14899
+ if (chosen === void 0) return fail7(`"${answer2}" is not one of the choices`);
13763
14900
  return chosen.scope;
13764
14901
  };
13765
14902
  var confirmUninstall = async (word) => {
13766
14903
  if (process.stdin.isTTY !== true) return fail7("non-interactive \u2014 pass --yes to remove");
13767
14904
  const readline = await import("node:readline/promises");
13768
14905
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13769
- const answer = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
14906
+ const answer2 = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
13770
14907
  rl.close();
13771
- return answer === word;
14908
+ return answer2 === word;
13772
14909
  };
13773
14910
  var uninstallCommand = async (config, args) => {
13774
14911
  const parsed = parseUninstallArgs(args);
@@ -14334,7 +15471,7 @@ _mend() {
14334
15471
  compadd setup status start stop restart logs upgrade
14335
15472
  else
14336
15473
  case $words[3] in
14337
- setup) compadd -- --version --assets-dir --offline --context --port --ssh-port --bind --url --origin --docker-socket ;;
15474
+ setup) compadd -- --yes --version --assets-dir --offline --context --port --ssh-port --bind --url --origin --docker-socket ;;
14338
15475
  upgrade) compadd -- --version --assets-dir --offline --from-preview ;;
14339
15476
  start|restart) compadd -- --offline ;;
14340
15477
  logs) compadd -- --tail ;;
@@ -14363,7 +15500,7 @@ var BASH_COMPLETIONS = `_mend() {
14363
15500
  options="setup status start stop restart logs upgrade"
14364
15501
  else
14365
15502
  case \${COMP_WORDS[2]} in
14366
- setup) options="--version --assets-dir --offline --context --port --ssh-port --bind --url --origin --docker-socket" ;;
15503
+ setup) options="--yes --version --assets-dir --offline --context --port --ssh-port --bind --url --origin --docker-socket" ;;
14367
15504
  upgrade) options="--version --assets-dir --offline --from-preview" ;;
14368
15505
  start|restart) options="--offline" ;;
14369
15506
  logs) options="--tail" ;;
@@ -15236,7 +16373,7 @@ var dashboard = async (config, options = {}) => {
15236
16373
  );
15237
16374
  process.exit(rerun.status ?? 0);
15238
16375
  }
15239
- const { runDashboard } = await import("./dashboard-OBFWHSPD.js");
16376
+ const { runDashboard } = await import("./dashboard-MPO6H5YF.js");
15240
16377
  const agentShare = await startShareIfBridge(config);
15241
16378
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
15242
16379
  listServices: () => fetchServices(config),