@sealant/mend 0.24.2 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +5 -6
  2. package/dist/main.js +601 -419
  3. package/man/mend-accounts.1 +1 -1
  4. package/man/mend-adopt.1 +1 -1
  5. package/man/mend-attach.1 +1 -1
  6. package/man/mend-codex.1 +1 -1
  7. package/man/mend-completions.1 +1 -1
  8. package/man/mend-connect.1 +1 -1
  9. package/man/mend-continue.1 +1 -1
  10. package/man/mend-doctor.1 +1 -1
  11. package/man/mend-dotfiles-sync.1 +1 -1
  12. package/man/mend-dotfiles.1 +1 -1
  13. package/man/mend-env-cluster.1 +1 -1
  14. package/man/mend-env-load.1 +1 -1
  15. package/man/mend-env-show.1 +1 -1
  16. package/man/mend-help.1 +1 -1
  17. package/man/mend-keys-init.1 +1 -1
  18. package/man/mend-keys-mode.1 +1 -1
  19. package/man/mend-keys-share.1 +1 -1
  20. package/man/mend-keys-show.1 +1 -1
  21. package/man/mend-login.1 +1 -1
  22. package/man/mend-logout.1 +1 -1
  23. package/man/mend-man.1 +1 -1
  24. package/man/mend-pair.1 +1 -1
  25. package/man/mend-projects.1 +1 -1
  26. package/man/mend-refresh.1 +1 -1
  27. package/man/mend-rejoin.1 +1 -1
  28. package/man/mend-resume.1 +1 -1
  29. package/man/mend-run.1 +1 -1
  30. package/man/mend-server-logs.1 +1 -1
  31. package/man/mend-server-restart.1 +3 -3
  32. package/man/mend-server-setup.1 +6 -9
  33. package/man/mend-server-start.1 +3 -3
  34. package/man/mend-server-status.1 +1 -1
  35. package/man/mend-server-stop.1 +1 -1
  36. package/man/mend-server-upgrade.1 +3 -3
  37. package/man/mend-server.1 +1 -1
  38. package/man/mend-service-add.1 +1 -1
  39. package/man/mend-service-connect.1 +1 -1
  40. package/man/mend-service-init.1 +1 -1
  41. package/man/mend-service-list.1 +1 -1
  42. package/man/mend-service-logs.1 +1 -1
  43. package/man/mend-service-restart.1 +1 -1
  44. package/man/mend-service-run.1 +1 -1
  45. package/man/mend-service-stop.1 +1 -1
  46. package/man/mend-sessions.1 +1 -1
  47. package/man/mend-shell.1 +1 -1
  48. package/man/mend-skills-push.1 +1 -1
  49. package/man/mend-skills.1 +1 -1
  50. package/man/mend-ssh-setup.1 +1 -1
  51. package/man/mend-ssh.1 +1 -1
  52. package/man/mend-stop.1 +1 -1
  53. package/man/mend-ui.1 +1 -1
  54. package/man/mend-uninstall.1 +35 -0
  55. package/man/mend-version.1 +1 -1
  56. package/man/mend-worktrees.1 +1 -1
  57. package/man/mend.1 +5 -2
  58. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -40,7 +40,7 @@ var bridgeUrlOf = (serverUrl, token, host) => {
40
40
  var retryDelayMs = (attempt2) => Math.min(3e4, 1e3 * 2 ** Math.min(Math.max(attempt2 - 1, 0), 5));
41
41
  var SSH_AGENTC_REQUEST_IDENTITIES = 11;
42
42
  var SSH_AGENTC_SIGN_REQUEST = 13;
43
- var askLocalAgent = (agentSock, payload) => new Promise((resolve5, reject) => {
43
+ var askLocalAgent = (agentSock, payload) => new Promise((resolve4, reject) => {
44
44
  const connection = net.connect(agentSock, () => {
45
45
  connection.write(payload);
46
46
  });
@@ -54,12 +54,12 @@ var askLocalAgent = (agentSock, payload) => new Promise((resolve5, reject) => {
54
54
  if (pending.length >= 4 && pending.length >= 4 + pending.readUInt32BE(0)) {
55
55
  clearTimeout(timer);
56
56
  connection.end();
57
- resolve5(pending.subarray(0, 4 + pending.readUInt32BE(0)));
57
+ resolve4(pending.subarray(0, 4 + pending.readUInt32BE(0)));
58
58
  }
59
59
  });
60
- connection.on("error", (error2) => {
60
+ connection.on("error", (error) => {
61
61
  clearTimeout(timer);
62
- reject(error2);
62
+ reject(error);
63
63
  });
64
64
  });
65
65
  var sleep = (ms, signal) => delay(ms, void 0, { signal }).then(
@@ -74,12 +74,12 @@ var shareAgent = async (options) => {
74
74
  const ws = new WebSocket(url);
75
75
  const onAbort = () => ws.close();
76
76
  signal.addEventListener("abort", onAbort, { once: true });
77
- const closed = new Promise((resolve5) => {
78
- ws.addEventListener("close", () => resolve5(), { once: true });
77
+ const closed = new Promise((resolve4) => {
78
+ ws.addEventListener("close", () => resolve4(), { once: true });
79
79
  });
80
- const opened = await new Promise((resolve5) => {
81
- ws.addEventListener("open", () => resolve5(true), { once: true });
82
- ws.addEventListener("error", () => resolve5(false), { once: true });
80
+ const opened = await new Promise((resolve4) => {
81
+ ws.addEventListener("open", () => resolve4(true), { once: true });
82
+ ws.addEventListener("error", () => resolve4(false), { once: true });
83
83
  });
84
84
  if (opened) {
85
85
  attempt2 = 0;
@@ -112,8 +112,8 @@ var shareAgent = async (options) => {
112
112
  if (type === SSH_AGENTC_SIGN_REQUEST) {
113
113
  onEvent({ kind: "signed", context, seconds: (Date.now() - started) / 1e3 });
114
114
  }
115
- } catch (error2) {
116
- const message = error2 instanceof Error ? error2.message : String(error2);
115
+ } catch (error) {
116
+ const message = error instanceof Error ? error.message : String(error);
117
117
  if (ws.readyState === WebSocket.OPEN) {
118
118
  ws.send(JSON.stringify({ t: "err", id, message }));
119
119
  }
@@ -171,13 +171,13 @@ var parsePngfHex = (output) => {
171
171
  return Buffer.from(match[1], "hex");
172
172
  };
173
173
  var MAX_BYTES = 32 * 1024 * 1024;
174
- var run = (command, args) => new Promise((resolve5) => {
174
+ var run = (command, args) => new Promise((resolve4) => {
175
175
  execFile(
176
176
  command,
177
177
  [...args],
178
178
  { encoding: "buffer", maxBuffer: MAX_BYTES, timeout: 3e3 },
179
- (error2, stdout) => {
180
- resolve5(error2 === null ? stdout : null);
179
+ (error, stdout) => {
180
+ resolve4(error === null ? stdout : null);
181
181
  }
182
182
  );
183
183
  });
@@ -1037,7 +1037,7 @@ var COMMANDS = [
1037
1037
  section: "this machine",
1038
1038
  summary: "install or repair the local Mend server",
1039
1039
  synopsis: [
1040
- "[--context <name>] [--version <version|latest>] [--bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--registry-port <n>] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
1040
+ "[--context <name>] [--version <version|latest>] [--bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
1041
1041
  ],
1042
1042
  description: [
1043
1043
  "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.",
@@ -1061,18 +1061,17 @@ var COMMANDS = [
1061
1061
  text: "additional exact browser origin; repeat for more than one"
1062
1062
  },
1063
1063
  { flag: "--port <n>", text: "external web port. Default: 3105" },
1064
- { flag: "--ssh-port <n>", text: "external workspace SSH port. Default: 2222" },
1065
1064
  {
1066
- flag: "--registry-port <n>",
1067
- text: "loopback registry port. Default: 5000; all three ports must differ"
1065
+ flag: "--ssh-port <n>",
1066
+ text: "external workspace SSH port. Default: 2222; must differ from --port"
1068
1067
  },
1069
1068
  {
1070
1069
  flag: "--assets-dir <dir>",
1071
- text: "copy compose.v1.yaml and postgres-init.sh from a release directory; fresh setup requires --version"
1070
+ text: "copy compose.v2.yaml and postgres-init.sh from a release directory; fresh setup requires --version"
1072
1071
  },
1073
1072
  {
1074
1073
  flag: "--offline",
1075
- text: "use retained or supplied assets and preloaded images only; no GitHub requests or release-image pulls; exact image label, health version and a loopback registry roundtrip required"
1074
+ text: "use retained or supplied assets and preloaded images only; no GitHub requests or release-image pulls; exact image label and health version required"
1076
1075
  },
1077
1076
  { flag: "--docker-socket <path>", text: "daemon-side socket mount override for diagnostics" }
1078
1077
  ],
@@ -1083,7 +1082,7 @@ var COMMANDS = [
1083
1082
  text: "explicit tailnet exposure"
1084
1083
  },
1085
1084
  {
1086
- command: "mend server setup --version 0.23.0 --assets-dir ./release-assets --offline --registry-port 5501",
1085
+ command: "mend server setup --version 0.25.0 --assets-dir ./release-assets --offline",
1087
1086
  text: "install from local release assets and preloaded images"
1088
1087
  }
1089
1088
  ],
@@ -1105,14 +1104,9 @@ var COMMANDS = [
1105
1104
  summary: "start the selected server generation",
1106
1105
  synopsis: ["[--offline]"],
1107
1106
  description: [
1108
- "Reuses the saved context, generation, credentials and exact pin. Uses preloaded release images, with no release-image pulls or asset downloads, even without --offline. Exact image label, health version and a loopback registry roundtrip are required."
1109
- ],
1110
- options: [
1111
- {
1112
- flag: "--offline",
1113
- text: "use local release images and assets; still probe the loopback registry"
1114
- }
1107
+ "Reuses the saved context, generation, credentials and exact pin. Uses preloaded release images, with no release-image pulls or asset downloads, even without --offline. Exact image label and health version are required."
1115
1108
  ],
1109
+ options: [{ flag: "--offline", text: "use local release images and assets" }],
1116
1110
  see: ["server status", "server upgrade"]
1117
1111
  },
1118
1112
  {
@@ -1131,14 +1125,9 @@ var COMMANDS = [
1131
1125
  summary: "restart Mend using the same generation and pin",
1132
1126
  synopsis: ["[--offline]"],
1133
1127
  description: [
1134
- "Checks preloaded images before stopping Mend, then starts the saved generation and verifies exact-version health and a loopback registry roundtrip. Postgres stays running. Connections are interrupted; workspace containers and data remain, but active work may need reconnection."
1135
- ],
1136
- options: [
1137
- {
1138
- flag: "--offline",
1139
- text: "use local release images and assets; still probe the loopback registry"
1140
- }
1128
+ "Checks preloaded images before stopping Mend, then starts the saved generation and verifies exact-version health. Postgres stays running. Connections are interrupted; workspace containers and data remain, but active work may need reconnection."
1141
1129
  ],
1130
+ options: [{ flag: "--offline", text: "use local release images and assets" }],
1142
1131
  see: ["server status", "server logs"]
1143
1132
  },
1144
1133
  {
@@ -1169,11 +1158,11 @@ var COMMANDS = [
1169
1158
  },
1170
1159
  {
1171
1160
  flag: "--assets-dir <dir>",
1172
- text: "copy target compose.v1.yaml and postgres-init.sh from local release assets"
1161
+ text: "copy target compose.v2.yaml and postgres-init.sh from local release assets"
1173
1162
  },
1174
1163
  {
1175
1164
  flag: "--offline",
1176
- text: "no GitHub requests or release-image pulls; target assets and exact-version images must be local; still probe the loopback registry"
1165
+ text: "no GitHub requests or release-image pulls; target assets and exact-version images must be local"
1177
1166
  }
1178
1167
  ],
1179
1168
  examples: [
@@ -1224,6 +1213,30 @@ var COMMANDS = [
1224
1213
  description: ["Other terminals and devices keep their own tokens."],
1225
1214
  see: ["login"]
1226
1215
  },
1216
+ {
1217
+ name: "uninstall",
1218
+ section: "this machine",
1219
+ summary: "remove the server, this machine's Mend files, or both",
1220
+ synopsis: ["[--all | --server | --home] [--yes]"],
1221
+ description: [
1222
+ "Asks which scope to remove when none is given, prints exactly what will go, and requires the word delete before the server is touched. The server scope removes the local Compose installation: its containers, every volume it owns (repositories, worktrees, the database), its release image, and the private configuration with its generations and backups. The home scope revokes this terminal's device token, then removes cli.json, the workspace SSH key, and the managed block in ~/.ssh/config.",
1223
+ "Nothing else under the configuration directory is touched; a host-run store or keys root is listed and left in place. Workspace containers carry no label Mend can filter on, so they are named with the command that removes them, never removed."
1224
+ ],
1225
+ options: [
1226
+ { flag: "--all", text: "the server and this machine's Mend files" },
1227
+ { flag: "--server", text: "only the local server installation" },
1228
+ { flag: "--home", text: "only this machine's sign-in, ssh key and config block" },
1229
+ { flag: "--yes", text: "skip the confirmation; required without a terminal" }
1230
+ ],
1231
+ examples: [
1232
+ { command: "mend uninstall", text: "choose a scope, then confirm" },
1233
+ {
1234
+ command: "mend uninstall --home --yes",
1235
+ text: "forget this machine's sign-in without asking"
1236
+ }
1237
+ ],
1238
+ see: ["server stop", "logout"]
1239
+ },
1227
1240
  {
1228
1241
  name: "completions",
1229
1242
  section: "this machine",
@@ -1677,7 +1690,7 @@ var retryAfterSecondsOf = (json) => {
1677
1690
  }
1678
1691
  return 5;
1679
1692
  };
1680
- var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
1693
+ var sleep2 = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
1681
1694
  var resolveServerUrl = async (args, deps) => {
1682
1695
  const given = takeFlagValue2(args, "--url") ?? deps.configuredUrl;
1683
1696
  if (given !== null) {
@@ -1750,7 +1763,7 @@ import { randomBytes } from "node:crypto";
1750
1763
  import { readFile } from "node:fs/promises";
1751
1764
  import * as net2 from "node:net";
1752
1765
  import * as os3 from "node:os";
1753
- import * as path6 from "node:path";
1766
+ import * as path5 from "node:path";
1754
1767
 
1755
1768
  // src/server-docker-volumes.ts
1756
1769
  import { createHash } from "node:crypto";
@@ -1955,223 +1968,10 @@ var ownership = async (runtime, input, mode) => {
1955
1968
  var claimServerDockerVolumes = (runtime, input) => ownership(runtime, input, "claim");
1956
1969
  var verifyServerDockerVolumes = (runtime, input) => ownership(runtime, input, "verify");
1957
1970
 
1958
- // src/server-registry-probe.ts
1959
- import { Buffer as Buffer2 } from "node:buffer";
1960
- import * as fs3 from "node:fs/promises";
1961
- import * as path3 from "node:path";
1962
- var ServerRegistryProbeError = class extends Error {
1963
- /** Stable error discriminator. */
1964
- _tag = "ServerRegistryProbeError";
1965
- /** Failed roundtrip operation, independent of the host operating system. */
1966
- stage;
1967
- /** Bounded, credential-scrubbed failure detail. */
1968
- diagnostic;
1969
- /** Explain the failed Engine operation without recommending a wider registry binding. */
1970
- constructor(stage, diagnostic) {
1971
- super(
1972
- `Docker registry probe failed (${stage}): ${diagnostic}. The selected Docker Engine must import, push and pull through 127.0.0.1 at the configured registry port. Check bundle health, Docker context and Engine loopback registry support, then retry. Do not widen the registry binding.`
1973
- );
1974
- this.stage = stage;
1975
- this.diagnostic = diagnostic;
1976
- }
1977
- };
1978
- var PROBE_LABEL = "dev.sealant.mend.registry-probe";
1979
- var COMMAND_TIMEOUT_MS = 6e4;
1980
- var CLEANUP_TIMEOUT_MS = 15e3;
1981
- var error = (stage, diagnostic) => ({ _tag: "error", error: new ServerRegistryProbeError(stage, diagnostic) });
1982
- var safeDiagnostic = (text) => text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1[redacted]@").replace(
1983
- /((?:password|passwd|token|secret|authorization|credential)[\w-]*\s*[=:]\s*)[^\s,;]+/gi,
1984
- "$1[redacted]"
1985
- ).replace(/Bearer\s+\S+/gi, "Bearer [redacted]").split("").map(
1986
- (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127 ? " " : character
1987
- ).join("").slice(0, 2048);
1988
- var docker2 = async (runtime, context, stage, args) => {
1989
- try {
1990
- const output = await runtime.run("docker", ["--context", context, ...args], {
1991
- timeoutMs: stage === "cleanup" ? CLEANUP_TIMEOUT_MS : COMMAND_TIMEOUT_MS
1992
- });
1993
- if (output.status !== 0 || output.error !== void 0)
1994
- return error(
1995
- stage,
1996
- safeDiagnostic(
1997
- output.error ?? (output.stderr.trim() || `exit ${output.status ?? "unknown"}`)
1998
- )
1999
- );
2000
- return { _tag: "ok", value: output.stdout };
2001
- } catch {
2002
- return error(
2003
- stage,
2004
- "Process runner rejected; check Docker access and command timeout handling"
2005
- );
2006
- }
2007
- };
2008
- var tinyRootfs = (nonce) => {
2009
- const body = Buffer2.from(`Mend registry connectivity probe ${nonce}
2010
- `);
2011
- const header = Buffer2.alloc(512);
2012
- const field = (value, offset, length) => header.write(value, offset, length, "ascii");
2013
- field("mend-registry-probe", 0, 100);
2014
- field("0000600\0", 100, 8);
2015
- field("0000000\0", 108, 8);
2016
- field("0000000\0", 116, 8);
2017
- field(`${body.length.toString(8).padStart(11, "0")}\0`, 124, 12);
2018
- field("00000000000\0", 136, 12);
2019
- field(" ", 148, 8);
2020
- field("0", 156, 1);
2021
- field("ustar\0", 257, 6);
2022
- field("00", 263, 2);
2023
- const checksum = header.reduce((sum, byte) => sum + byte, 0);
2024
- field(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, 8);
2025
- return Buffer2.concat([header, body, Buffer2.alloc(512 - body.length), Buffer2.alloc(1024)]);
2026
- };
2027
- var imageExists = async (runtime, input, reference, stage) => {
2028
- const listed = await docker2(runtime, input.dockerContext, stage, [
2029
- "image",
2030
- "ls",
2031
- "--all",
2032
- "--filter",
2033
- `reference=${reference}`,
2034
- "--format",
2035
- "{{json .ID}}"
2036
- ]);
2037
- if (listed._tag === "error") return listed;
2038
- try {
2039
- const lines = listed.value.trim().split("\n").filter(Boolean);
2040
- for (const line of lines) {
2041
- const id = JSON.parse(line);
2042
- if (typeof id !== "string" || !/^(?:sha256:)?[a-f0-9]{12,64}$/.test(id))
2043
- return error(stage, "Docker returned an inconclusive image listing");
2044
- }
2045
- return { _tag: "ok", value: lines.length > 0 };
2046
- } catch {
2047
- return error(stage, "Docker returned an invalid image listing");
2048
- }
2049
- };
2050
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2051
- var ownedImage = async (runtime, input, reference, stage) => {
2052
- const inspected = await docker2(runtime, input.dockerContext, stage, [
2053
- "image",
2054
- "inspect",
2055
- reference,
2056
- "--format",
2057
- "{{json .}}"
2058
- ]);
2059
- if (inspected._tag === "error") return inspected;
2060
- try {
2061
- const image = JSON.parse(inspected.value);
2062
- if (!isRecord(image) || typeof image.Id !== "string" || !/^sha256:[a-f0-9]{64}$/.test(image.Id) || !isRecord(image.Config) || !isRecord(image.Config.Labels) || image.Config.Labels[PROBE_LABEL] !== input.nonce)
2063
- return error(stage, "Image ownership or ID could not be verified; the image was retained");
2064
- return { _tag: "ok", value: image.Id };
2065
- } catch {
2066
- return error(stage, "Docker returned an invalid image inspection; the image was retained");
2067
- }
2068
- };
2069
- var removeOwnedTag = async (runtime, input, reference, stage) => {
2070
- const exists = await imageExists(runtime, input, reference, stage);
2071
- if (exists._tag === "error") return exists;
2072
- if (!exists.value) return { _tag: "ok", value: void 0 };
2073
- const owned = await ownedImage(runtime, input, reference, stage);
2074
- if (owned._tag === "error") return owned;
2075
- const removed = await docker2(runtime, input.dockerContext, stage, ["image", "rm", reference]);
2076
- return removed._tag === "error" ? removed : { _tag: "ok", value: void 0 };
2077
- };
2078
- var roundtrip = async (runtime, input, reference, tarPath) => {
2079
- const imported = await docker2(runtime, input.dockerContext, "import", [
2080
- "image",
2081
- "import",
2082
- "--change",
2083
- `LABEL ${PROBE_LABEL}=${input.nonce}`,
2084
- tarPath,
2085
- reference
2086
- ]);
2087
- if (imported._tag === "error") return imported;
2088
- const original = await ownedImage(runtime, input, reference, "inspect");
2089
- if (original._tag === "error") return original;
2090
- const pushed = await docker2(runtime, input.dockerContext, "push", ["image", "push", reference]);
2091
- if (pushed._tag === "error") return pushed;
2092
- const removed = await removeOwnedTag(runtime, input, reference, "remove-local");
2093
- if (removed._tag === "error") return removed;
2094
- const absent = await imageExists(runtime, input, reference, "remove-local");
2095
- if (absent._tag === "error") return absent;
2096
- if (absent.value)
2097
- return error("remove-local", "Probe tag remained local; refusing to claim a pull roundtrip");
2098
- const pulled = await docker2(runtime, input.dockerContext, "pull", ["image", "pull", reference]);
2099
- if (pulled._tag === "error") return pulled;
2100
- const observed = await ownedImage(runtime, input, reference, "inspect");
2101
- if (observed._tag === "error") return observed;
2102
- return observed.value === original.value ? observed : error("inspect", "Pulled image ID differs from the imported image ID");
2103
- };
2104
- var parseProbeReference = (input) => {
2105
- if (!input.dockerContext.trim() || !Number.isInteger(input.registryPort) || input.registryPort < 1 || input.registryPort > 65535 || !/^[a-f0-9]{32,64}$/.test(input.nonce) || !path3.isAbsolute(input.temporaryDirectory))
2106
- return error(
2107
- "input",
2108
- "Expected an explicit context, TCP port, fresh 16 to 32 byte hex nonce and absolute temporary directory"
2109
- );
2110
- return {
2111
- _tag: "ok",
2112
- value: `127.0.0.1:${input.registryPort}/mend-registry-probe/${input.nonce}:probe`
2113
- };
2114
- };
2115
- var probeServerRegistry = async (runtime, input) => {
2116
- const cleanupWarnings = [];
2117
- const parsed = parseProbeReference(input);
2118
- if (parsed._tag === "error") return { ...parsed, cleanupWarnings };
2119
- const reference = parsed.value;
2120
- const exists = await imageExists(runtime, input, reference, "collision-check");
2121
- if (exists._tag === "error") return { ...exists, cleanupWarnings };
2122
- if (exists.value)
2123
- return {
2124
- ...error(
2125
- "collision-check",
2126
- "Probe reference already exists; supply a fresh nonce. Existing image retained"
2127
- ),
2128
- cleanupWarnings
2129
- };
2130
- let directory;
2131
- try {
2132
- directory = await fs3.mkdtemp(path3.join(input.temporaryDirectory, "mend-registry-probe-"));
2133
- } catch {
2134
- return {
2135
- ...error("temporary-files", "Could not create private probe directory"),
2136
- cleanupWarnings
2137
- };
2138
- }
2139
- let result;
2140
- let importAttempted = false;
2141
- try {
2142
- const tarPath = path3.join(directory, "rootfs.tar");
2143
- await fs3.writeFile(tarPath, tinyRootfs(input.nonce), { mode: 384, flag: "wx" });
2144
- importAttempted = true;
2145
- result = await roundtrip(runtime, input, reference, tarPath);
2146
- } catch {
2147
- result = error("temporary-files", "Could not write the private probe archive");
2148
- } finally {
2149
- if (importAttempted) {
2150
- const cleanup = await removeOwnedTag(runtime, input, reference, "cleanup");
2151
- if (cleanup._tag === "error") cleanupWarnings.push(cleanup.error);
2152
- }
2153
- try {
2154
- await fs3.rm(directory, { recursive: true, force: true });
2155
- } catch {
2156
- cleanupWarnings.push(
2157
- new ServerRegistryProbeError(
2158
- "cleanup",
2159
- `Could not remove private probe directory ${directory}`
2160
- )
2161
- );
2162
- }
2163
- }
2164
- return result._tag === "error" ? { ...result, cleanupWarnings } : {
2165
- _tag: "ok",
2166
- value: { reference, imageId: result.value },
2167
- cleanupWarnings
2168
- };
2169
- };
2170
-
2171
1971
  // src/server-runtime.ts
2172
1972
  import { spawn as spawn2 } from "node:child_process";
2173
- import * as fs4 from "node:fs";
2174
- import * as path4 from "node:path";
1973
+ import * as fs3 from "node:fs";
1974
+ import * as path3 from "node:path";
2175
1975
  var serverProcessDeadlines = {
2176
1976
  ordinary: 3e4,
2177
1977
  pull: 10 * 6e4,
@@ -2214,10 +2014,10 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
2214
2014
  ]) {
2215
2015
  if (parentEnvironment[key] !== void 0) env[key] = parentEnvironment[key];
2216
2016
  }
2217
- return new Promise((resolve5) => {
2017
+ return new Promise((resolve4) => {
2218
2018
  let fd;
2219
2019
  try {
2220
- if (options.stdoutFile !== void 0) fd = fs4.openSync(options.stdoutFile, "wx", 384);
2020
+ if (options.stdoutFile !== void 0) fd = fs3.openSync(options.stdoutFile, "wx", 384);
2221
2021
  const child = spawn2(command, [...args], {
2222
2022
  env,
2223
2023
  detached: process.platform !== "win32",
@@ -2262,23 +2062,23 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
2262
2062
  failure2 ??= `Process timed out after ${timeoutMs}ms`;
2263
2063
  terminate();
2264
2064
  }, timeoutMs);
2265
- child.once("error", (error2) => {
2266
- failure2 ??= error2.message;
2065
+ child.once("error", (error) => {
2066
+ failure2 ??= error.message;
2267
2067
  });
2268
2068
  child.once("close", (status) => {
2269
2069
  globalThis.clearTimeout(timer);
2270
2070
  try {
2271
- if (fd !== void 0) fs4.fsyncSync(fd);
2071
+ if (fd !== void 0) fs3.fsyncSync(fd);
2272
2072
  } catch {
2273
2073
  failure2 ??= "Could not durably write process output.";
2274
2074
  } finally {
2275
2075
  try {
2276
- if (fd !== void 0) fs4.closeSync(fd);
2076
+ if (fd !== void 0) fs3.closeSync(fd);
2277
2077
  } catch {
2278
2078
  failure2 ??= "Could not close process output file.";
2279
2079
  }
2280
2080
  }
2281
- resolve5({
2081
+ resolve4({
2282
2082
  status: failure2 === void 0 ? status : null,
2283
2083
  stdout,
2284
2084
  stderr,
@@ -2287,10 +2087,10 @@ var runServerProcess = (command, args, parentEnvironment, options = {}) => {
2287
2087
  });
2288
2088
  } catch (cause) {
2289
2089
  try {
2290
- if (fd !== void 0) fs4.closeSync(fd);
2090
+ if (fd !== void 0) fs3.closeSync(fd);
2291
2091
  } catch {
2292
2092
  }
2293
- resolve5({
2093
+ resolve4({
2294
2094
  status: null,
2295
2095
  stdout: "",
2296
2096
  stderr: "",
@@ -2308,17 +2108,17 @@ var serverComposeArgs = (installation, command) => [
2308
2108
  "--project-directory",
2309
2109
  installation.directory,
2310
2110
  "--env-file",
2311
- path4.join(installation.directory, "server.env"),
2111
+ path3.join(installation.directory, "server.env"),
2312
2112
  "-f",
2313
- path4.join(installation.directory, "compose.yaml"),
2113
+ path3.join(installation.directory, "compose.yaml"),
2314
2114
  ...command
2315
2115
  ];
2316
2116
 
2317
2117
  // src/server-store.ts
2318
2118
  import { randomUUID } from "node:crypto";
2319
- import * as fs5 from "node:fs";
2119
+ import * as fs4 from "node:fs";
2320
2120
  import * as os2 from "node:os";
2321
- import * as path5 from "node:path";
2121
+ import * as path4 from "node:path";
2322
2122
  var ServerStoreError = class extends Error {
2323
2123
  _tag = "ServerStoreError";
2324
2124
  };
@@ -2339,28 +2139,28 @@ var attempt = (operation) => {
2339
2139
  var hasCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
2340
2140
  var readOptional = (file) => {
2341
2141
  try {
2342
- return fs5.readFileSync(file, "utf8");
2142
+ return fs4.readFileSync(file, "utf8");
2343
2143
  } catch (cause) {
2344
2144
  if (hasCode(cause, "ENOENT")) return null;
2345
2145
  throw cause;
2346
2146
  }
2347
2147
  };
2348
2148
  var syncDirectory = (directory) => {
2349
- const fd = fs5.openSync(directory, "r");
2149
+ const fd = fs4.openSync(directory, "r");
2350
2150
  try {
2351
- fs5.fsyncSync(fd);
2151
+ fs4.fsyncSync(fd);
2352
2152
  } finally {
2353
- fs5.closeSync(fd);
2153
+ fs4.closeSync(fd);
2354
2154
  }
2355
2155
  };
2356
2156
  var writeDurable = (file, content, mode = 384) => {
2357
- const fd = fs5.openSync(file, "wx", mode);
2157
+ const fd = fs4.openSync(file, "wx", mode);
2358
2158
  try {
2359
- fs5.writeFileSync(fd, content, "utf8");
2360
- fs5.fchmodSync(fd, mode);
2361
- fs5.fsyncSync(fd);
2159
+ fs4.writeFileSync(fd, content, "utf8");
2160
+ fs4.fchmodSync(fd, mode);
2161
+ fs4.fsyncSync(fd);
2362
2162
  } finally {
2363
- fs5.closeSync(fd);
2163
+ fs4.closeSync(fd);
2364
2164
  }
2365
2165
  };
2366
2166
  var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
@@ -2380,7 +2180,7 @@ var parseLockOwner = (raw) => {
2380
2180
  };
2381
2181
  var describeOwner = (lockDir) => {
2382
2182
  try {
2383
- const raw = JSON.parse(fs5.readFileSync(path5.join(lockDir, "owner.json"), "utf8"));
2183
+ const raw = JSON.parse(fs4.readFileSync(path4.join(lockDir, "owner.json"), "utf8"));
2384
2184
  const owner = parseLockOwner(raw);
2385
2185
  if (owner === null) return "owner metadata is invalid";
2386
2186
  const label = `PID ${owner.pid} on ${owner.hostname}`;
@@ -2400,28 +2200,28 @@ var lockGuidance = (lockDir) => new ServerStoreError(
2400
2200
  );
2401
2201
  var acquireLock = (configDir, create) => {
2402
2202
  if (create) {
2403
- fs5.mkdirSync(configDir, { recursive: true, mode: 448 });
2404
- fs5.chmodSync(configDir, 448);
2405
- } else if (!fs5.existsSync(configDir)) {
2203
+ fs4.mkdirSync(configDir, { recursive: true, mode: 448 });
2204
+ fs4.chmodSync(configDir, 448);
2205
+ } else if (!fs4.existsSync(configDir)) {
2406
2206
  throw new ServerStoreError(
2407
2207
  "No Mend server is configured. Run mend server setup explicitly to install one."
2408
2208
  );
2409
2209
  }
2410
- const lockDir = path5.join(configDir, "server.lock");
2210
+ const lockDir = path4.join(configDir, "server.lock");
2411
2211
  try {
2412
- fs5.mkdirSync(lockDir, { mode: 448 });
2212
+ fs4.mkdirSync(lockDir, { mode: 448 });
2413
2213
  } catch (cause) {
2414
2214
  if (hasCode(cause, "EEXIST")) throw lockGuidance(lockDir);
2415
2215
  throw cause;
2416
2216
  }
2417
- const owned = fs5.statSync(lockDir);
2418
- const ownerFile = path5.join(lockDir, "owner.json");
2217
+ const owned = fs4.statSync(lockDir);
2218
+ const ownerFile = path4.join(lockDir, "owner.json");
2419
2219
  const owner = `${JSON.stringify({ pid: process.pid, hostname: os2.hostname(), token: randomUUID() })}
2420
2220
  `;
2421
2221
  let open = true;
2422
2222
  const assertOwned = () => {
2423
- const current = fs5.statSync(lockDir);
2424
- if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs5.readFileSync(ownerFile, "utf8") !== owner) {
2223
+ const current = fs4.statSync(lockDir);
2224
+ if (!open || current.dev !== owned.dev || current.ino !== owned.ino || fs4.readFileSync(ownerFile, "utf8") !== owner) {
2425
2225
  throw new ServerStoreError(
2426
2226
  "Server lock ownership was lost. Stop and check for another server command before retrying."
2427
2227
  );
@@ -2437,8 +2237,8 @@ var acquireLock = (configDir, create) => {
2437
2237
  release: () => {
2438
2238
  const result = attempt(() => {
2439
2239
  assertOwned();
2440
- fs5.unlinkSync(ownerFile);
2441
- fs5.rmdirSync(lockDir);
2240
+ fs4.unlinkSync(ownerFile);
2241
+ fs4.rmdirSync(lockDir);
2442
2242
  });
2443
2243
  open = false;
2444
2244
  return result;
@@ -2447,7 +2247,7 @@ var acquireLock = (configDir, create) => {
2447
2247
  };
2448
2248
  var readIdentity = (paths) => {
2449
2249
  const identity = readOptional(paths.identity);
2450
- if (identity === null && (fs5.existsSync(paths.active) || fs5.existsSync(paths.generations) && fs5.readdirSync(paths.generations).length > 0)) {
2250
+ if (identity === null && (fs4.existsSync(paths.active) || fs4.existsSync(paths.generations) && fs4.readdirSync(paths.generations).length > 0)) {
2451
2251
  throw new ServerStoreError(
2452
2252
  "Server identity.env is missing. Restore it from a retained generation or backup; refusing to generate replacement credentials."
2453
2253
  );
@@ -2457,7 +2257,7 @@ var readIdentity = (paths) => {
2457
2257
  var readActive = (paths) => {
2458
2258
  let target;
2459
2259
  try {
2460
- target = fs5.readlinkSync(paths.active);
2260
+ target = fs4.readlinkSync(paths.active);
2461
2261
  } catch (cause) {
2462
2262
  if (hasCode(cause, "ENOENT")) return null;
2463
2263
  throw cause;
@@ -2467,13 +2267,13 @@ var readActive = (paths) => {
2467
2267
  "Server active pointer is invalid. Restore the symlink to a retained generation before retrying."
2468
2268
  );
2469
2269
  }
2470
- const directory = path5.join(paths.configDir, target);
2270
+ const directory = path4.join(paths.configDir, target);
2471
2271
  const files = {
2472
- identity: fs5.readFileSync(path5.join(directory, fileNames.identity), "utf8"),
2473
- config: fs5.readFileSync(path5.join(directory, fileNames.config), "utf8"),
2474
- env: fs5.readFileSync(path5.join(directory, fileNames.env), "utf8"),
2475
- compose: fs5.readFileSync(path5.join(directory, fileNames.compose), "utf8"),
2476
- postgresInit: fs5.readFileSync(path5.join(directory, fileNames.postgresInit), "utf8")
2272
+ identity: fs4.readFileSync(path4.join(directory, fileNames.identity), "utf8"),
2273
+ config: fs4.readFileSync(path4.join(directory, fileNames.config), "utf8"),
2274
+ env: fs4.readFileSync(path4.join(directory, fileNames.env), "utf8"),
2275
+ compose: fs4.readFileSync(path4.join(directory, fileNames.compose), "utf8"),
2276
+ postgresInit: fs4.readFileSync(path4.join(directory, fileNames.postgresInit), "utf8")
2477
2277
  };
2478
2278
  if (readIdentity(paths) !== files.identity) {
2479
2279
  throw new ServerStoreError(
@@ -2483,20 +2283,20 @@ var readActive = (paths) => {
2483
2283
  return { directory, files };
2484
2284
  };
2485
2285
  var publishIdentity = (paths, identity) => {
2486
- const temporary = path5.join(paths.configDir, `.identity-${randomUUID()}`);
2286
+ const temporary = path4.join(paths.configDir, `.identity-${randomUUID()}`);
2487
2287
  writeDurable(temporary, identity);
2488
- fs5.linkSync(temporary, paths.identity);
2288
+ fs4.linkSync(temporary, paths.identity);
2489
2289
  syncDirectory(paths.configDir);
2490
- fs5.unlinkSync(temporary);
2290
+ fs4.unlinkSync(temporary);
2491
2291
  };
2492
2292
  var prepareGeneration = (paths, files) => {
2493
- fs5.mkdirSync(paths.generations, { recursive: true, mode: 448 });
2293
+ fs4.mkdirSync(paths.generations, { recursive: true, mode: 448 });
2494
2294
  const generationName = `gen-${randomUUID()}`;
2495
- const directory = path5.join(paths.generations, generationName);
2496
- fs5.mkdirSync(directory, { mode: 448 });
2295
+ const directory = path4.join(paths.generations, generationName);
2296
+ fs4.mkdirSync(directory, { mode: 448 });
2497
2297
  for (const key of fileKeys) {
2498
2298
  writeDurable(
2499
- path5.join(directory, fileNames[key]),
2299
+ path4.join(directory, fileNames[key]),
2500
2300
  files[key],
2501
2301
  key === "postgresInit" ? 493 : 384
2502
2302
  );
@@ -2507,18 +2307,18 @@ var prepareGeneration = (paths, files) => {
2507
2307
  return { directory, files };
2508
2308
  };
2509
2309
  var activateGeneration = (paths, generation) => {
2510
- const relative3 = path5.relative(paths.configDir, generation.directory);
2511
- if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative3) || readIdentity(paths) !== fs5.readFileSync(path5.join(generation.directory, "identity.env"), "utf8")) {
2310
+ const relative3 = path4.relative(paths.configDir, generation.directory);
2311
+ if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative3) || readIdentity(paths) !== fs4.readFileSync(path4.join(generation.directory, "identity.env"), "utf8")) {
2512
2312
  throw new ServerStoreError("Cannot activate a generation outside this installation identity.");
2513
2313
  }
2514
2314
  for (const key of fileKeys) {
2515
- if (fs5.readFileSync(path5.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
2315
+ if (fs4.readFileSync(path4.join(generation.directory, fileNames[key]), "utf8") !== generation.files[key]) {
2516
2316
  throw new ServerStoreError("Cannot activate an incomplete or changed server generation.");
2517
2317
  }
2518
2318
  }
2519
- const pointer = path5.join(paths.configDir, `.active-${randomUUID()}`);
2520
- fs5.symlinkSync(relative3, pointer);
2521
- fs5.renameSync(pointer, paths.active);
2319
+ const pointer = path4.join(paths.configDir, `.active-${randomUUID()}`);
2320
+ fs4.symlinkSync(relative3, pointer);
2321
+ fs4.renameSync(pointer, paths.active);
2522
2322
  syncDirectory(paths.configDir);
2523
2323
  };
2524
2324
  var prepareFiles = (paths, files) => {
@@ -2527,7 +2327,7 @@ var prepareFiles = (paths, files) => {
2527
2327
  throw new ServerStoreError("Server identity already exists; refusing to replace credentials.");
2528
2328
  }
2529
2329
  const active = readActive(paths);
2530
- if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs5.statSync(path5.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
2330
+ if (active !== null && fileKeys.every((key) => files[key] === active.files[key]) && (fs4.statSync(path4.join(active.directory, fileNames.postgresInit)).mode & 4095) === 493)
2531
2331
  return active;
2532
2332
  if (identity === null) publishIdentity(paths, files.identity);
2533
2333
  return prepareGeneration(paths, files);
@@ -2540,9 +2340,9 @@ var commitGeneration = (paths, files) => {
2540
2340
  var createStore = (configDir, lock) => {
2541
2341
  const paths = {
2542
2342
  configDir,
2543
- identity: path5.join(configDir, "identity.env"),
2544
- generations: path5.join(configDir, "generations"),
2545
- active: path5.join(configDir, "active")
2343
+ identity: path4.join(configDir, "identity.env"),
2344
+ generations: path4.join(configDir, "generations"),
2345
+ active: path4.join(configDir, "active")
2546
2346
  };
2547
2347
  const whileOwned = (operation) => attempt(() => {
2548
2348
  lock.assertOwned();
@@ -2555,13 +2355,13 @@ var createStore = (configDir, lock) => {
2555
2355
  prepare: (files) => whileOwned(() => prepareFiles(paths, files)),
2556
2356
  activate: (generation) => whileOwned(() => activateGeneration(paths, generation)),
2557
2357
  createBackup: (previous, target) => whileOwned(() => {
2558
- const backups = path5.join(configDir, "backups");
2559
- fs5.mkdirSync(backups, { recursive: true, mode: 448 });
2560
- fs5.chmodSync(backups, 448);
2561
- const directory = path5.join(backups, `upgrade-${randomUUID()}`);
2562
- fs5.mkdirSync(directory, { mode: 448 });
2358
+ const backups = path4.join(configDir, "backups");
2359
+ fs4.mkdirSync(backups, { recursive: true, mode: 448 });
2360
+ fs4.chmodSync(backups, 448);
2361
+ const directory = path4.join(backups, `upgrade-${randomUUID()}`);
2362
+ fs4.mkdirSync(directory, { mode: 448 });
2563
2363
  writeDurable(
2564
- path5.join(directory, "recovery.json"),
2364
+ path4.join(directory, "recovery.json"),
2565
2365
  `${JSON.stringify(
2566
2366
  {
2567
2367
  previousGeneration: previous.directory,
@@ -2577,21 +2377,21 @@ var createStore = (configDir, lock) => {
2577
2377
  syncDirectory(directory);
2578
2378
  syncDirectory(backups);
2579
2379
  syncDirectory(configDir);
2580
- const partialFile = path5.join(directory, "database.sql.partial");
2380
+ const partialFile = path4.join(directory, "database.sql.partial");
2581
2381
  return {
2582
2382
  directory,
2583
2383
  partialFile,
2584
2384
  complete: () => whileOwned(() => {
2585
- if (fs5.statSync(partialFile).size === 0)
2385
+ if (fs4.statSync(partialFile).size === 0)
2586
2386
  throw new ServerStoreError("Database backup is empty; refusing target startup.");
2587
- const fd = fs5.openSync(partialFile, "r");
2387
+ const fd = fs4.openSync(partialFile, "r");
2588
2388
  try {
2589
- fs5.fchmodSync(fd, 384);
2590
- fs5.fsyncSync(fd);
2389
+ fs4.fchmodSync(fd, 384);
2390
+ fs4.fsyncSync(fd);
2591
2391
  } finally {
2592
- fs5.closeSync(fd);
2392
+ fs4.closeSync(fd);
2593
2393
  }
2594
- fs5.renameSync(partialFile, path5.join(directory, "database.sql"));
2394
+ fs4.renameSync(partialFile, path4.join(directory, "database.sql"));
2595
2395
  syncDirectory(directory);
2596
2396
  })
2597
2397
  };
@@ -2600,7 +2400,7 @@ var createStore = (configDir, lock) => {
2600
2400
  };
2601
2401
  var refuseFlatLayout = (configDir) => {
2602
2402
  if (["server.json", "server.env", "compose.yaml", "postgres-init.sh"].some(
2603
- (name) => fs5.existsSync(path5.join(configDir, name))
2403
+ (name) => fs4.existsSync(path4.join(configDir, name))
2604
2404
  )) {
2605
2405
  throw new ServerStoreError(
2606
2406
  "Unreleased flat server configuration found. Preserve its credentials and volumes; migrate it to the generation layout before retrying. Setup will not replace this identity."
@@ -2608,7 +2408,7 @@ var refuseFlatLayout = (configDir) => {
2608
2408
  }
2609
2409
  };
2610
2410
  var withServerStore = async (directory, operation, options = {}) => {
2611
- const configDir = path5.resolve(directory);
2411
+ const configDir = path4.resolve(directory);
2612
2412
  let lock;
2613
2413
  let result;
2614
2414
  try {
@@ -2625,9 +2425,9 @@ var withServerStore = async (directory, operation, options = {}) => {
2625
2425
  };
2626
2426
 
2627
2427
  // src/version.ts
2628
- import * as fs6 from "node:fs";
2428
+ import * as fs5 from "node:fs";
2629
2429
  var cliVersion = () => {
2630
- const raw = fs6.readFileSync(new URL("../package.json", import.meta.url), "utf8");
2430
+ const raw = fs5.readFileSync(new URL("../package.json", import.meta.url), "utf8");
2631
2431
  const parsed = JSON.parse(raw);
2632
2432
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
2633
2433
  const version = parsed.version;
@@ -2661,12 +2461,13 @@ var versionLines = (cli, server) => {
2661
2461
 
2662
2462
  // src/server-setup.ts
2663
2463
  var CONFIG_SCHEMA_VERSION = 1;
2664
- var ASSET_CONTRACT = "mend-docker-v1";
2464
+ var ASSET_CONTRACT = "mend-docker-v2";
2465
+ var LEGACY_ASSET_CONTRACTS = /* @__PURE__ */ new Set(["mend-docker-v1"]);
2665
2466
  var MINIMUM_DOCKER_API = "1.45";
2666
2467
  var DEFAULT_APP_PORT = 3105;
2667
2468
  var DEFAULT_SSH_PORT = 2222;
2668
2469
  var DEFAULT_BIND = "127.0.0.1";
2669
- var COMPOSE_ASSET = "compose.v1.yaml";
2470
+ var COMPOSE_ASSET = "compose.v2.yaml";
2670
2471
  var POSTGRES_INIT_ASSET = "postgres-init.sh";
2671
2472
  var RELEASE_BASE = "https://github.com/sealant-sh/Mend/releases/download";
2672
2473
  var LATEST_RELEASE_URL = "https://api.github.com/repos/sealant-sh/Mend/releases/latest";
@@ -2726,7 +2527,6 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
2726
2527
  "--port",
2727
2528
  "--ssh-port",
2728
2529
  "--docker-socket",
2729
- "--registry-port",
2730
2530
  "--assets-dir",
2731
2531
  "--offline"
2732
2532
  ]);
@@ -2755,7 +2555,6 @@ var parseSetupOptions = (args) => {
2755
2555
  const version = flagValue2("--version");
2756
2556
  const appPort = flagValue2("--port");
2757
2557
  const sshPort = flagValue2("--ssh-port");
2758
- const registryPort = flagValue2("--registry-port");
2759
2558
  const origins = values.get("--origin");
2760
2559
  return {
2761
2560
  context: flagValue2("--context"),
@@ -2766,7 +2565,6 @@ var parseSetupOptions = (args) => {
2766
2565
  appPort: appPort === void 0 ? void 0 : parsePort(appPort, "--port"),
2767
2566
  sshPort: sshPort === void 0 ? void 0 : parsePort(sshPort, "--ssh-port"),
2768
2567
  dockerSocket: flagValue2("--docker-socket"),
2769
- registryPort: registryPort === void 0 ? void 0 : parsePort(registryPort, "--registry-port"),
2770
2568
  assetsDir: flagValue2("--assets-dir"),
2771
2569
  offline: values.has("--offline")
2772
2570
  };
@@ -2813,9 +2611,8 @@ var checkExposurePair = (bind, appUrl, requireExplicitUrl) => {
2813
2611
  var validateExposure = (existing, options) => {
2814
2612
  const appPort = options.appPort ?? existing?.appPort ?? DEFAULT_APP_PORT;
2815
2613
  const sshPort = options.sshPort ?? existing?.sshPort ?? DEFAULT_SSH_PORT;
2816
- const registryPort = options.registryPort ?? existing?.registryPort ?? 5e3;
2817
- if ((/* @__PURE__ */ new Set([appPort, sshPort, registryPort])).size !== 3) {
2818
- throw setupError("--port, --ssh-port and --registry-port must use different ports.");
2614
+ if (appPort === sshPort) {
2615
+ throw setupError("--port and --ssh-port must use different ports.");
2819
2616
  }
2820
2617
  const bind = options.bind ?? existing?.bind ?? DEFAULT_BIND;
2821
2618
  if (net2.isIP(bind) === 0) {
@@ -2830,7 +2627,7 @@ var validateExposure = (existing, options) => {
2830
2627
  const allowedOrigins = [
2831
2628
  ...new Set((requestedOrigins ?? inheritedOrigins).filter((origin) => origin !== appUrl))
2832
2629
  ];
2833
- return { bind, appUrl, allowedOrigins, appPort, sshPort, registryPort };
2630
+ return { bind, appUrl, allowedOrigins, appPort, sshPort };
2834
2631
  };
2835
2632
  var parseServerConfig = (raw) => {
2836
2633
  let decoded;
@@ -2871,16 +2668,17 @@ var parseServerConfig = (raw) => {
2871
2668
  ),
2872
2669
  appPort: requiredInteger(fields, "appPort"),
2873
2670
  sshPort: requiredInteger(fields, "sshPort"),
2874
- registryPort: fields.has("registryPort") ? requiredInteger(fields, "registryPort") : 5e3
2671
+ ...fields.has("registryPort") ? { registryPort: requiredInteger(fields, "registryPort") } : {}
2875
2672
  };
2876
- if (config.schemaVersion !== CONFIG_SCHEMA_VERSION || config.assetContract !== ASSET_CONTRACT) {
2673
+ if (config.schemaVersion !== CONFIG_SCHEMA_VERSION || config.assetContract !== ASSET_CONTRACT && !LEGACY_ASSET_CONTRACTS.has(config.assetContract)) {
2877
2674
  throw setupError(
2878
2675
  `Server config uses unsupported contract ${config.schemaVersion}/${config.assetContract}. Upgrade the CLI before setup.`
2879
2676
  );
2880
2677
  }
2881
2678
  parsePort(String(config.appPort), "Server config appPort");
2882
2679
  parsePort(String(config.sshPort), "Server config sshPort");
2883
- parsePort(String(config.registryPort), "Server config registryPort");
2680
+ if (config.registryPort !== void 0)
2681
+ parsePort(String(config.registryPort), "Server config registryPort");
2884
2682
  validateExposure(null, {
2885
2683
  context: void 0,
2886
2684
  version: void 0,
@@ -2890,7 +2688,6 @@ var parseServerConfig = (raw) => {
2890
2688
  appPort: config.appPort,
2891
2689
  sshPort: config.sshPort,
2892
2690
  dockerSocket: void 0,
2893
- registryPort: config.registryPort,
2894
2691
  assetsDir: void 0,
2895
2692
  offline: false
2896
2693
  });
@@ -2919,7 +2716,7 @@ var parseSecrets = (raw) => {
2919
2716
  postgresAdminPassword: envValue(fields, "MEND_POSTGRES_ADMIN_PASSWORD"),
2920
2717
  mendDatabasePassword: envValue(fields, "MEND_DB_PASSWORD"),
2921
2718
  sealantDatabasePassword: envValue(fields, "SEALANT_DB_PASSWORD"),
2922
- queuePassword: envValue(fields, "MEND_RABBITMQ_PASSWORD"),
2719
+ ...fields.has("MEND_RABBITMQ_PASSWORD") ? { queuePassword: envValue(fields, "MEND_RABBITMQ_PASSWORD") } : {},
2923
2720
  betterAuthSecret: envValue(fields, "BETTER_AUTH_SECRET"),
2924
2721
  sealantCredentialsKey: envValue(fields, "SEALANT_CREDENTIALS_KEY"),
2925
2722
  sealantServiceKey: envValue(fields, "SEALANT_SERVICE_KEY"),
@@ -2932,7 +2729,7 @@ var parseSecrets = (raw) => {
2932
2729
  ["MEND_POSTGRES_ADMIN_PASSWORD", secrets.postgresAdminPassword],
2933
2730
  ["MEND_DB_PASSWORD", secrets.mendDatabasePassword],
2934
2731
  ["SEALANT_DB_PASSWORD", secrets.sealantDatabasePassword],
2935
- ["MEND_RABBITMQ_PASSWORD", secrets.queuePassword],
2732
+ ...secrets.queuePassword === void 0 ? [] : [["MEND_RABBITMQ_PASSWORD", secrets.queuePassword]],
2936
2733
  ["WORKSPACE_SSH_GATEWAY_TOKEN", secrets.workspaceSshGatewayToken]
2937
2734
  ];
2938
2735
  for (const [name, value] of hexSecrets) {
@@ -2957,7 +2754,6 @@ var createSecrets = (runtime) => {
2957
2754
  postgresAdminPassword: hex(0),
2958
2755
  mendDatabasePassword: hex(32),
2959
2756
  sealantDatabasePassword: hex(64),
2960
- queuePassword: hex(96),
2961
2757
  betterAuthSecret: hex(128),
2962
2758
  sealantCredentialsKey: bytes.subarray(160, 192).toString("base64"),
2963
2759
  sealantServiceKey: `slt_svc_${hex(192)}`,
@@ -3132,8 +2928,6 @@ var validateComposeAsset = (body) => {
3132
2928
  "mend-control:",
3133
2929
  "mend-config:",
3134
2930
  "mend-ssh:",
3135
- "mend-rabbitmq:",
3136
- "mend-registry:",
3137
2931
  "mend-postgres:",
3138
2932
  "/var/lib/mend/store",
3139
2933
  "/run/sealant/sockets"
@@ -3161,7 +2955,7 @@ var renderIdentity = (secrets) => [
3161
2955
  `MEND_POSTGRES_ADMIN_PASSWORD=${secrets.postgresAdminPassword}`,
3162
2956
  `MEND_DB_PASSWORD=${secrets.mendDatabasePassword}`,
3163
2957
  `SEALANT_DB_PASSWORD=${secrets.sealantDatabasePassword}`,
3164
- `MEND_RABBITMQ_PASSWORD=${secrets.queuePassword}`,
2958
+ ...secrets.queuePassword === void 0 ? [] : [`MEND_RABBITMQ_PASSWORD=${secrets.queuePassword}`],
3165
2959
  `BETTER_AUTH_SECRET=${secrets.betterAuthSecret}`,
3166
2960
  `SEALANT_CREDENTIALS_KEY=${secrets.sealantCredentialsKey}`,
3167
2961
  `SEALANT_SERVICE_KEY=${secrets.sealantServiceKey}`,
@@ -3180,7 +2974,7 @@ var renderSecrets = (secrets, config) => {
3180
2974
  `MEND_BIND_HOST=${composeBind}`,
3181
2975
  `MEND_PORT=${config.appPort}`,
3182
2976
  `MEND_SSH_PORT=${config.sshPort}`,
3183
- `MEND_REGISTRY_PORT=${config.registryPort}`,
2977
+ ...config.registryPort === void 0 ? [] : [`MEND_REGISTRY_PORT=${config.registryPort}`],
3184
2978
  `SEALANT_SSH_HOST=${sshHost}`,
3185
2979
  "MEND_STORE_VOLUME_NAME=mend-store",
3186
2980
  "MEND_CONTROL_VOLUME_NAME=mend-control",
@@ -3262,7 +3056,7 @@ var resolveServerVersion = async (runtime, options, existing) => {
3262
3056
  return version;
3263
3057
  };
3264
3058
  var parseDockerSocketPath = (socket) => {
3265
- if (!path6.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
3059
+ if (!path5.isAbsolute(socket) || /[\r\n$'"#:\\]/.test(socket)) {
3266
3060
  throw setupError(
3267
3061
  "--docker-socket must be an absolute path without line breaks or Compose interpolation characters."
3268
3062
  );
@@ -3288,8 +3082,8 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
3288
3082
  if (options.assetsDir !== void 0) {
3289
3083
  try {
3290
3084
  const [compose2, postgresInit2] = await Promise.all([
3291
- readFile(path6.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
3292
- readFile(path6.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
3085
+ readFile(path5.join(options.assetsDir, COMPOSE_ASSET), "utf8"),
3086
+ readFile(path5.join(options.assetsDir, POSTGRES_INIT_ASSET), "utf8")
3293
3087
  ]);
3294
3088
  validateComposeAsset(compose2);
3295
3089
  validatePostgresAsset(postgresInit2);
@@ -3297,7 +3091,7 @@ var resolveAssets = async (runtime, serverVersion, existing, store, options) =>
3297
3091
  } catch (cause) {
3298
3092
  if (cause instanceof ServerSetupError) throw cause;
3299
3093
  throw setupError(
3300
- "Could not read release assets from --assets-dir. Supply compose.v1.yaml and postgres-init.sh."
3094
+ "Could not read release assets from --assets-dir. Supply compose.v2.yaml and postgres-init.sh."
3301
3095
  );
3302
3096
  }
3303
3097
  }
@@ -3408,6 +3202,11 @@ var setupServer = async (args, runtime, store) => {
3408
3202
  `Setup retains Mend ${existing.config.serverVersion}. Use mend server upgrade --version ${options.version} to change the server pin.`
3409
3203
  );
3410
3204
  }
3205
+ if (existing !== null && existing.config.assetContract !== ASSET_CONTRACT) {
3206
+ throw setupError(
3207
+ `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.`
3208
+ );
3209
+ }
3411
3210
  const selectedContext = await selectDockerContext(
3412
3211
  runtime,
3413
3212
  options.context ?? existing?.config.dockerContext
@@ -3440,7 +3239,6 @@ var setupServer = async (args, runtime, store) => {
3440
3239
  storeValue(store.activate(generation));
3441
3240
  await startCompose(runtime, config, generation);
3442
3241
  await probeHealth(runtime, config.appUrl, config.serverVersion);
3443
- await verifyRegistry(runtime, config);
3444
3242
  runtime.writeLine(`Mend ${config.serverVersion} is reachable at ${config.appUrl}`);
3445
3243
  runtime.writeLine(
3446
3244
  `Open ${config.appUrl}, create the first account, then run: mend login --url ${config.appUrl}`
@@ -3504,16 +3302,6 @@ var composeCommand = async (runtime, installation, args) => {
3504
3302
  var interruptionNotice = (runtime) => runtime.writeLine(
3505
3303
  "Connections will be interrupted. Workspace containers and data are retained, but active work can lose connectivity and may need reconnection. Mend does not stop workspace containers."
3506
3304
  );
3507
- var verifyRegistry = async (runtime, config) => {
3508
- const registry = await probeServerRegistry(runtime, {
3509
- dockerContext: config.dockerContext,
3510
- registryPort: config.registryPort,
3511
- nonce: runtime.randomBytes(24).toString("hex"),
3512
- temporaryDirectory: path6.resolve(runtime.configDir)
3513
- });
3514
- for (const warning of registry.cleanupWarnings) runtime.writeLine(`Warning: ${warning.message}`);
3515
- if (registry._tag === "error") throw setupError(registry.error.message);
3516
- };
3517
3305
  var startInstallation = async (runtime, installation) => {
3518
3306
  startingNotice(runtime, installation.config.serverVersion);
3519
3307
  await composeCommand(runtime, installation, [
@@ -3525,7 +3313,6 @@ var startInstallation = async (runtime, installation) => {
3525
3313
  "--no-build"
3526
3314
  ]);
3527
3315
  await probeHealth(runtime, installation.config.appUrl, installation.config.serverVersion);
3528
- await verifyRegistry(runtime, installation.config);
3529
3316
  runtime.writeLine(
3530
3317
  `Mend ${installation.config.serverVersion} is reachable at ${installation.config.appUrl}`
3531
3318
  );
@@ -3562,7 +3349,12 @@ var upgradeServer = async (args, runtime, store, existing) => {
3562
3349
  const previous = storeValue(store.readActive());
3563
3350
  if (previous === null) throw setupError("The active server generation is missing.");
3564
3351
  const assets = await resolveAssets(runtime, version, existing, store, options);
3565
- const config = { ...existing.config, serverVersion: version };
3352
+ const { registryPort: _legacyRegistryPort, ...carried } = existing.config;
3353
+ const config = {
3354
+ ...carried,
3355
+ assetContract: ASSET_CONTRACT,
3356
+ serverVersion: version
3357
+ };
3566
3358
  const secrets = parseSecrets(previous.files.identity);
3567
3359
  const files = {
3568
3360
  identity: previous.files.identity,
@@ -3734,9 +3526,9 @@ var manageServer = async (command, args, runtime, store) => {
3734
3526
  var nodeServerRuntime = () => {
3735
3527
  const environment = { ...process.env };
3736
3528
  const home = os3.homedir();
3737
- const configHome = environment["XDG_CONFIG_HOME"] ?? path6.join(home, ".config");
3529
+ const configHome = environment["XDG_CONFIG_HOME"] ?? path5.join(home, ".config");
3738
3530
  return {
3739
- configDir: path6.join(configHome, "mend"),
3531
+ configDir: path5.join(configHome, "mend"),
3740
3532
  platform: process.platform,
3741
3533
  cliVersion: cliVersion(),
3742
3534
  run: (command, args, options) => runServerProcess(command, args, environment, options),
@@ -3753,7 +3545,7 @@ var nodeServerRuntime = () => {
3753
3545
  }
3754
3546
  },
3755
3547
  randomBytes,
3756
- sleep: (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)),
3548
+ sleep: (milliseconds) => new Promise((resolve4) => setTimeout(resolve4, milliseconds)),
3757
3549
  writeLine: (line) => process.stdout.write(`${line}
3758
3550
  `)
3759
3551
  };
@@ -3962,8 +3754,8 @@ var renderMendToml = (proposals) => {
3962
3754
  };
3963
3755
 
3964
3756
  // src/skills.ts
3965
- import * as fs7 from "node:fs";
3966
- import * as path7 from "node:path";
3757
+ import * as fs6 from "node:fs";
3758
+ import * as path6 from "node:path";
3967
3759
  var DEFAULT_SKILLS_DIR = ".agents/skills";
3968
3760
  var MAX_SKILL_FILES = 64;
3969
3761
  var MAX_SKILL_FILE_BYTES = 512 * 1024;
@@ -3986,12 +3778,12 @@ var readFrontmatterDescription = (contents) => {
3986
3778
  var walkFiles = (root) => {
3987
3779
  const found = [];
3988
3780
  const visit = (dir) => {
3989
- for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
3781
+ for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
3990
3782
  if (IGNORED_DIRS.has(entry.name)) continue;
3991
- const absolute = path7.join(dir, entry.name);
3783
+ const absolute = path6.join(dir, entry.name);
3992
3784
  let stat;
3993
3785
  try {
3994
- stat = fs7.statSync(absolute);
3786
+ stat = fs6.statSync(absolute);
3995
3787
  } catch {
3996
3788
  continue;
3997
3789
  }
@@ -4005,7 +3797,7 @@ var walkFiles = (root) => {
4005
3797
  var scanSkillLibrary = (root) => {
4006
3798
  let entries;
4007
3799
  try {
4008
- entries = fs7.readdirSync(root, { withFileTypes: true });
3800
+ entries = fs6.readdirSync(root, { withFileTypes: true });
4009
3801
  } catch {
4010
3802
  return { error: `${root} is not a readable directory` };
4011
3803
  }
@@ -4013,23 +3805,23 @@ var scanSkillLibrary = (root) => {
4013
3805
  const notes = [];
4014
3806
  for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
4015
3807
  if (entry.name.startsWith(".")) continue;
4016
- const skillDir = path7.join(root, entry.name);
3808
+ const skillDir = path6.join(root, entry.name);
4017
3809
  let stat;
4018
3810
  try {
4019
- stat = fs7.statSync(skillDir);
3811
+ stat = fs6.statSync(skillDir);
4020
3812
  } catch {
4021
3813
  continue;
4022
3814
  }
4023
3815
  if (!stat.isDirectory()) continue;
4024
- if (!fs7.existsSync(path7.join(skillDir, "SKILL.md"))) {
3816
+ if (!fs6.existsSync(path6.join(skillDir, "SKILL.md"))) {
4025
3817
  notes.push({ skill: entry.name, message: "no SKILL.md \u2014 skipped" });
4026
3818
  continue;
4027
3819
  }
4028
3820
  const files = [];
4029
3821
  let bytes = 0;
4030
3822
  for (const absolute of walkFiles(skillDir)) {
4031
- const relative3 = path7.relative(skillDir, absolute).split(path7.sep).join("/");
4032
- const buffer = fs7.readFileSync(absolute);
3823
+ const relative3 = path6.relative(skillDir, absolute).split(path6.sep).join("/");
3824
+ const buffer = fs6.readFileSync(absolute);
4033
3825
  if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
4034
3826
  notes.push({ skill: entry.name, message: `${relative3} is over 512KB \u2014 skipped` });
4035
3827
  continue;
@@ -4065,15 +3857,15 @@ var scanSkillLibrary = (root) => {
4065
3857
 
4066
3858
  // src/ssh-setup.ts
4067
3859
  import * as os5 from "node:os";
4068
- import * as path9 from "node:path";
3860
+ import * as path8 from "node:path";
4069
3861
 
4070
3862
  // ../../packages/workspace-ssh/src/index.ts
4071
3863
  import { spawnSync } from "node:child_process";
4072
3864
  import { createHash as createHash2 } from "node:crypto";
4073
- import * as fs8 from "node:fs";
3865
+ import * as fs7 from "node:fs";
4074
3866
  import { isIP as isIP2 } from "node:net";
4075
3867
  import * as os4 from "node:os";
4076
- import * as path8 from "node:path";
3868
+ import * as path7 from "node:path";
4077
3869
  var LEGACY_BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
4078
3870
  var LEGACY_BLOCK_END = "# <<< mend workspace ssh <<<";
4079
3871
  var WorkspaceSshError = class extends Error {
@@ -4089,7 +3881,7 @@ var WorkspaceSshError = class extends Error {
4089
3881
  }
4090
3882
  };
4091
3883
  var success = (value) => ({ ok: true, value });
4092
- var failure = (error2) => ({ ok: false, error: error2 });
3884
+ var failure = (error) => ({ ok: false, error });
4093
3885
  var stripIpv6Brackets = (hostname6) => hostname6.startsWith("[") && hostname6.endsWith("]") ? hostname6.slice(1, -1) : hostname6;
4094
3886
  var hashIdentity = (identity) => createHash2("sha256").update(identity).digest("hex").slice(0, 24);
4095
3887
  var aliasLabel = (hostname6) => {
@@ -4160,8 +3952,8 @@ var blockBegin = (alias) => `# >>> mend workspace ssh ${alias} (managed) >>>`;
4160
3952
  var blockEnd = (alias) => `# <<< mend workspace ssh ${alias} <<<`;
4161
3953
  var hasControls = (value) => /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value);
4162
3954
  var parseIdentityPath = (value) => {
4163
- const resolved = path8.resolve(
4164
- value.startsWith("~/") ? path8.join(os4.homedir(), value.slice(2)) : value
3955
+ const resolved = path7.resolve(
3956
+ value.startsWith("~/") ? path7.join(os4.homedir(), value.slice(2)) : value
4165
3957
  );
4166
3958
  if (value === "" || hasControls(value) || hasControls(resolved) || value.includes("${") || resolved.includes("${")) {
4167
3959
  return failure(
@@ -4241,6 +4033,26 @@ var removeManagedBlock = (config, beginMarker, endMarker) => {
4241
4033
  }
4242
4034
  return success(`${config.slice(0, begin)}${after}`);
4243
4035
  };
4036
+ var stripManagedWorkspaceSshBlocks = (existing) => {
4037
+ let config = existing;
4038
+ let removed = 0;
4039
+ const aliases = [
4040
+ ...config.matchAll(/^# >>> mend workspace ssh (mend-ws-[a-z0-9-]+) \(managed\) >>>$/gm)
4041
+ ].map((match) => match[1]).filter((alias) => alias !== void 0);
4042
+ for (const alias of new Set(aliases)) {
4043
+ const stripped = removeManagedBlock(config, blockBegin(alias), blockEnd(alias));
4044
+ if (!stripped.ok) return stripped;
4045
+ config = stripped.value;
4046
+ removed += 1;
4047
+ }
4048
+ if (config.includes(LEGACY_BLOCK_BEGIN)) {
4049
+ const stripped = removeManagedBlock(config, LEGACY_BLOCK_BEGIN, LEGACY_BLOCK_END);
4050
+ if (!stripped.ok) return stripped;
4051
+ config = stripped.value;
4052
+ removed += 1;
4053
+ }
4054
+ return success({ config, removed });
4055
+ };
4244
4056
  var containsExactHost = (config, alias) => config.split("\n").some((line) => {
4245
4057
  const match = /^\s*Host\s+(.+?)\s*$/i.exec(line);
4246
4058
  return match?.[1]?.split(/\s+/).includes(alias) === true;
@@ -4297,10 +4109,10 @@ var agentKeys = () => {
4297
4109
  };
4298
4110
  var readText = (file) => {
4299
4111
  try {
4300
- return success(fs8.readFileSync(file, "utf8"));
4112
+ return success(fs7.readFileSync(file, "utf8"));
4301
4113
  } catch (cause) {
4302
- const error2 = cause instanceof Error && "code" in cause ? cause.code : void 0;
4303
- return error2 === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
4114
+ const error = cause instanceof Error && "code" in cause ? cause.code : void 0;
4115
+ return error === "ENOENT" ? success("") : failure(new WorkspaceSshError("key", `Could not read ${file}.`, cause));
4304
4116
  }
4305
4117
  };
4306
4118
  var configuredWorkspaceSshIdentityFile = (config, target) => {
@@ -4355,19 +4167,19 @@ var readRequiredKey = (keyPath, source) => {
4355
4167
  var readOptionalKey = (keyPath) => {
4356
4168
  const parsed = parseIdentityPath(keyPath);
4357
4169
  if (!parsed.ok) return parsed;
4358
- if (!fs8.existsSync(parsed.value) && !fs8.existsSync(publicKeyPath(parsed.value)))
4170
+ if (!fs7.existsSync(parsed.value) && !fs7.existsSync(publicKeyPath(parsed.value)))
4359
4171
  return success(null);
4360
4172
  return readRequiredKey(parsed.value, "existing");
4361
4173
  };
4362
4174
  var pinAgentKey = (configHome, publicKey) => {
4363
- const identityFile = path8.resolve(
4175
+ const identityFile = path7.resolve(
4364
4176
  configHome,
4365
4177
  "ssh",
4366
4178
  `agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
4367
4179
  );
4368
4180
  try {
4369
- fs8.mkdirSync(path8.dirname(identityFile), { recursive: true, mode: 448 });
4370
- fs8.writeFileSync(identityFile, `${publicKey}
4181
+ fs7.mkdirSync(path7.dirname(identityFile), { recursive: true, mode: 448 });
4182
+ fs7.writeFileSync(identityFile, `${publicKey}
4371
4183
  `, { mode: 384 });
4372
4184
  return success({ publicKey, identityFile, source: "agent" });
4373
4185
  } catch (cause) {
@@ -4378,10 +4190,10 @@ var pinAgentKey = (configHome, publicKey) => {
4378
4190
  };
4379
4191
  var generateDedicatedKey = (privatePath) => {
4380
4192
  try {
4381
- fs8.mkdirSync(path8.dirname(privatePath), { recursive: true, mode: 448 });
4193
+ fs7.mkdirSync(path7.dirname(privatePath), { recursive: true, mode: 448 });
4382
4194
  } catch (cause) {
4383
4195
  return failure(
4384
- new WorkspaceSshError("key", `Could not create ${path8.dirname(privatePath)}.`, cause)
4196
+ new WorkspaceSshError("key", `Could not create ${path7.dirname(privatePath)}.`, cause)
4385
4197
  );
4386
4198
  }
4387
4199
  const generated = spawnSync(
@@ -4410,7 +4222,7 @@ var generateDedicatedKey = (privatePath) => {
4410
4222
  return fresh;
4411
4223
  };
4412
4224
  var dedicatedKey = (configHome, create) => {
4413
- const privatePath = path8.join(configHome, "ssh", "id_ed25519");
4225
+ const privatePath = path7.join(configHome, "ssh", "id_ed25519");
4414
4226
  const existing = readOptionalKey(privatePath);
4415
4227
  if (existing.ok === false) return failure(existing.error);
4416
4228
  if (existing.value !== null || !create) return existing;
@@ -4458,10 +4270,10 @@ var writeWorkspaceSshConfig = (configFile, target, identityFile) => {
4458
4270
  const reconciled = reconcileWorkspaceSshConfig(before.value, target, identityFile);
4459
4271
  if (reconciled.ok === false) return failure(reconciled.error);
4460
4272
  try {
4461
- fs8.mkdirSync(path8.dirname(configFile), { recursive: true, mode: 448 });
4462
- fs8.chmodSync(path8.dirname(configFile), 448);
4463
- fs8.writeFileSync(configFile, reconciled.value, { mode: 384 });
4464
- fs8.chmodSync(configFile, 384);
4273
+ fs7.mkdirSync(path7.dirname(configFile), { recursive: true, mode: 448 });
4274
+ fs7.chmodSync(path7.dirname(configFile), 448);
4275
+ fs7.writeFileSync(configFile, reconciled.value, { mode: 384 });
4276
+ fs7.chmodSync(configFile, 384);
4465
4277
  return success(void 0);
4466
4278
  } catch (cause) {
4467
4279
  return failure(new WorkspaceSshError("config", `Could not write ${configFile}.`, cause));
@@ -4474,7 +4286,7 @@ var dim3 = ansi("2");
4474
4286
  var green3 = ansi("32");
4475
4287
  var warn = ansi("33");
4476
4288
  var say3 = (line) => console.log(line);
4477
- var sshConfigPath = () => path9.join(os5.homedir(), ".ssh", "config");
4289
+ var sshConfigPath = () => path8.join(os5.homedir(), ".ssh", "config");
4478
4290
  var flagValue = (args, flag) => {
4479
4291
  const index = args.indexOf(flag);
4480
4292
  return index === -1 || args[index + 1] === void 0 ? null : String(args[index + 1]);
@@ -4585,6 +4397,299 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
4585
4397
  }
4586
4398
  };
4587
4399
 
4400
+ // src/uninstall.ts
4401
+ import * as fs8 from "node:fs";
4402
+ import * as path9 from "node:path";
4403
+ var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
4404
+ var parseUninstallArgs = (args) => {
4405
+ let scope = null;
4406
+ let yes = false;
4407
+ for (const arg of args) {
4408
+ if (arg === "--yes" || arg === "-y") {
4409
+ yes = true;
4410
+ continue;
4411
+ }
4412
+ const next = arg === "--all" ? "all" : arg === "--server" ? "server" : arg === "--home" ? "home" : null;
4413
+ if (next === null) return { error: `${UNINSTALL_USAGE} \xB7 "${arg}" is not an option` };
4414
+ if (scope !== null && scope !== next) {
4415
+ return { error: `${UNINSTALL_USAGE} \xB7 pick one scope` };
4416
+ }
4417
+ scope = next;
4418
+ }
4419
+ return { scope, yes };
4420
+ };
4421
+ var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
4422
+ var countEntries = (directory) => {
4423
+ try {
4424
+ return fs8.readdirSync(directory).length;
4425
+ } catch {
4426
+ return 0;
4427
+ }
4428
+ };
4429
+ var exists = (file) => {
4430
+ try {
4431
+ fs8.lstatSync(file);
4432
+ return true;
4433
+ } catch {
4434
+ return false;
4435
+ }
4436
+ };
4437
+ var managedBlockCount = (sshConfigFile) => {
4438
+ let config;
4439
+ try {
4440
+ config = fs8.readFileSync(sshConfigFile, "utf8");
4441
+ } catch {
4442
+ return 0;
4443
+ }
4444
+ const stripped = stripManagedWorkspaceSshBlocks(config);
4445
+ return stripped.ok ? stripped.value.removed : 0;
4446
+ };
4447
+ var describeUninstall = async (runtime, scope) => {
4448
+ let server = null;
4449
+ if (scope !== "home") {
4450
+ const configDir = runtime.server.configDir;
4451
+ const read = await withServerStore(
4452
+ configDir,
4453
+ async (store) => {
4454
+ const installation = readServerInstallation(store);
4455
+ if (installation._tag === "error") throw installation.error;
4456
+ return installation.value;
4457
+ },
4458
+ { create: false }
4459
+ );
4460
+ if (read._tag === "error") {
4461
+ if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
4462
+ server = "none";
4463
+ } else if (read.value === null) {
4464
+ server = exists(path9.join(configDir, "identity.env")) ? {
4465
+ version: "(no active generation)",
4466
+ appUrl: "",
4467
+ dockerContext: "",
4468
+ generations: countEntries(path9.join(configDir, "generations")),
4469
+ backups: countEntries(path9.join(configDir, "backups"))
4470
+ } : "none";
4471
+ } else {
4472
+ server = {
4473
+ version: read.value.config.serverVersion,
4474
+ appUrl: read.value.config.appUrl,
4475
+ dockerContext: read.value.config.dockerContext,
4476
+ generations: countEntries(path9.join(configDir, "generations")),
4477
+ backups: countEntries(path9.join(configDir, "backups"))
4478
+ };
4479
+ }
4480
+ }
4481
+ let home = null;
4482
+ if (scope !== "server") {
4483
+ const cliConfig = path9.join(runtime.cliHome, "cli.json");
4484
+ const sshDirectory = path9.join(runtime.cliHome, "ssh");
4485
+ home = {
4486
+ cliConfig: exists(cliConfig) ? cliConfig : null,
4487
+ sshDirectory: exists(sshDirectory) ? sshDirectory : null,
4488
+ managedSshBlocks: managedBlockCount(runtime.sshConfigFile),
4489
+ signedIn: runtime.signedIn
4490
+ };
4491
+ }
4492
+ return { scope, server, home };
4493
+ };
4494
+ var plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
4495
+ var planLines = (plan, configDir) => {
4496
+ const lines = [];
4497
+ if (plan.server === "none") {
4498
+ lines.push(`server none installed under ${configDir}`);
4499
+ } else if (plan.server !== null) {
4500
+ const { version, appUrl, dockerContext, generations, backups } = plan.server;
4501
+ lines.push(
4502
+ `server Mend ${version}${appUrl === "" ? "" : ` at ${appUrl}`}${dockerContext === "" ? "" : ` \xB7 docker context ${dockerContext}`}`
4503
+ );
4504
+ if (dockerContext !== "") {
4505
+ lines.push(
4506
+ ` containers mend, postgres \xB7 volumes ${[MEND_DOCKER_NAMESPACE.store, MEND_DOCKER_NAMESPACE.control, "mend-config", "mend-ssh", "mend-postgres"].join(", ")} \xB7 image ghcr.io/sealant-sh/mend:${version}`
4507
+ );
4508
+ }
4509
+ lines.push(
4510
+ ` ${configDir}: identity.env, active, ${plural(generations, "generation")}, ${plural(backups, "backup")}`
4511
+ );
4512
+ }
4513
+ if (plan.home !== null) {
4514
+ const parts = [];
4515
+ if (plan.home.cliConfig !== null) {
4516
+ parts.push(
4517
+ plan.home.signedIn === null ? plan.home.cliConfig : `${plan.home.cliConfig} (signed in to ${plan.home.signedIn.url})`
4518
+ );
4519
+ }
4520
+ if (plan.home.sshDirectory !== null) parts.push(plan.home.sshDirectory);
4521
+ if (plan.home.managedSshBlocks > 0) {
4522
+ parts.push(`${plural(plan.home.managedSshBlocks, "managed block")} in ~/.ssh/config`);
4523
+ }
4524
+ lines.push(parts.length === 0 ? "home nothing of Mend's here" : `home ${parts[0]}`);
4525
+ for (const part of parts.slice(1)) lines.push(` ${part}`);
4526
+ }
4527
+ return lines;
4528
+ };
4529
+ var planDeletesData = (plan) => plan.server !== null && plan.server !== "none";
4530
+ var dockerArgs = (context, ...rest) => [
4531
+ "--context",
4532
+ context,
4533
+ ...rest
4534
+ ];
4535
+ var removeServer = async (runtime, failures, leftovers) => {
4536
+ const { server } = runtime;
4537
+ const configDir = server.configDir;
4538
+ const result = await withServerStore(
4539
+ configDir,
4540
+ async (store) => {
4541
+ const read = readServerInstallation(store);
4542
+ if (read._tag === "error") throw read.error;
4543
+ const installation = read.value;
4544
+ if (installation !== null) {
4545
+ const context = installation.config.dockerContext;
4546
+ const down = await server.run(
4547
+ "docker",
4548
+ serverComposeArgs({ directory: installation.directory, dockerContext: context }, [
4549
+ "down",
4550
+ "--volumes",
4551
+ "--remove-orphans",
4552
+ "--timeout",
4553
+ "30"
4554
+ ]),
4555
+ { timeoutMs: serverProcessDeadlines.stop }
4556
+ );
4557
+ if (down.status !== 0) {
4558
+ throw new Error(
4559
+ `docker compose down failed: ${(down.error ?? down.stderr.trim()) || "no output"}. Containers and files are retained; fix Docker and run mend uninstall again.`
4560
+ );
4561
+ }
4562
+ server.writeLine("removed containers mend, postgres and the Compose-owned volumes");
4563
+ const identity = store.readIdentity();
4564
+ if (identity._tag === "error") throw identity.error;
4565
+ const owned = identity.value === null ? null : await verifyServerDockerVolumes(server, {
4566
+ dockerContext: context,
4567
+ identityBytes: Buffer.from(identity.value)
4568
+ });
4569
+ if (owned !== null && owned._tag === "ok") {
4570
+ const removed = await server.run(
4571
+ "docker",
4572
+ dockerArgs(
4573
+ context,
4574
+ "volume",
4575
+ "rm",
4576
+ MEND_DOCKER_NAMESPACE.store,
4577
+ MEND_DOCKER_NAMESPACE.control
4578
+ )
4579
+ );
4580
+ if (removed.status === 0) {
4581
+ server.writeLine(
4582
+ `removed volumes ${MEND_DOCKER_NAMESPACE.store}, ${MEND_DOCKER_NAMESPACE.control}`
4583
+ );
4584
+ } else {
4585
+ failures.push(
4586
+ `could not remove volumes ${MEND_DOCKER_NAMESPACE.store}, ${MEND_DOCKER_NAMESPACE.control}: ${(removed.error ?? removed.stderr.trim()) || "no output"}`
4587
+ );
4588
+ }
4589
+ } else if (owned !== null && owned.error.reason === "missing") {
4590
+ server.writeLine(
4591
+ `volumes ${MEND_DOCKER_NAMESPACE.store}, ${MEND_DOCKER_NAMESPACE.control} were already gone`
4592
+ );
4593
+ } else {
4594
+ leftovers.push(
4595
+ `volumes ${MEND_DOCKER_NAMESPACE.store}, ${MEND_DOCKER_NAMESPACE.control}: ownership could not be confirmed for this installation, so they stay (docker --context ${context} volume ls)`
4596
+ );
4597
+ }
4598
+ const image = `ghcr.io/sealant-sh/mend:${installation.config.serverVersion}`;
4599
+ const untagged = await server.run("docker", dockerArgs(context, "image", "rm", image));
4600
+ if (untagged.status === 0) server.writeLine(`removed image ${image}`);
4601
+ else leftovers.push(`image ${image}: not removed (${untagged.stderr.trim() || "absent"})`);
4602
+ const workspaces = await server.run(
4603
+ "docker",
4604
+ dockerArgs(
4605
+ context,
4606
+ "container",
4607
+ "ls",
4608
+ "--all",
4609
+ "--filter",
4610
+ "name=^sealant-",
4611
+ "--format",
4612
+ "{{.Names}}"
4613
+ )
4614
+ );
4615
+ const names = workspaces.stdout.split(/\s+/).filter((name) => name !== "");
4616
+ if (workspaces.status === 0 && names.length > 0) {
4617
+ leftovers.push(
4618
+ `${plural(names.length, "workspace container")} left on docker context ${context} (${names.slice(0, 3).join(", ")}${names.length > 3 ? ", \u2026" : ""}): docker --context ${context} rm -f ${names.join(" ")}`
4619
+ );
4620
+ }
4621
+ }
4622
+ for (const name of SERVER_FILES) {
4623
+ fs8.rmSync(path9.join(configDir, name), { recursive: true, force: true });
4624
+ }
4625
+ server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
4626
+ },
4627
+ { create: false }
4628
+ );
4629
+ if (result._tag === "error") failures.push(result.error.message);
4630
+ };
4631
+ var removeHome = async (runtime, plan, failures, leftovers) => {
4632
+ const { server } = runtime;
4633
+ if (plan.cliConfig !== null) {
4634
+ fs8.rmSync(plan.cliConfig, { force: true });
4635
+ server.writeLine(`removed ${plan.cliConfig}`);
4636
+ }
4637
+ if (plan.sshDirectory !== null) {
4638
+ fs8.rmSync(plan.sshDirectory, { recursive: true, force: true });
4639
+ server.writeLine(`removed ${plan.sshDirectory}`);
4640
+ }
4641
+ if (plan.managedSshBlocks > 0) {
4642
+ try {
4643
+ const before = fs8.readFileSync(runtime.sshConfigFile, "utf8");
4644
+ const stripped = stripManagedWorkspaceSshBlocks(before);
4645
+ if (!stripped.ok) throw stripped.error;
4646
+ if (stripped.value.removed > 0) {
4647
+ fs8.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
4648
+ server.writeLine(
4649
+ `removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
4650
+ );
4651
+ }
4652
+ } catch (cause) {
4653
+ failures.push(
4654
+ `${runtime.sshConfigFile}: ${cause instanceof Error ? cause.message : String(cause)}`
4655
+ );
4656
+ }
4657
+ }
4658
+ let remaining = [];
4659
+ try {
4660
+ remaining = fs8.readdirSync(runtime.cliHome);
4661
+ } catch {
4662
+ return;
4663
+ }
4664
+ if (remaining.length === 0) {
4665
+ fs8.rmdirSync(runtime.cliHome);
4666
+ server.writeLine(`removed ${runtime.cliHome}`);
4667
+ } else {
4668
+ leftovers.push(
4669
+ `${runtime.cliHome} kept: ${remaining.join(", ")} ${remaining.length === 1 ? "is" : "are"} not this CLI's (a host-run server's store or keys, or another tool's files)`
4670
+ );
4671
+ }
4672
+ };
4673
+ var executeUninstall = async (runtime, plan) => {
4674
+ const failures = [];
4675
+ const leftovers = [];
4676
+ if (plan.home?.signedIn !== null && plan.home?.signedIn?.deviceId != null) {
4677
+ const failure2 = await runtime.revokeDevice();
4678
+ if (failure2 === null) {
4679
+ runtime.server.writeLine(`revoked this terminal's device on ${plan.home.signedIn.url}`);
4680
+ } else {
4681
+ leftovers.push(
4682
+ `device token on ${plan.home.signedIn.url}: ${failure2} (end it under Settings \u2192 Devices)`
4683
+ );
4684
+ }
4685
+ }
4686
+ if (plan.server !== null && plan.server !== "none") {
4687
+ await removeServer(runtime, failures, leftovers);
4688
+ }
4689
+ if (plan.home !== null) await removeHome(runtime, plan.home, failures, leftovers);
4690
+ return { failures, leftovers };
4691
+ };
4692
+
4588
4693
  // src/main.ts
4589
4694
  var mendCliHome = () => {
4590
4695
  const xdg = process.env["XDG_CONFIG_HOME"];
@@ -4700,8 +4805,8 @@ var request = async (config, method, route, body) => {
4700
4805
  var api = async (config, method, route, body) => {
4701
4806
  try {
4702
4807
  return await request(config, method, route, body);
4703
- } catch (error2) {
4704
- return fail4(error2 instanceof Error ? error2.message : String(error2));
4808
+ } catch (error) {
4809
+ return fail4(error instanceof Error ? error.message : String(error));
4705
4810
  }
4706
4811
  };
4707
4812
  var boundApi = (config) => (method, route, body) => api(config, method, route, body);
@@ -4927,8 +5032,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
4927
5032
  const ws = new WebSocket(url);
4928
5033
  ws.binaryType = "arraybuffer";
4929
5034
  try {
4930
- await new Promise((resolve5, reject) => {
4931
- ws.addEventListener("open", () => resolve5(), { once: true });
5035
+ await new Promise((resolve4, reject) => {
5036
+ ws.addEventListener("open", () => resolve4(), { once: true });
4932
5037
  ws.addEventListener("error", () => reject(new Error("could not connect")), { once: true });
4933
5038
  });
4934
5039
  } catch {
@@ -4936,8 +5041,8 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
4936
5041
  }
4937
5042
  const stopHerdrHint = hintHerdrAttachment(harness);
4938
5043
  let finishAttachment;
4939
- const finished = new Promise((resolve5) => {
4940
- finishAttachment = resolve5;
5044
+ const finished = new Promise((resolve4) => {
5045
+ finishAttachment = resolve4;
4941
5046
  });
4942
5047
  const onClose = () => finishAttachment?.();
4943
5048
  if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
@@ -5555,7 +5660,7 @@ var serviceLogs = async (config, args) => {
5555
5660
  cursor = page.nextFrom;
5556
5661
  const ended = page.status === "exited" || page.status === "failed";
5557
5662
  if (ended && !advanced) break;
5558
- if (!advanced) await new Promise((resolve5) => setTimeout(resolve5, 250));
5663
+ if (!advanced) await new Promise((resolve4) => setTimeout(resolve4, 250));
5559
5664
  }
5560
5665
  say4("");
5561
5666
  say4(dim4(`stream ended \xB7 next sequence ${cursor}`));
@@ -5670,16 +5775,16 @@ var tunnelServices = async (config, services, portOverride) => {
5670
5775
  socket.on("close", () => ws.close());
5671
5776
  socket.on("error", () => ws.close());
5672
5777
  });
5673
- await new Promise((resolve5, reject) => {
5674
- server.once("error", (error2) => {
5778
+ await new Promise((resolve4, reject) => {
5779
+ server.once("error", (error) => {
5675
5780
  reject(
5676
5781
  new Error(
5677
- error2.code === "EADDRINUSE" ? `127.0.0.1:${port} is already in use here \u2014 pick one with: mend service connect ${service.label} --port <n>` : error2.message
5782
+ error.code === "EADDRINUSE" ? `127.0.0.1:${port} is already in use here \u2014 pick one with: mend service connect ${service.label} --port <n>` : error.message
5678
5783
  )
5679
5784
  );
5680
5785
  });
5681
- server.listen(port, "127.0.0.1", () => resolve5());
5682
- }).catch((error2) => fail4(error2.message));
5786
+ server.listen(port, "127.0.0.1", () => resolve4());
5787
+ }).catch((error) => fail4(error.message));
5683
5788
  say4(
5684
5789
  `${green4("\u25CF")} ${service.label ?? service.id.slice(0, 8)} \u2192 127.0.0.1:${port} ${dim4(`(tunnel to ${config.url})`)}`
5685
5790
  );
@@ -5853,6 +5958,80 @@ var logout = async (config) => {
5853
5958
  saveCliConfig({ ...config, token: null, deviceId: null });
5854
5959
  say4(`${green4("\u2713")} signed out \xB7 ${dim4(`token removed from ${CONFIG_PATH}`)}`);
5855
5960
  };
5961
+ var UNINSTALL_CHOICES = [
5962
+ { scope: "all", text: "everything \xB7 the server on this machine and this machine's Mend files" },
5963
+ { scope: "server", text: "the server only \xB7 containers, volumes, configuration, backups" },
5964
+ {
5965
+ scope: "home",
5966
+ text: "this machine's files only \xB7 sign-in, workspace ssh key, ~/.ssh/config block"
5967
+ }
5968
+ ];
5969
+ var askUninstallScope = async () => {
5970
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
5971
+ return fail4(`${UNINSTALL_USAGE} \xB7 no terminal to ask on`);
5972
+ }
5973
+ say4("what should go?");
5974
+ UNINSTALL_CHOICES.forEach((choice, index) => say4(` ${index + 1}. ${choice.text}`));
5975
+ const readline = await import("node:readline/promises");
5976
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5977
+ const answer = (await rl.question(" 1, 2 or 3: ")).trim();
5978
+ rl.close();
5979
+ const chosen = UNINSTALL_CHOICES[Number(answer) - 1];
5980
+ if (chosen === void 0) return fail4(`"${answer}" is not one of the choices`);
5981
+ return chosen.scope;
5982
+ };
5983
+ var confirmUninstall = async (word) => {
5984
+ if (process.stdin.isTTY !== true) return fail4("non-interactive \u2014 pass --yes to remove");
5985
+ const readline = await import("node:readline/promises");
5986
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5987
+ const answer = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
5988
+ rl.close();
5989
+ return answer === word;
5990
+ };
5991
+ var uninstallCommand = async (config, args) => {
5992
+ const parsed = parseUninstallArgs(args);
5993
+ if ("error" in parsed) return fail4(parsed.error);
5994
+ const scope = parsed.scope ?? await askUninstallScope();
5995
+ const server = nodeServerRuntime();
5996
+ const runtime = {
5997
+ server,
5998
+ cliHome: mendCliHome(),
5999
+ sshConfigFile: path10.join(os6.homedir(), ".ssh", "config"),
6000
+ signedIn: config.token === null || !fs9.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
6001
+ revokeDevice: async () => {
6002
+ if (config.deviceId === null) return "no device id saved";
6003
+ try {
6004
+ await request(config, "DELETE", `/me/devices/${config.deviceId}`);
6005
+ return null;
6006
+ } catch (error) {
6007
+ return error instanceof Error ? error.message : String(error);
6008
+ }
6009
+ }
6010
+ };
6011
+ const plan = await describeUninstall(runtime, scope);
6012
+ say4(dim4(`mend uninstall \xB7 ${scope === "all" ? "everything" : scope}`));
6013
+ for (const line of planLines(plan, server.configDir)) say4(` ${line}`);
6014
+ const nothing = (plan.server === null || plan.server === "none") && (plan.home === null || plan.home.cliConfig === null && plan.home.sshDirectory === null && plan.home.managedSshBlocks === 0);
6015
+ if (nothing) {
6016
+ say4(dim4("nothing to remove"));
6017
+ return;
6018
+ }
6019
+ if (planDeletesData(plan)) {
6020
+ say4("repositories, worktrees, the database and its backups are deleted with the server");
6021
+ }
6022
+ if (!parsed.yes && !await confirmUninstall(planDeletesData(plan) ? "delete" : "y")) {
6023
+ say4(dim4("nothing removed"));
6024
+ return;
6025
+ }
6026
+ const outcome = await executeUninstall(runtime, plan);
6027
+ for (const line of outcome.leftovers) say4(dim4(` kept \xB7 ${line}`));
6028
+ if (outcome.failures.length > 0) {
6029
+ return fail4(outcome.failures.join("\n"));
6030
+ }
6031
+ say4(
6032
+ `${green4("\u2713")} ${scope === "all" ? "Mend is gone from this machine" : scope === "server" ? "the server is gone from this machine" : "this machine no longer holds Mend files"}`
6033
+ );
6034
+ };
5856
6035
  var printGitKey = (key) => {
5857
6036
  if (key.publicKey === null) return;
5858
6037
  say4(key.publicKey);
@@ -6308,6 +6487,7 @@ _mend() {
6308
6487
  'shell:open a shell in a live session workspace'
6309
6488
  'service:reachable ports \u2014 add, list, stop'
6310
6489
  'server:local server setup, lifecycle and upgrades'
6490
+ 'uninstall:remove the server, local Mend files, or both'
6311
6491
  'keys:the machine Mend deploy key \u2014 init, show, share'
6312
6492
  'skills:skill libraries \u2014 list, push'
6313
6493
  'accounts:your connected accounts on the platform'
@@ -6328,7 +6508,7 @@ _mend() {
6328
6508
  compadd setup status start stop restart logs upgrade
6329
6509
  else
6330
6510
  case $words[3] in
6331
- setup) compadd -- --version --assets-dir --offline --context --port --ssh-port --registry-port --bind --url --origin --docker-socket ;;
6511
+ setup) compadd -- --version --assets-dir --offline --context --port --ssh-port --bind --url --origin --docker-socket ;;
6332
6512
  upgrade) compadd -- --version --assets-dir --offline ;;
6333
6513
  start|restart) compadd -- --offline ;;
6334
6514
  logs) compadd -- --tail ;;
@@ -6347,7 +6527,7 @@ _mend "$@"
6347
6527
  var BASH_COMPLETIONS = `_mend() {
6348
6528
  local cur=\${COMP_WORDS[COMP_CWORD]}
6349
6529
  if [ "$COMP_CWORD" -eq 1 ]; then
6350
- COMPREPLY=( $(compgen -W "adopt codex claude opencode run attach stop shell service server keys skills pair doctor continue resume rejoin refresh projects sessions status ui help" -- "$cur") )
6530
+ COMPREPLY=( $(compgen -W "adopt codex claude opencode run attach stop shell service server uninstall keys skills pair doctor continue resume rejoin refresh projects sessions status ui help" -- "$cur") )
6351
6531
  return
6352
6532
  fi
6353
6533
  case \${COMP_WORDS[1]} in
@@ -6357,7 +6537,7 @@ var BASH_COMPLETIONS = `_mend() {
6357
6537
  options="setup status start stop restart logs upgrade"
6358
6538
  else
6359
6539
  case \${COMP_WORDS[2]} in
6360
- setup) options="--version --assets-dir --offline --context --port --ssh-port --registry-port --bind --url --origin --docker-socket" ;;
6540
+ setup) options="--version --assets-dir --offline --context --port --ssh-port --bind --url --origin --docker-socket" ;;
6361
6541
  upgrade) options="--version --assets-dir --offline" ;;
6362
6542
  start|restart) options="--offline" ;;
6363
6543
  logs) options="--tail" ;;
@@ -6539,10 +6719,10 @@ var resumeForRejoin = async (config, sessionId, label) => {
6539
6719
  request(config, "POST", `/sessions/${sessionId}/resume`, { harness: null })
6540
6720
  );
6541
6721
  return true;
6542
- } catch (error2) {
6722
+ } catch (error) {
6543
6723
  const refreshed = await api(config, "GET", `/sessions/${sessionId}`);
6544
6724
  if (agentIsLive(refreshed.session, refreshed.currentAgent)) return false;
6545
- return fail4(error2 instanceof Error ? error2.message : String(error2));
6725
+ return fail4(error instanceof Error ? error.message : String(error));
6546
6726
  }
6547
6727
  };
6548
6728
  var rejoinCommand = async (config, args) => {
@@ -6992,6 +7172,8 @@ var main = async () => {
6992
7172
  return login(config, rest);
6993
7173
  case "logout":
6994
7174
  return logout(config);
7175
+ case "uninstall":
7176
+ return uninstallCommand(config, rest);
6995
7177
  case "keys":
6996
7178
  return keysCommand(config, rest);
6997
7179
  case "dotfiles":