@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.
- package/dist/{chunk-DHM7VXTR.js → chunk-GF5A6W2Y.js} +12 -1
- package/dist/{dashboard-OBFWHSPD.js → dashboard-MPO6H5YF.js} +823 -176
- package/dist/main.js +1221 -84
- package/man/mend-accounts.1 +1 -1
- package/man/mend-adopt.1 +1 -1
- package/man/mend-attach.1 +1 -1
- package/man/mend-codex.1 +1 -1
- package/man/mend-completions.1 +1 -1
- package/man/mend-connect-pi.1 +1 -1
- package/man/mend-connect.1 +1 -1
- package/man/mend-continue.1 +1 -1
- package/man/mend-doctor.1 +1 -1
- package/man/mend-dotfiles-repo.1 +1 -1
- package/man/mend-dotfiles-sync.1 +1 -1
- package/man/mend-dotfiles.1 +1 -1
- package/man/mend-env-cluster.1 +1 -1
- package/man/mend-env-load.1 +1 -1
- package/man/mend-env-show.1 +1 -1
- package/man/mend-folder-create.1 +1 -1
- package/man/mend-folder-list.1 +1 -1
- package/man/mend-folder-push.1 +1 -1
- package/man/mend-folder-rm.1 +1 -1
- package/man/mend-git-author.1 +1 -1
- package/man/mend-help.1 +1 -1
- package/man/mend-invite.1 +1 -1
- package/man/mend-keys-init.1 +1 -1
- package/man/mend-keys-mode.1 +1 -1
- package/man/mend-keys-share.1 +1 -1
- package/man/mend-keys-show.1 +1 -1
- package/man/mend-land.1 +1 -1
- package/man/mend-login.1 +1 -1
- package/man/mend-logout.1 +1 -1
- package/man/mend-logs.1 +1 -1
- package/man/mend-man.1 +1 -1
- package/man/mend-members.1 +1 -1
- package/man/mend-memory-import.1 +1 -1
- package/man/mend-memory-rm.1 +1 -1
- package/man/mend-memory-show.1 +1 -1
- package/man/mend-memory.1 +1 -1
- package/man/mend-models.1 +1 -1
- package/man/mend-operator-exposure.1 +2 -2
- package/man/mend-operator-gate.1 +1 -1
- package/man/mend-operator-grant-owner.1 +1 -1
- package/man/mend-operator-org-create.1 +1 -1
- package/man/mend-operator-org-invite-owner.1 +1 -1
- package/man/mend-operator-org-list.1 +1 -1
- package/man/mend-operator-org-rename.1 +1 -1
- package/man/mend-operator-reset-link.1 +1 -1
- package/man/mend-pair.1 +1 -1
- package/man/mend-projects.1 +1 -1
- package/man/mend-pull.1 +1 -1
- package/man/mend-refresh.1 +1 -1
- package/man/mend-rejoin.1 +1 -1
- package/man/mend-resume.1 +1 -1
- package/man/mend-run.1 +1 -1
- package/man/mend-secrets-add.1 +1 -1
- package/man/mend-secrets-rm.1 +1 -1
- package/man/mend-secrets.1 +1 -1
- package/man/mend-server-logs.1 +1 -1
- package/man/mend-server-restart.1 +1 -1
- package/man/mend-server-setup.1 +37 -6
- package/man/mend-server-start.1 +1 -1
- package/man/mend-server-status.1 +1 -1
- package/man/mend-server-stop.1 +1 -1
- package/man/mend-server-upgrade.1 +1 -1
- package/man/mend-server.1 +1 -1
- package/man/mend-service-add.1 +1 -1
- package/man/mend-service-connect.1 +1 -1
- package/man/mend-service-init.1 +1 -1
- package/man/mend-service-list.1 +1 -1
- package/man/mend-service-logs.1 +1 -1
- package/man/mend-service-restart.1 +1 -1
- package/man/mend-service-run.1 +1 -1
- package/man/mend-service-stop.1 +1 -1
- package/man/mend-session-share.1 +1 -1
- package/man/mend-sessions.1 +1 -1
- package/man/mend-shell.1 +1 -1
- package/man/mend-skills-push.1 +1 -1
- package/man/mend-skills.1 +1 -1
- package/man/mend-snake.1 +2 -2
- package/man/mend-ssh-keys-remove.1 +1 -1
- package/man/mend-ssh-keys.1 +1 -1
- package/man/mend-ssh-setup.1 +1 -1
- package/man/mend-ssh.1 +1 -1
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +3 -1
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-wait.1 +1 -1
- package/man/mend-workspace-replace.1 +1 -1
- package/man/mend-worktrees-rm.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +2 -2
- 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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3286
|
+
summary: "install or change the local Mend server, guided",
|
|
3249
3287
|
synopsis: [
|
|
3250
|
-
"
|
|
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
|
|
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
|
-
{
|
|
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
|
|
5518
|
+
const typed2 = (await rl.question(`mend server url ${dim3(`[${deps.defaultUrl}]`)}: `)).trim();
|
|
5449
5519
|
rl.close();
|
|
5450
|
-
const chosen =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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 &&
|
|
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
|
|
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) =>
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
10752
|
-
fs18.chmodSync(configFile, 384);
|
|
11804
|
+
replaceFileAtomically(configFile, reconciled.value, 384);
|
|
10753
11805
|
return success(void 0);
|
|
10754
11806
|
} catch (cause) {
|
|
10755
|
-
return failure(
|
|
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
|
|
11885
|
-
return
|
|
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
|
|
13675
|
+
const answer2 = await rl.question(" session #: ");
|
|
12538
13676
|
rl.close();
|
|
12539
|
-
const chosen = rows[Number(
|
|
12540
|
-
if (chosen === void 0) return fail7(`"${
|
|
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 =
|
|
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:
|
|
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
|
|
14311
|
+
const answer2 = await rl.question("write it? [y/N] ");
|
|
13175
14312
|
rl.close();
|
|
13176
|
-
if (
|
|
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 =
|
|
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
|
|
14896
|
+
const answer2 = (await rl.question(" 1, 2 or 3: ")).trim();
|
|
13760
14897
|
rl.close();
|
|
13761
|
-
const chosen = UNINSTALL_CHOICES[Number(
|
|
13762
|
-
if (chosen === void 0) return fail7(`"${
|
|
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
|
|
14906
|
+
const answer2 = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
|
|
13770
14907
|
rl.close();
|
|
13771
|
-
return
|
|
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-
|
|
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),
|