@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.
- package/README.md +5 -6
- package/dist/main.js +601 -419
- 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.1 +1 -1
- package/man/mend-continue.1 +1 -1
- package/man/mend-doctor.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-help.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-login.1 +1 -1
- package/man/mend-logout.1 +1 -1
- package/man/mend-man.1 +1 -1
- package/man/mend-pair.1 +1 -1
- package/man/mend-projects.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-server-logs.1 +1 -1
- package/man/mend-server-restart.1 +3 -3
- package/man/mend-server-setup.1 +6 -9
- package/man/mend-server-start.1 +3 -3
- package/man/mend-server-status.1 +1 -1
- package/man/mend-server-stop.1 +1 -1
- package/man/mend-server-upgrade.1 +3 -3
- 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-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-ssh-setup.1 +1 -1
- package/man/mend-ssh.1 +1 -1
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +1 -1
- package/man/mend-uninstall.1 +35 -0
- package/man/mend-version.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +5 -2
- 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((
|
|
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
|
-
|
|
57
|
+
resolve4(pending.subarray(0, 4 + pending.readUInt32BE(0)));
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
|
-
connection.on("error", (
|
|
60
|
+
connection.on("error", (error) => {
|
|
61
61
|
clearTimeout(timer);
|
|
62
|
-
reject(
|
|
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((
|
|
78
|
-
ws.addEventListener("close", () =>
|
|
77
|
+
const closed = new Promise((resolve4) => {
|
|
78
|
+
ws.addEventListener("close", () => resolve4(), { once: true });
|
|
79
79
|
});
|
|
80
|
-
const opened = await new Promise((
|
|
81
|
-
ws.addEventListener("open", () =>
|
|
82
|
-
ws.addEventListener("error", () =>
|
|
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 (
|
|
116
|
-
const message =
|
|
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((
|
|
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
|
-
(
|
|
180
|
-
|
|
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>] [--
|
|
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: "--
|
|
1067
|
-
text: "
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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((
|
|
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
|
|
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
|
|
2174
|
-
import * as
|
|
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((
|
|
2017
|
+
return new Promise((resolve4) => {
|
|
2218
2018
|
let fd;
|
|
2219
2019
|
try {
|
|
2220
|
-
if (options.stdoutFile !== void 0) fd =
|
|
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", (
|
|
2266
|
-
failure2 ??=
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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)
|
|
2090
|
+
if (fd !== void 0) fs3.closeSync(fd);
|
|
2291
2091
|
} catch {
|
|
2292
2092
|
}
|
|
2293
|
-
|
|
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
|
-
|
|
2111
|
+
path3.join(installation.directory, "server.env"),
|
|
2312
2112
|
"-f",
|
|
2313
|
-
|
|
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
|
|
2119
|
+
import * as fs4 from "node:fs";
|
|
2320
2120
|
import * as os2 from "node:os";
|
|
2321
|
-
import * as
|
|
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
|
|
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 =
|
|
2149
|
+
const fd = fs4.openSync(directory, "r");
|
|
2350
2150
|
try {
|
|
2351
|
-
|
|
2151
|
+
fs4.fsyncSync(fd);
|
|
2352
2152
|
} finally {
|
|
2353
|
-
|
|
2153
|
+
fs4.closeSync(fd);
|
|
2354
2154
|
}
|
|
2355
2155
|
};
|
|
2356
2156
|
var writeDurable = (file, content, mode = 384) => {
|
|
2357
|
-
const fd =
|
|
2157
|
+
const fd = fs4.openSync(file, "wx", mode);
|
|
2358
2158
|
try {
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2159
|
+
fs4.writeFileSync(fd, content, "utf8");
|
|
2160
|
+
fs4.fchmodSync(fd, mode);
|
|
2161
|
+
fs4.fsyncSync(fd);
|
|
2362
2162
|
} finally {
|
|
2363
|
-
|
|
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(
|
|
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
|
-
|
|
2404
|
-
|
|
2405
|
-
} else if (!
|
|
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 =
|
|
2210
|
+
const lockDir = path4.join(configDir, "server.lock");
|
|
2411
2211
|
try {
|
|
2412
|
-
|
|
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 =
|
|
2418
|
-
const ownerFile =
|
|
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 =
|
|
2424
|
-
if (!open || current.dev !== owned.dev || current.ino !== owned.ino ||
|
|
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
|
-
|
|
2441
|
-
|
|
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 && (
|
|
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 =
|
|
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 =
|
|
2270
|
+
const directory = path4.join(paths.configDir, target);
|
|
2471
2271
|
const files = {
|
|
2472
|
-
identity:
|
|
2473
|
-
config:
|
|
2474
|
-
env:
|
|
2475
|
-
compose:
|
|
2476
|
-
postgresInit:
|
|
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 =
|
|
2286
|
+
const temporary = path4.join(paths.configDir, `.identity-${randomUUID()}`);
|
|
2487
2287
|
writeDurable(temporary, identity);
|
|
2488
|
-
|
|
2288
|
+
fs4.linkSync(temporary, paths.identity);
|
|
2489
2289
|
syncDirectory(paths.configDir);
|
|
2490
|
-
|
|
2290
|
+
fs4.unlinkSync(temporary);
|
|
2491
2291
|
};
|
|
2492
2292
|
var prepareGeneration = (paths, files) => {
|
|
2493
|
-
|
|
2293
|
+
fs4.mkdirSync(paths.generations, { recursive: true, mode: 448 });
|
|
2494
2294
|
const generationName = `gen-${randomUUID()}`;
|
|
2495
|
-
const directory =
|
|
2496
|
-
|
|
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
|
-
|
|
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 =
|
|
2511
|
-
if (!/^generations\/gen-[0-9a-f-]{36}$/.test(relative3) || readIdentity(paths) !==
|
|
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 (
|
|
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 =
|
|
2520
|
-
|
|
2521
|
-
|
|
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]) && (
|
|
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:
|
|
2544
|
-
generations:
|
|
2545
|
-
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 =
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
const directory =
|
|
2562
|
-
|
|
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
|
-
|
|
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 =
|
|
2380
|
+
const partialFile = path4.join(directory, "database.sql.partial");
|
|
2581
2381
|
return {
|
|
2582
2382
|
directory,
|
|
2583
2383
|
partialFile,
|
|
2584
2384
|
complete: () => whileOwned(() => {
|
|
2585
|
-
if (
|
|
2385
|
+
if (fs4.statSync(partialFile).size === 0)
|
|
2586
2386
|
throw new ServerStoreError("Database backup is empty; refusing target startup.");
|
|
2587
|
-
const fd =
|
|
2387
|
+
const fd = fs4.openSync(partialFile, "r");
|
|
2588
2388
|
try {
|
|
2589
|
-
|
|
2590
|
-
|
|
2389
|
+
fs4.fchmodSync(fd, 384);
|
|
2390
|
+
fs4.fsyncSync(fd);
|
|
2591
2391
|
} finally {
|
|
2592
|
-
|
|
2392
|
+
fs4.closeSync(fd);
|
|
2593
2393
|
}
|
|
2594
|
-
|
|
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) =>
|
|
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 =
|
|
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
|
|
2428
|
+
import * as fs5 from "node:fs";
|
|
2629
2429
|
var cliVersion = () => {
|
|
2630
|
-
const raw =
|
|
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-
|
|
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.
|
|
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
|
-
|
|
2817
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
3292
|
-
readFile(
|
|
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.
|
|
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
|
|
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"] ??
|
|
3529
|
+
const configHome = environment["XDG_CONFIG_HOME"] ?? path5.join(home, ".config");
|
|
3738
3530
|
return {
|
|
3739
|
-
configDir:
|
|
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((
|
|
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
|
|
3966
|
-
import * as
|
|
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
|
|
3781
|
+
for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
|
|
3990
3782
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3991
|
-
const absolute =
|
|
3783
|
+
const absolute = path6.join(dir, entry.name);
|
|
3992
3784
|
let stat;
|
|
3993
3785
|
try {
|
|
3994
|
-
stat =
|
|
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 =
|
|
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 =
|
|
3808
|
+
const skillDir = path6.join(root, entry.name);
|
|
4017
3809
|
let stat;
|
|
4018
3810
|
try {
|
|
4019
|
-
stat =
|
|
3811
|
+
stat = fs6.statSync(skillDir);
|
|
4020
3812
|
} catch {
|
|
4021
3813
|
continue;
|
|
4022
3814
|
}
|
|
4023
3815
|
if (!stat.isDirectory()) continue;
|
|
4024
|
-
if (!
|
|
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 =
|
|
4032
|
-
const buffer =
|
|
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
|
|
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
|
|
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
|
|
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 = (
|
|
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 =
|
|
4164
|
-
value.startsWith("~/") ?
|
|
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(
|
|
4112
|
+
return success(fs7.readFileSync(file, "utf8"));
|
|
4301
4113
|
} catch (cause) {
|
|
4302
|
-
const
|
|
4303
|
-
return
|
|
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 (!
|
|
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 =
|
|
4175
|
+
const identityFile = path7.resolve(
|
|
4364
4176
|
configHome,
|
|
4365
4177
|
"ssh",
|
|
4366
4178
|
`agent-${hashIdentity(publicKeyIdentity(publicKey))}.pub`
|
|
4367
4179
|
);
|
|
4368
4180
|
try {
|
|
4369
|
-
|
|
4370
|
-
|
|
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
|
-
|
|
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 ${
|
|
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 =
|
|
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
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
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 = () =>
|
|
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 (
|
|
4704
|
-
return fail4(
|
|
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((
|
|
4931
|
-
ws.addEventListener("open", () =>
|
|
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((
|
|
4940
|
-
finishAttachment =
|
|
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((
|
|
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((
|
|
5674
|
-
server.once("error", (
|
|
5778
|
+
await new Promise((resolve4, reject) => {
|
|
5779
|
+
server.once("error", (error) => {
|
|
5675
5780
|
reject(
|
|
5676
5781
|
new Error(
|
|
5677
|
-
|
|
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", () =>
|
|
5682
|
-
}).catch((
|
|
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 --
|
|
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 --
|
|
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 (
|
|
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(
|
|
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":
|