@sealant/mend 0.25.0 → 0.27.2
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 -5
- package/dist/{chunk-4MX7ZC34.js → chunk-TBMOGB3T.js} +9 -1
- package/dist/{dashboard-JIWYW3EM.js → dashboard-5CBGTPSJ.js} +1 -1
- package/dist/main.js +613 -74
- 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 +2 -2
- package/man/mend-server-setup.1 +1 -1
- package/man/mend-server-start.1 +1 -1
- package/man/mend-server-status.1 +1 -1
- package/man/mend-server-stop.1 +2 -2
- package/man/mend-server-upgrade.1 +1 -1
- package/man/mend-server.1 +1 -1
- package/man/mend-service-add.1 +1 -1
- package/man/mend-service-connect.1 +1 -1
- package/man/mend-service-init.1 +1 -1
- package/man/mend-service-list.1 +1 -1
- package/man/mend-service-logs.1 +1 -1
- package/man/mend-service-restart.1 +1 -1
- package/man/mend-service-run.1 +1 -1
- package/man/mend-service-stop.1 +1 -1
- package/man/mend-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 +6 -3
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -17,15 +17,15 @@ import {
|
|
|
17
17
|
repositoryCloneUrlIssue,
|
|
18
18
|
sessionDisplayName,
|
|
19
19
|
trackBracketedPaste
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-TBMOGB3T.js";
|
|
21
21
|
|
|
22
22
|
// src/main.ts
|
|
23
23
|
import { spawn as spawn3, spawnSync as spawnSync2 } from "node:child_process";
|
|
24
|
-
import * as
|
|
24
|
+
import * as fs9 from "node:fs";
|
|
25
25
|
import { createRequire } from "node:module";
|
|
26
26
|
import * as net3 from "node:net";
|
|
27
27
|
import * as os6 from "node:os";
|
|
28
|
-
import * as
|
|
28
|
+
import * as path10 from "node:path";
|
|
29
29
|
|
|
30
30
|
// src/agent-share.ts
|
|
31
31
|
import * as net from "node:net";
|
|
@@ -1112,7 +1112,7 @@ var COMMANDS = [
|
|
|
1112
1112
|
{
|
|
1113
1113
|
name: "server stop",
|
|
1114
1114
|
section: "this machine",
|
|
1115
|
-
summary: "stop Mend and
|
|
1115
|
+
summary: "stop Mend, Postgres and Garage without deleting data",
|
|
1116
1116
|
synopsis: [""],
|
|
1117
1117
|
description: [
|
|
1118
1118
|
"Stops only the installation's Compose services. Connections are interrupted. Workspace containers and volumes remain, but active work may lose connectivity and need reconnection. No volume deletion or Docker prune is performed."
|
|
@@ -1125,7 +1125,7 @@ var COMMANDS = [
|
|
|
1125
1125
|
summary: "restart Mend using the same generation and pin",
|
|
1126
1126
|
synopsis: ["[--offline]"],
|
|
1127
1127
|
description: [
|
|
1128
|
-
"Checks preloaded images before stopping Mend, then starts the saved generation and verifies exact-version health. Postgres
|
|
1128
|
+
"Checks preloaded images before stopping Mend, then starts the saved generation and verifies exact-version health. Postgres and Garage stay running. Connections are interrupted; workspace containers and data remain, but active work may need reconnection."
|
|
1129
1129
|
],
|
|
1130
1130
|
options: [{ flag: "--offline", text: "use local release images and assets" }],
|
|
1131
1131
|
see: ["server status", "server logs"]
|
|
@@ -1213,6 +1213,30 @@ var COMMANDS = [
|
|
|
1213
1213
|
description: ["Other terminals and devices keep their own tokens."],
|
|
1214
1214
|
see: ["login"]
|
|
1215
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
|
+
},
|
|
1216
1240
|
{
|
|
1217
1241
|
name: "completions",
|
|
1218
1242
|
section: "this machine",
|
|
@@ -1735,7 +1759,7 @@ var loginCommand = async (args, deps) => {
|
|
|
1735
1759
|
};
|
|
1736
1760
|
|
|
1737
1761
|
// src/server-setup.ts
|
|
1738
|
-
import { randomBytes } from "node:crypto";
|
|
1762
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
1739
1763
|
import { readFile } from "node:fs/promises";
|
|
1740
1764
|
import * as net2 from "node:net";
|
|
1741
1765
|
import * as os3 from "node:os";
|
|
@@ -1748,6 +1772,11 @@ var MEND_DOCKER_NAMESPACE = {
|
|
|
1748
1772
|
store: "mend-store",
|
|
1749
1773
|
control: "mend-control"
|
|
1750
1774
|
};
|
|
1775
|
+
var MEND_DOCKER_NAMESPACE_WITH_GARAGE = {
|
|
1776
|
+
...MEND_DOCKER_NAMESPACE,
|
|
1777
|
+
garage: "mend-garage"
|
|
1778
|
+
};
|
|
1779
|
+
var secondaryVolumesOf = (namespace) => namespace.garage === void 0 ? [namespace.control] : [namespace.control, namespace.garage];
|
|
1751
1780
|
var SERVER_VOLUME_OWNER_LABEL = "dev.sealant.mend.installation";
|
|
1752
1781
|
var ServerVolumeOwnershipError = class extends Error {
|
|
1753
1782
|
/** Stable error discriminator. */
|
|
@@ -1839,9 +1868,10 @@ var inProjectNamespace = (name, project2) => ["-", "_"].some(
|
|
|
1839
1868
|
(separator) => name.startsWith(`${project2}${separator}`) && name.length > project2.length + 1
|
|
1840
1869
|
);
|
|
1841
1870
|
var reservedResourceName = (name, kind, namespace) => {
|
|
1842
|
-
if ([namespace.project, namespace.store, namespace.control].includes(name))
|
|
1871
|
+
if ([namespace.project, namespace.store, namespace.control, namespace.garage ?? ""].includes(name))
|
|
1872
|
+
return true;
|
|
1843
1873
|
return ["-", "_"].some(
|
|
1844
|
-
(separator) => (kind === "container" ? ["mend", "postgres"] : ["default"]).some((service) => {
|
|
1874
|
+
(separator) => (kind === "container" ? ["mend", "postgres", "garage"] : ["default"]).some((service) => {
|
|
1845
1875
|
const reserved = `${namespace.project}${separator}${service}`;
|
|
1846
1876
|
return name === reserved || kind === "container" && name.startsWith(reserved) && /^[-_](?:[0-9]+|run[-_][a-f0-9]+)$/.test(name.slice(reserved.length));
|
|
1847
1877
|
})
|
|
@@ -1877,7 +1907,9 @@ var inspectForeignProject = async (runtime, context, kind, name, project2) => {
|
|
|
1877
1907
|
}
|
|
1878
1908
|
};
|
|
1879
1909
|
var refuseOldData = async (runtime, context, namespace, volumes) => {
|
|
1880
|
-
if (volumes.some(
|
|
1910
|
+
if (volumes.some(
|
|
1911
|
+
(name) => secondaryVolumesOf(namespace).includes(name) || name.startsWith(`${namespace.project}_`)
|
|
1912
|
+
))
|
|
1881
1913
|
return fail3("unowned-data", "existing volumes without anchor");
|
|
1882
1914
|
for (const kind of ["volume", "container", "network"]) {
|
|
1883
1915
|
const labelled = await listNames(runtime, context, kind, namespace.project);
|
|
@@ -1906,9 +1938,9 @@ var refuseOldData = async (runtime, context, namespace, volumes) => {
|
|
|
1906
1938
|
};
|
|
1907
1939
|
var parseOwnershipInput = (input) => {
|
|
1908
1940
|
const namespace = input.namespace ?? MEND_DOCKER_NAMESPACE;
|
|
1909
|
-
if (!input.dockerContext.trim() || input.identityBytes.byteLength === 0 || !/^[a-z0-9][a-z0-9_-]*$/.test(namespace.project) || ![namespace.store, namespace
|
|
1941
|
+
if (!input.dockerContext.trim() || input.identityBytes.byteLength === 0 || !/^[a-z0-9][a-z0-9_-]*$/.test(namespace.project) || ![namespace.store, ...secondaryVolumesOf(namespace)].every(
|
|
1910
1942
|
(name) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/.test(name)
|
|
1911
|
-
) || namespace.store
|
|
1943
|
+
) || (/* @__PURE__ */ new Set([namespace.store, ...secondaryVolumesOf(namespace)])).size !== 1 + secondaryVolumesOf(namespace).length)
|
|
1912
1944
|
return fail3("invalid-input", "ownership inputs");
|
|
1913
1945
|
const owner = createHash("sha256").update(input.identityBytes).digest("hex");
|
|
1914
1946
|
return { _tag: "ok", value: { owner, namespace } };
|
|
@@ -1931,13 +1963,16 @@ var ownership = async (runtime, input, mode) => {
|
|
|
1931
1963
|
}
|
|
1932
1964
|
const current = await listNames(runtime, input.dockerContext, "volume");
|
|
1933
1965
|
if (current._tag === "error") return current;
|
|
1934
|
-
|
|
1935
|
-
const
|
|
1936
|
-
if (
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1966
|
+
for (const name of secondaryVolumesOf(namespace)) {
|
|
1967
|
+
const role = name === namespace.control ? "control" : "garage";
|
|
1968
|
+
if (current.value.includes(name)) {
|
|
1969
|
+
const owned = await inspectOwner(runtime, input.dockerContext, name, owner);
|
|
1970
|
+
if (owned._tag === "error") return owned;
|
|
1971
|
+
} else {
|
|
1972
|
+
if (mode === "verify") return fail3("missing", role);
|
|
1973
|
+
const created = await ensureVolume(runtime, input.dockerContext, name, owner);
|
|
1974
|
+
if (created._tag === "error") return created;
|
|
1975
|
+
}
|
|
1941
1976
|
}
|
|
1942
1977
|
return { _tag: "ok", value: { owner, namespace } };
|
|
1943
1978
|
};
|
|
@@ -2447,6 +2482,8 @@ var COMPOSE_ASSET = "compose.v2.yaml";
|
|
|
2447
2482
|
var POSTGRES_INIT_ASSET = "postgres-init.sh";
|
|
2448
2483
|
var RELEASE_BASE = "https://github.com/sealant-sh/Mend/releases/download";
|
|
2449
2484
|
var LATEST_RELEASE_URL = "https://api.github.com/repos/sealant-sh/Mend/releases/latest";
|
|
2485
|
+
var GARAGE_IMAGE = "dxflrs/garage:v2.4.1";
|
|
2486
|
+
var GARAGE_BUCKET = "mend";
|
|
2450
2487
|
var ServerSetupError = class extends Error {
|
|
2451
2488
|
_tag = "ServerSetupError";
|
|
2452
2489
|
};
|
|
@@ -2462,6 +2499,12 @@ var requiredString = (fields, key) => {
|
|
|
2462
2499
|
}
|
|
2463
2500
|
return value;
|
|
2464
2501
|
};
|
|
2502
|
+
var requiredBucket = (fields) => {
|
|
2503
|
+
const value = fields.get("bucket");
|
|
2504
|
+
if (value !== "garage")
|
|
2505
|
+
throw setupError("Server config is corrupt: bucket must be garage when present.");
|
|
2506
|
+
return value;
|
|
2507
|
+
};
|
|
2465
2508
|
var requiredInteger = (fields, key) => {
|
|
2466
2509
|
const value = fields.get(key);
|
|
2467
2510
|
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
@@ -2644,7 +2687,8 @@ var parseServerConfig = (raw) => {
|
|
|
2644
2687
|
),
|
|
2645
2688
|
appPort: requiredInteger(fields, "appPort"),
|
|
2646
2689
|
sshPort: requiredInteger(fields, "sshPort"),
|
|
2647
|
-
...fields.has("registryPort") ? { registryPort: requiredInteger(fields, "registryPort") } : {}
|
|
2690
|
+
...fields.has("registryPort") ? { registryPort: requiredInteger(fields, "registryPort") } : {},
|
|
2691
|
+
...fields.has("bucket") ? { bucket: requiredBucket(fields) } : {}
|
|
2648
2692
|
};
|
|
2649
2693
|
if (config.schemaVersion !== CONFIG_SCHEMA_VERSION || config.assetContract !== ASSET_CONTRACT && !LEGACY_ASSET_CONTRACTS.has(config.assetContract)) {
|
|
2650
2694
|
throw setupError(
|
|
@@ -2869,10 +2913,15 @@ var resolveLatestVersion = async (runtime) => {
|
|
|
2869
2913
|
if (version === "latest") throw setupError("GitHub returned an invalid latest release tag.");
|
|
2870
2914
|
return version;
|
|
2871
2915
|
};
|
|
2872
|
-
var
|
|
2916
|
+
var composeServiceNames = (body) => {
|
|
2873
2917
|
const [, afterServices = ""] = body.split(/^services:\s*$/m);
|
|
2874
2918
|
const [servicesBlock = ""] = afterServices.split(/^\S/m);
|
|
2875
|
-
|
|
2919
|
+
return [...servicesBlock.matchAll(/^ {2}([0-9A-Za-z_-]+):\s*$/gm)].map((match) => match[1]).filter((name) => name !== void 0).toSorted((left, right) => left.localeCompare(right));
|
|
2920
|
+
};
|
|
2921
|
+
var composeBucket = (body) => composeServiceNames(body).includes("garage") ? "garage" : void 0;
|
|
2922
|
+
var validateComposeAsset = (body) => {
|
|
2923
|
+
const serviceNames = composeServiceNames(body);
|
|
2924
|
+
const withGarage = serviceNames.includes("garage");
|
|
2876
2925
|
const volumeSections = body.split(/^volumes:[ \t]*$/m);
|
|
2877
2926
|
const volumeBlock = (volumeSections[1] ?? "").split(/^\S/m)[0] ?? "";
|
|
2878
2927
|
const volumeDeclarations = [
|
|
@@ -2880,7 +2929,8 @@ var validateComposeAsset = (body) => {
|
|
|
2880
2929
|
];
|
|
2881
2930
|
for (const [name, variable] of [
|
|
2882
2931
|
["mend-store", "MEND_STORE_VOLUME_NAME"],
|
|
2883
|
-
["mend-control", "MEND_CONTROL_VOLUME_NAME"]
|
|
2932
|
+
["mend-control", "MEND_CONTROL_VOLUME_NAME"],
|
|
2933
|
+
...withGarage ? [["mend-garage", "MEND_GARAGE_VOLUME_NAME"]] : []
|
|
2884
2934
|
]) {
|
|
2885
2935
|
const declarations = volumeDeclarations.filter((match) => match[1] === name);
|
|
2886
2936
|
const properties = (declarations[0]?.[2] ?? "").split("\n").map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#"));
|
|
@@ -2906,9 +2956,22 @@ var validateComposeAsset = (body) => {
|
|
|
2906
2956
|
"mend-ssh:",
|
|
2907
2957
|
"mend-postgres:",
|
|
2908
2958
|
"/var/lib/mend/store",
|
|
2909
|
-
"/run/sealant/sockets"
|
|
2959
|
+
"/run/sealant/sockets",
|
|
2960
|
+
// The capture store's bucket (a release since the capture store); an older release's asset
|
|
2961
|
+
// has none and is still a valid v2 bundle.
|
|
2962
|
+
...withGarage ? [
|
|
2963
|
+
`image: ${GARAGE_IMAGE}`,
|
|
2964
|
+
" garage:",
|
|
2965
|
+
"mend-garage:",
|
|
2966
|
+
"MEND_GARAGE_RPC_SECRET",
|
|
2967
|
+
"MEND_GARAGE_ADMIN_TOKEN",
|
|
2968
|
+
"MEND_GARAGE_KEY_ID",
|
|
2969
|
+
"MEND_GARAGE_KEY_SECRET",
|
|
2970
|
+
"MEND_BLOB_STORE",
|
|
2971
|
+
"MEND_SESSION_ENDPOINT_URL"
|
|
2972
|
+
] : []
|
|
2910
2973
|
];
|
|
2911
|
-
if (requiredFragments.some((fragment) => !body.includes(fragment)) || serviceNames.join(",") !== "mend,postgres") {
|
|
2974
|
+
if (requiredFragments.some((fragment) => !body.includes(fragment)) || serviceNames.join(",") !== "mend,postgres" && serviceNames.join(",") !== "garage,mend,postgres") {
|
|
2912
2975
|
throw setupError(`Downloaded ${COMPOSE_ASSET} does not implement ${ASSET_CONTRACT}.`);
|
|
2913
2976
|
}
|
|
2914
2977
|
};
|
|
@@ -2938,6 +3001,27 @@ var renderIdentity = (secrets) => [
|
|
|
2938
3001
|
`WORKSPACE_SSH_GATEWAY_TOKEN=${secrets.workspaceSshGatewayToken}`,
|
|
2939
3002
|
""
|
|
2940
3003
|
].join("\n");
|
|
3004
|
+
var garageSecrets = (secrets) => {
|
|
3005
|
+
const derive = (label) => createHmac("sha256", Buffer.from(secrets.betterAuthSecret, "hex")).update(label).digest("hex");
|
|
3006
|
+
return {
|
|
3007
|
+
rpcSecret: derive("mend-garage-rpc-secret"),
|
|
3008
|
+
adminToken: derive("mend-garage-admin-token"),
|
|
3009
|
+
keyId: `GK${derive("mend-garage-key-id").slice(0, 24)}`,
|
|
3010
|
+
keySecret: derive("mend-garage-key-secret")
|
|
3011
|
+
};
|
|
3012
|
+
};
|
|
3013
|
+
var renderGarage = (secrets, config) => {
|
|
3014
|
+
if (config.bucket !== "garage") return [];
|
|
3015
|
+
const garage = garageSecrets(secrets);
|
|
3016
|
+
return [
|
|
3017
|
+
`MEND_GARAGE_RPC_SECRET=${garage.rpcSecret}`,
|
|
3018
|
+
`MEND_GARAGE_ADMIN_TOKEN=${garage.adminToken}`,
|
|
3019
|
+
`MEND_GARAGE_KEY_ID=${garage.keyId}`,
|
|
3020
|
+
`MEND_GARAGE_KEY_SECRET=${garage.keySecret}`,
|
|
3021
|
+
"MEND_GARAGE_VOLUME_NAME=mend-garage"
|
|
3022
|
+
];
|
|
3023
|
+
};
|
|
3024
|
+
var namespaceOf = (config) => config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
|
|
2941
3025
|
var renderSecrets = (secrets, config) => {
|
|
2942
3026
|
const sshHost = parseUrl(config.appUrl, "Server config appUrl").hostname.replace(/^\[|\]$/g, "");
|
|
2943
3027
|
const composeBind = net2.isIP(config.bind) === 6 ? `[${config.bind}]` : config.bind;
|
|
@@ -2954,6 +3038,7 @@ var renderSecrets = (secrets, config) => {
|
|
|
2954
3038
|
`SEALANT_SSH_HOST=${sshHost}`,
|
|
2955
3039
|
"MEND_STORE_VOLUME_NAME=mend-store",
|
|
2956
3040
|
"MEND_CONTROL_VOLUME_NAME=mend-control",
|
|
3041
|
+
...renderGarage(secrets, config),
|
|
2957
3042
|
`DOCKER_SOCKET_PATH=${config.dockerSocket}`,
|
|
2958
3043
|
""
|
|
2959
3044
|
].join("\n");
|
|
@@ -3123,6 +3208,17 @@ var checkLocalImages = async (runtime, config, policy = "local") => {
|
|
|
3123
3208
|
);
|
|
3124
3209
|
if (postgres.status !== 0)
|
|
3125
3210
|
throw commandFailure("Preload postgres:17-alpine before continuing", postgres);
|
|
3211
|
+
if (config.bucket === "garage") {
|
|
3212
|
+
const garage = await inspectImage(
|
|
3213
|
+
runtime,
|
|
3214
|
+
config.dockerContext,
|
|
3215
|
+
GARAGE_IMAGE,
|
|
3216
|
+
"{{.Id}}",
|
|
3217
|
+
policy
|
|
3218
|
+
);
|
|
3219
|
+
if (garage.status !== 0)
|
|
3220
|
+
throw commandFailure(`Preload ${GARAGE_IMAGE} before continuing`, garage);
|
|
3221
|
+
}
|
|
3126
3222
|
};
|
|
3127
3223
|
var checkComposeImages = async (runtime, installation) => {
|
|
3128
3224
|
const output = await runtime.run(
|
|
@@ -3136,14 +3232,50 @@ var checkComposeImages = async (runtime, installation) => {
|
|
|
3136
3232
|
const images = output.stdout.trim().split(/\s+/).toSorted();
|
|
3137
3233
|
const expected = [
|
|
3138
3234
|
`ghcr.io/sealant-sh/mend:${installation.config.serverVersion}`,
|
|
3139
|
-
"postgres:17-alpine"
|
|
3235
|
+
"postgres:17-alpine",
|
|
3236
|
+
...installation.config.bucket === "garage" ? [GARAGE_IMAGE] : []
|
|
3140
3237
|
].toSorted();
|
|
3141
3238
|
if (images.join("\n") !== expected.join("\n")) {
|
|
3142
3239
|
throw setupError(
|
|
3143
|
-
|
|
3240
|
+
`Compose must use only the canonical pinned Mend image, official postgres:17-alpine${installation.config.bucket === "garage" ? ` and ${GARAGE_IMAGE}` : ""}.`
|
|
3144
3241
|
);
|
|
3145
3242
|
}
|
|
3146
3243
|
};
|
|
3244
|
+
var initGarage = async (runtime, installation, secrets) => {
|
|
3245
|
+
if (installation.config.bucket !== "garage") return;
|
|
3246
|
+
const garage = garageSecrets(secrets);
|
|
3247
|
+
const exec = (args) => runtime.run(
|
|
3248
|
+
"docker",
|
|
3249
|
+
serverComposeArgs(
|
|
3250
|
+
{ directory: installation.directory, dockerContext: installation.config.dockerContext },
|
|
3251
|
+
["exec", "-T", "garage", "/garage", "-c", "/etc/garage.toml", ...args]
|
|
3252
|
+
),
|
|
3253
|
+
{ timeoutMs: serverProcessDeadlines.ordinary }
|
|
3254
|
+
);
|
|
3255
|
+
const status = await exec(["status"]);
|
|
3256
|
+
if (status.status !== 0 || status.error !== void 0)
|
|
3257
|
+
throw commandFailure("Garage did not answer its status", status);
|
|
3258
|
+
const node = status.stdout.match(/^([0-9a-f]{16})\s/m)?.[1];
|
|
3259
|
+
if (node === void 0) throw setupError("Garage reported no node in `garage status`.");
|
|
3260
|
+
await exec(["layout", "assign", "-z", "mend", "-c", "1GB", node]);
|
|
3261
|
+
await exec(["layout", "apply", "--version", "1"]);
|
|
3262
|
+
await exec(["bucket", "create", GARAGE_BUCKET]);
|
|
3263
|
+
await exec(["key", "import", "--yes", "-n", GARAGE_BUCKET, garage.keyId, garage.keySecret]);
|
|
3264
|
+
await exec([
|
|
3265
|
+
"bucket",
|
|
3266
|
+
"allow",
|
|
3267
|
+
"--read",
|
|
3268
|
+
"--write",
|
|
3269
|
+
"--owner",
|
|
3270
|
+
GARAGE_BUCKET,
|
|
3271
|
+
"--key",
|
|
3272
|
+
GARAGE_BUCKET
|
|
3273
|
+
]);
|
|
3274
|
+
const info = await exec(["bucket", "info", GARAGE_BUCKET]);
|
|
3275
|
+
if (info.status !== 0 || info.error !== void 0 || !info.stdout.includes(garage.keyId))
|
|
3276
|
+
throw commandFailure(`Garage bucket ${GARAGE_BUCKET} is not readable by Mend's key`, info);
|
|
3277
|
+
runtime.writeLine(`Capture store bucket ${GARAGE_BUCKET} is laid out in Garage`);
|
|
3278
|
+
};
|
|
3147
3279
|
var startingNotice = (runtime, version) => runtime.writeLine(
|
|
3148
3280
|
`Starting Mend ${version} containers; Docker waits up to ${serverProcessDeadlines.composeWaitSeconds}s for them to report healthy`
|
|
3149
3281
|
);
|
|
@@ -3190,7 +3322,7 @@ var setupServer = async (args, runtime, store) => {
|
|
|
3190
3322
|
const operatingSystem = await checkDocker(runtime, selectedContext.name);
|
|
3191
3323
|
runtime.writeLine(`Using Docker context "${selectedContext.name}" (${selectedContext.endpoint})`);
|
|
3192
3324
|
const serverVersion = await resolveServerVersion(runtime, options, existing?.config ?? null);
|
|
3193
|
-
const
|
|
3325
|
+
const configWithoutBucket = {
|
|
3194
3326
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
3195
3327
|
assetContract: ASSET_CONTRACT,
|
|
3196
3328
|
serverVersion,
|
|
@@ -3203,17 +3335,24 @@ var setupServer = async (args, runtime, store) => {
|
|
|
3203
3335
|
...validateExposure(existing?.config ?? null, options)
|
|
3204
3336
|
};
|
|
3205
3337
|
const assets = await resolveAssets(runtime, serverVersion, existing, store, options);
|
|
3338
|
+
const bucket = composeBucket(assets.compose);
|
|
3339
|
+
const config = {
|
|
3340
|
+
...configWithoutBucket,
|
|
3341
|
+
...bucket === void 0 ? {} : { bucket }
|
|
3342
|
+
};
|
|
3206
3343
|
const secrets = savedSecrets ?? createSecrets(runtime);
|
|
3207
3344
|
const generation = persistSetup(store, config, secrets, assets);
|
|
3208
3345
|
await checkComposeImages(runtime, { directory: generation.directory, config });
|
|
3209
3346
|
const ownership2 = await claimServerDockerVolumes(runtime, {
|
|
3210
3347
|
dockerContext: config.dockerContext,
|
|
3211
|
-
identityBytes: Buffer.from(generation.files.identity)
|
|
3348
|
+
identityBytes: Buffer.from(generation.files.identity),
|
|
3349
|
+
namespace: namespaceOf(config)
|
|
3212
3350
|
});
|
|
3213
3351
|
if (ownership2._tag === "error") throw setupError(ownership2.error.message);
|
|
3214
3352
|
await checkLocalImages(runtime, config, options.offline ? "local" : "pull-missing");
|
|
3215
3353
|
storeValue(store.activate(generation));
|
|
3216
3354
|
await startCompose(runtime, config, generation);
|
|
3355
|
+
await initGarage(runtime, { directory: generation.directory, config }, secrets);
|
|
3217
3356
|
await probeHealth(runtime, config.appUrl, config.serverVersion);
|
|
3218
3357
|
runtime.writeLine(`Mend ${config.serverVersion} is reachable at ${config.appUrl}`);
|
|
3219
3358
|
runtime.writeLine(
|
|
@@ -3278,7 +3417,7 @@ var composeCommand = async (runtime, installation, args) => {
|
|
|
3278
3417
|
var interruptionNotice = (runtime) => runtime.writeLine(
|
|
3279
3418
|
"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."
|
|
3280
3419
|
);
|
|
3281
|
-
var startInstallation = async (runtime, installation) => {
|
|
3420
|
+
var startInstallation = async (runtime, installation, secrets) => {
|
|
3282
3421
|
startingNotice(runtime, installation.config.serverVersion);
|
|
3283
3422
|
await composeCommand(runtime, installation, [
|
|
3284
3423
|
"up",
|
|
@@ -3288,6 +3427,7 @@ var startInstallation = async (runtime, installation) => {
|
|
|
3288
3427
|
"never",
|
|
3289
3428
|
"--no-build"
|
|
3290
3429
|
]);
|
|
3430
|
+
await initGarage(runtime, installation, secrets);
|
|
3291
3431
|
await probeHealth(runtime, installation.config.appUrl, installation.config.serverVersion);
|
|
3292
3432
|
runtime.writeLine(
|
|
3293
3433
|
`Mend ${installation.config.serverVersion} is reachable at ${installation.config.appUrl}`
|
|
@@ -3325,11 +3465,17 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
3325
3465
|
const previous = storeValue(store.readActive());
|
|
3326
3466
|
if (previous === null) throw setupError("The active server generation is missing.");
|
|
3327
3467
|
const assets = await resolveAssets(runtime, version, existing, store, options);
|
|
3328
|
-
const {
|
|
3468
|
+
const {
|
|
3469
|
+
registryPort: _legacyRegistryPort,
|
|
3470
|
+
bucket: _previousBucket,
|
|
3471
|
+
...carried
|
|
3472
|
+
} = existing.config;
|
|
3473
|
+
const bucket = composeBucket(assets.compose);
|
|
3329
3474
|
const config = {
|
|
3330
3475
|
...carried,
|
|
3331
3476
|
assetContract: ASSET_CONTRACT,
|
|
3332
|
-
serverVersion: version
|
|
3477
|
+
serverVersion: version,
|
|
3478
|
+
...bucket === void 0 ? {} : { bucket }
|
|
3333
3479
|
};
|
|
3334
3480
|
const secrets = parseSecrets(previous.files.identity);
|
|
3335
3481
|
const files = {
|
|
@@ -3347,6 +3493,14 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
3347
3493
|
const target = storeValue(store.prepare(files));
|
|
3348
3494
|
const installation = { directory: target.directory, config };
|
|
3349
3495
|
await checkComposeImages(runtime, installation);
|
|
3496
|
+
if (config.bucket === "garage" && existing.config.bucket !== "garage") {
|
|
3497
|
+
const ownership2 = await claimServerDockerVolumes(runtime, {
|
|
3498
|
+
dockerContext: config.dockerContext,
|
|
3499
|
+
identityBytes: Buffer.from(previous.files.identity),
|
|
3500
|
+
namespace: namespaceOf(config)
|
|
3501
|
+
});
|
|
3502
|
+
if (ownership2._tag === "error") throw setupError(ownership2.error.message);
|
|
3503
|
+
}
|
|
3350
3504
|
const running = await composeCommand(runtime, existing, [
|
|
3351
3505
|
"ps",
|
|
3352
3506
|
"--status",
|
|
@@ -3394,7 +3548,7 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
3394
3548
|
recovery = "Could not reselect the previous generation. Inspect active before retrying any command.";
|
|
3395
3549
|
else if (appWasRunning) {
|
|
3396
3550
|
try {
|
|
3397
|
-
await startInstallation(runtime, existing);
|
|
3551
|
+
await startInstallation(runtime, existing, secrets);
|
|
3398
3552
|
recovery = "Previous pin and app recovered.";
|
|
3399
3553
|
} catch {
|
|
3400
3554
|
recovery = "Previous pin retained, but the old app could not recover. Run mend server start after fixing Docker.";
|
|
@@ -3406,7 +3560,7 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
3406
3560
|
);
|
|
3407
3561
|
}
|
|
3408
3562
|
try {
|
|
3409
|
-
await startInstallation(runtime, installation);
|
|
3563
|
+
await startInstallation(runtime, installation, secrets);
|
|
3410
3564
|
} catch {
|
|
3411
3565
|
throw setupError(
|
|
3412
3566
|
`Mend ${version} startup, exact-version health or registry verification failed; migrations may have begun. The target pin remains active. Do NOT downgrade or restore the database automatically. Run mend server logs --tail 100 and mend server status; fix the target, then mend server start --offline. Previous generation: ${previous.directory}. Target generation: ${target.directory}. Database backup and recovery record: ${backup.directory}.`
|
|
@@ -3456,9 +3610,11 @@ var manageServer = async (command, args, runtime, store) => {
|
|
|
3456
3610
|
if (identity === null) throw setupError("The persisted server identity is missing.");
|
|
3457
3611
|
const ownership2 = await verifyServerDockerVolumes(runtime, {
|
|
3458
3612
|
dockerContext: installation.config.dockerContext,
|
|
3459
|
-
identityBytes: Buffer.from(identity)
|
|
3613
|
+
identityBytes: Buffer.from(identity),
|
|
3614
|
+
namespace: namespaceOf(installation.config)
|
|
3460
3615
|
});
|
|
3461
3616
|
if (ownership2._tag === "error") throw setupError(ownership2.error.message);
|
|
3617
|
+
const secrets = parseSecrets(identity);
|
|
3462
3618
|
if (command === "upgrade") return upgradeServer(args, runtime, store, installation);
|
|
3463
3619
|
if (command === "logs") {
|
|
3464
3620
|
let tail = 100;
|
|
@@ -3488,7 +3644,7 @@ var manageServer = async (command, args, runtime, store) => {
|
|
|
3488
3644
|
interruptionNotice(runtime);
|
|
3489
3645
|
await composeCommand(runtime, installation, ["stop", "--timeout", "30"]);
|
|
3490
3646
|
runtime.writeLine(
|
|
3491
|
-
|
|
3647
|
+
`Mend, Postgres${installation.config.bucket === "garage" ? " and Garage" : ""} stopped. Volumes, configuration and workspace containers are retained.`
|
|
3492
3648
|
);
|
|
3493
3649
|
return;
|
|
3494
3650
|
}
|
|
@@ -3497,7 +3653,7 @@ var manageServer = async (command, args, runtime, store) => {
|
|
|
3497
3653
|
interruptionNotice(runtime);
|
|
3498
3654
|
await composeCommand(runtime, installation, ["stop", "--timeout", "30", "mend"]);
|
|
3499
3655
|
}
|
|
3500
|
-
await startInstallation(runtime, installation);
|
|
3656
|
+
await startInstallation(runtime, installation, secrets);
|
|
3501
3657
|
};
|
|
3502
3658
|
var nodeServerRuntime = () => {
|
|
3503
3659
|
const environment = { ...process.env };
|
|
@@ -4009,6 +4165,26 @@ var removeManagedBlock = (config, beginMarker, endMarker) => {
|
|
|
4009
4165
|
}
|
|
4010
4166
|
return success(`${config.slice(0, begin)}${after}`);
|
|
4011
4167
|
};
|
|
4168
|
+
var stripManagedWorkspaceSshBlocks = (existing) => {
|
|
4169
|
+
let config = existing;
|
|
4170
|
+
let removed = 0;
|
|
4171
|
+
const aliases = [
|
|
4172
|
+
...config.matchAll(/^# >>> mend workspace ssh (mend-ws-[a-z0-9-]+) \(managed\) >>>$/gm)
|
|
4173
|
+
].map((match) => match[1]).filter((alias) => alias !== void 0);
|
|
4174
|
+
for (const alias of new Set(aliases)) {
|
|
4175
|
+
const stripped = removeManagedBlock(config, blockBegin(alias), blockEnd(alias));
|
|
4176
|
+
if (!stripped.ok) return stripped;
|
|
4177
|
+
config = stripped.value;
|
|
4178
|
+
removed += 1;
|
|
4179
|
+
}
|
|
4180
|
+
if (config.includes(LEGACY_BLOCK_BEGIN)) {
|
|
4181
|
+
const stripped = removeManagedBlock(config, LEGACY_BLOCK_BEGIN, LEGACY_BLOCK_END);
|
|
4182
|
+
if (!stripped.ok) return stripped;
|
|
4183
|
+
config = stripped.value;
|
|
4184
|
+
removed += 1;
|
|
4185
|
+
}
|
|
4186
|
+
return success({ config, removed });
|
|
4187
|
+
};
|
|
4012
4188
|
var containsExactHost = (config, alias) => config.split("\n").some((line) => {
|
|
4013
4189
|
const match = /^\s*Host\s+(.+?)\s*$/i.exec(line);
|
|
4014
4190
|
return match?.[1]?.split(/\s+/).includes(alias) === true;
|
|
@@ -4353,22 +4529,308 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
|
|
|
4353
4529
|
}
|
|
4354
4530
|
};
|
|
4355
4531
|
|
|
4532
|
+
// src/uninstall.ts
|
|
4533
|
+
import * as fs8 from "node:fs";
|
|
4534
|
+
import * as path9 from "node:path";
|
|
4535
|
+
var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
|
|
4536
|
+
var parseUninstallArgs = (args) => {
|
|
4537
|
+
let scope = null;
|
|
4538
|
+
let yes = false;
|
|
4539
|
+
for (const arg of args) {
|
|
4540
|
+
if (arg === "--yes" || arg === "-y") {
|
|
4541
|
+
yes = true;
|
|
4542
|
+
continue;
|
|
4543
|
+
}
|
|
4544
|
+
const next = arg === "--all" ? "all" : arg === "--server" ? "server" : arg === "--home" ? "home" : null;
|
|
4545
|
+
if (next === null) return { error: `${UNINSTALL_USAGE} \xB7 "${arg}" is not an option` };
|
|
4546
|
+
if (scope !== null && scope !== next) {
|
|
4547
|
+
return { error: `${UNINSTALL_USAGE} \xB7 pick one scope` };
|
|
4548
|
+
}
|
|
4549
|
+
scope = next;
|
|
4550
|
+
}
|
|
4551
|
+
return { scope, yes };
|
|
4552
|
+
};
|
|
4553
|
+
var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
|
|
4554
|
+
var countEntries = (directory) => {
|
|
4555
|
+
try {
|
|
4556
|
+
return fs8.readdirSync(directory).length;
|
|
4557
|
+
} catch {
|
|
4558
|
+
return 0;
|
|
4559
|
+
}
|
|
4560
|
+
};
|
|
4561
|
+
var exists = (file) => {
|
|
4562
|
+
try {
|
|
4563
|
+
fs8.lstatSync(file);
|
|
4564
|
+
return true;
|
|
4565
|
+
} catch {
|
|
4566
|
+
return false;
|
|
4567
|
+
}
|
|
4568
|
+
};
|
|
4569
|
+
var managedBlockCount = (sshConfigFile) => {
|
|
4570
|
+
let config;
|
|
4571
|
+
try {
|
|
4572
|
+
config = fs8.readFileSync(sshConfigFile, "utf8");
|
|
4573
|
+
} catch {
|
|
4574
|
+
return 0;
|
|
4575
|
+
}
|
|
4576
|
+
const stripped = stripManagedWorkspaceSshBlocks(config);
|
|
4577
|
+
return stripped.ok ? stripped.value.removed : 0;
|
|
4578
|
+
};
|
|
4579
|
+
var describeUninstall = async (runtime, scope) => {
|
|
4580
|
+
let server = null;
|
|
4581
|
+
if (scope !== "home") {
|
|
4582
|
+
const configDir = runtime.server.configDir;
|
|
4583
|
+
const read = await withServerStore(
|
|
4584
|
+
configDir,
|
|
4585
|
+
async (store) => {
|
|
4586
|
+
const installation = readServerInstallation(store);
|
|
4587
|
+
if (installation._tag === "error") throw installation.error;
|
|
4588
|
+
return installation.value;
|
|
4589
|
+
},
|
|
4590
|
+
{ create: false }
|
|
4591
|
+
);
|
|
4592
|
+
if (read._tag === "error") {
|
|
4593
|
+
if (!read.error.message.startsWith("No Mend server is configured")) throw read.error;
|
|
4594
|
+
server = "none";
|
|
4595
|
+
} else if (read.value === null) {
|
|
4596
|
+
server = exists(path9.join(configDir, "identity.env")) ? {
|
|
4597
|
+
version: "(no active generation)",
|
|
4598
|
+
appUrl: "",
|
|
4599
|
+
dockerContext: "",
|
|
4600
|
+
generations: countEntries(path9.join(configDir, "generations")),
|
|
4601
|
+
backups: countEntries(path9.join(configDir, "backups"))
|
|
4602
|
+
} : "none";
|
|
4603
|
+
} else {
|
|
4604
|
+
server = {
|
|
4605
|
+
version: read.value.config.serverVersion,
|
|
4606
|
+
appUrl: read.value.config.appUrl,
|
|
4607
|
+
dockerContext: read.value.config.dockerContext,
|
|
4608
|
+
generations: countEntries(path9.join(configDir, "generations")),
|
|
4609
|
+
backups: countEntries(path9.join(configDir, "backups"))
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
let home = null;
|
|
4614
|
+
if (scope !== "server") {
|
|
4615
|
+
const cliConfig = path9.join(runtime.cliHome, "cli.json");
|
|
4616
|
+
const sshDirectory = path9.join(runtime.cliHome, "ssh");
|
|
4617
|
+
home = {
|
|
4618
|
+
cliConfig: exists(cliConfig) ? cliConfig : null,
|
|
4619
|
+
sshDirectory: exists(sshDirectory) ? sshDirectory : null,
|
|
4620
|
+
managedSshBlocks: managedBlockCount(runtime.sshConfigFile),
|
|
4621
|
+
signedIn: runtime.signedIn
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4624
|
+
return { scope, server, home };
|
|
4625
|
+
};
|
|
4626
|
+
var plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
4627
|
+
var planLines = (plan, configDir) => {
|
|
4628
|
+
const lines = [];
|
|
4629
|
+
if (plan.server === "none") {
|
|
4630
|
+
lines.push(`server none installed under ${configDir}`);
|
|
4631
|
+
} else if (plan.server !== null) {
|
|
4632
|
+
const { version, appUrl, dockerContext, generations, backups } = plan.server;
|
|
4633
|
+
lines.push(
|
|
4634
|
+
`server Mend ${version}${appUrl === "" ? "" : ` at ${appUrl}`}${dockerContext === "" ? "" : ` \xB7 docker context ${dockerContext}`}`
|
|
4635
|
+
);
|
|
4636
|
+
if (dockerContext !== "") {
|
|
4637
|
+
lines.push(
|
|
4638
|
+
` containers mend, postgres, garage \xB7 volumes ${[MEND_DOCKER_NAMESPACE_WITH_GARAGE.store, ...secondaryVolumesOf(MEND_DOCKER_NAMESPACE_WITH_GARAGE), "mend-config", "mend-ssh", "mend-postgres"].join(", ")} \xB7 image ghcr.io/sealant-sh/mend:${version}`
|
|
4639
|
+
);
|
|
4640
|
+
}
|
|
4641
|
+
lines.push(
|
|
4642
|
+
` ${configDir}: identity.env, active, ${plural(generations, "generation")}, ${plural(backups, "backup")}`
|
|
4643
|
+
);
|
|
4644
|
+
}
|
|
4645
|
+
if (plan.home !== null) {
|
|
4646
|
+
const parts = [];
|
|
4647
|
+
if (plan.home.cliConfig !== null) {
|
|
4648
|
+
parts.push(
|
|
4649
|
+
plan.home.signedIn === null ? plan.home.cliConfig : `${plan.home.cliConfig} (signed in to ${plan.home.signedIn.url})`
|
|
4650
|
+
);
|
|
4651
|
+
}
|
|
4652
|
+
if (plan.home.sshDirectory !== null) parts.push(plan.home.sshDirectory);
|
|
4653
|
+
if (plan.home.managedSshBlocks > 0) {
|
|
4654
|
+
parts.push(`${plural(plan.home.managedSshBlocks, "managed block")} in ~/.ssh/config`);
|
|
4655
|
+
}
|
|
4656
|
+
lines.push(parts.length === 0 ? "home nothing of Mend's here" : `home ${parts[0]}`);
|
|
4657
|
+
for (const part of parts.slice(1)) lines.push(` ${part}`);
|
|
4658
|
+
}
|
|
4659
|
+
return lines;
|
|
4660
|
+
};
|
|
4661
|
+
var planDeletesData = (plan) => plan.server !== null && plan.server !== "none";
|
|
4662
|
+
var dockerArgs = (context, ...rest) => [
|
|
4663
|
+
"--context",
|
|
4664
|
+
context,
|
|
4665
|
+
...rest
|
|
4666
|
+
];
|
|
4667
|
+
var removeServer = async (runtime, failures, leftovers) => {
|
|
4668
|
+
const { server } = runtime;
|
|
4669
|
+
const configDir = server.configDir;
|
|
4670
|
+
const result = await withServerStore(
|
|
4671
|
+
configDir,
|
|
4672
|
+
async (store) => {
|
|
4673
|
+
const read = readServerInstallation(store);
|
|
4674
|
+
if (read._tag === "error") throw read.error;
|
|
4675
|
+
const installation = read.value;
|
|
4676
|
+
if (installation !== null) {
|
|
4677
|
+
const context = installation.config.dockerContext;
|
|
4678
|
+
const down = await server.run(
|
|
4679
|
+
"docker",
|
|
4680
|
+
serverComposeArgs({ directory: installation.directory, dockerContext: context }, [
|
|
4681
|
+
"down",
|
|
4682
|
+
"--volumes",
|
|
4683
|
+
"--remove-orphans",
|
|
4684
|
+
"--timeout",
|
|
4685
|
+
"30"
|
|
4686
|
+
]),
|
|
4687
|
+
{ timeoutMs: serverProcessDeadlines.stop }
|
|
4688
|
+
);
|
|
4689
|
+
if (down.status !== 0) {
|
|
4690
|
+
throw new Error(
|
|
4691
|
+
`docker compose down failed: ${(down.error ?? down.stderr.trim()) || "no output"}. Containers and files are retained; fix Docker and run mend uninstall again.`
|
|
4692
|
+
);
|
|
4693
|
+
}
|
|
4694
|
+
server.writeLine("removed containers mend, postgres, garage and the Compose-owned volumes");
|
|
4695
|
+
const namespace = installation.config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
|
|
4696
|
+
const dataVolumes = [namespace.store, ...secondaryVolumesOf(namespace)];
|
|
4697
|
+
const identity = store.readIdentity();
|
|
4698
|
+
if (identity._tag === "error") throw identity.error;
|
|
4699
|
+
const owned = identity.value === null ? null : await verifyServerDockerVolumes(server, {
|
|
4700
|
+
dockerContext: context,
|
|
4701
|
+
identityBytes: Buffer.from(identity.value),
|
|
4702
|
+
namespace
|
|
4703
|
+
});
|
|
4704
|
+
if (owned !== null && owned._tag === "ok") {
|
|
4705
|
+
const removed = await server.run(
|
|
4706
|
+
"docker",
|
|
4707
|
+
dockerArgs(context, "volume", "rm", ...dataVolumes)
|
|
4708
|
+
);
|
|
4709
|
+
if (removed.status === 0) {
|
|
4710
|
+
server.writeLine(`removed volumes ${dataVolumes.join(", ")}`);
|
|
4711
|
+
} else {
|
|
4712
|
+
failures.push(
|
|
4713
|
+
`could not remove volumes ${dataVolumes.join(", ")}: ${(removed.error ?? removed.stderr.trim()) || "no output"}`
|
|
4714
|
+
);
|
|
4715
|
+
}
|
|
4716
|
+
} else if (owned !== null && owned.error.reason === "missing") {
|
|
4717
|
+
server.writeLine(`volumes ${dataVolumes.join(", ")} were already gone`);
|
|
4718
|
+
} else {
|
|
4719
|
+
leftovers.push(
|
|
4720
|
+
`volumes ${dataVolumes.join(", ")}: ownership could not be confirmed for this installation, so they stay (docker --context ${context} volume ls)`
|
|
4721
|
+
);
|
|
4722
|
+
}
|
|
4723
|
+
const image = `ghcr.io/sealant-sh/mend:${installation.config.serverVersion}`;
|
|
4724
|
+
const untagged = await server.run("docker", dockerArgs(context, "image", "rm", image));
|
|
4725
|
+
if (untagged.status === 0) server.writeLine(`removed image ${image}`);
|
|
4726
|
+
else leftovers.push(`image ${image}: not removed (${untagged.stderr.trim() || "absent"})`);
|
|
4727
|
+
const workspaces = await server.run(
|
|
4728
|
+
"docker",
|
|
4729
|
+
dockerArgs(
|
|
4730
|
+
context,
|
|
4731
|
+
"container",
|
|
4732
|
+
"ls",
|
|
4733
|
+
"--all",
|
|
4734
|
+
"--filter",
|
|
4735
|
+
"name=^sealant-",
|
|
4736
|
+
"--format",
|
|
4737
|
+
"{{.Names}}"
|
|
4738
|
+
)
|
|
4739
|
+
);
|
|
4740
|
+
const names = workspaces.stdout.split(/\s+/).filter((name) => name !== "");
|
|
4741
|
+
if (workspaces.status === 0 && names.length > 0) {
|
|
4742
|
+
leftovers.push(
|
|
4743
|
+
`${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(" ")}`
|
|
4744
|
+
);
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
for (const name of SERVER_FILES) {
|
|
4748
|
+
fs8.rmSync(path9.join(configDir, name), { recursive: true, force: true });
|
|
4749
|
+
}
|
|
4750
|
+
server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
|
|
4751
|
+
},
|
|
4752
|
+
{ create: false }
|
|
4753
|
+
);
|
|
4754
|
+
if (result._tag === "error") failures.push(result.error.message);
|
|
4755
|
+
};
|
|
4756
|
+
var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
4757
|
+
const { server } = runtime;
|
|
4758
|
+
if (plan.cliConfig !== null) {
|
|
4759
|
+
fs8.rmSync(plan.cliConfig, { force: true });
|
|
4760
|
+
server.writeLine(`removed ${plan.cliConfig}`);
|
|
4761
|
+
}
|
|
4762
|
+
if (plan.sshDirectory !== null) {
|
|
4763
|
+
fs8.rmSync(plan.sshDirectory, { recursive: true, force: true });
|
|
4764
|
+
server.writeLine(`removed ${plan.sshDirectory}`);
|
|
4765
|
+
}
|
|
4766
|
+
if (plan.managedSshBlocks > 0) {
|
|
4767
|
+
try {
|
|
4768
|
+
const before = fs8.readFileSync(runtime.sshConfigFile, "utf8");
|
|
4769
|
+
const stripped = stripManagedWorkspaceSshBlocks(before);
|
|
4770
|
+
if (!stripped.ok) throw stripped.error;
|
|
4771
|
+
if (stripped.value.removed > 0) {
|
|
4772
|
+
fs8.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
|
|
4773
|
+
server.writeLine(
|
|
4774
|
+
`removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
|
|
4775
|
+
);
|
|
4776
|
+
}
|
|
4777
|
+
} catch (cause) {
|
|
4778
|
+
failures.push(
|
|
4779
|
+
`${runtime.sshConfigFile}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
4780
|
+
);
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
let remaining = [];
|
|
4784
|
+
try {
|
|
4785
|
+
remaining = fs8.readdirSync(runtime.cliHome);
|
|
4786
|
+
} catch {
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4789
|
+
if (remaining.length === 0) {
|
|
4790
|
+
fs8.rmdirSync(runtime.cliHome);
|
|
4791
|
+
server.writeLine(`removed ${runtime.cliHome}`);
|
|
4792
|
+
} else {
|
|
4793
|
+
leftovers.push(
|
|
4794
|
+
`${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)`
|
|
4795
|
+
);
|
|
4796
|
+
}
|
|
4797
|
+
};
|
|
4798
|
+
var executeUninstall = async (runtime, plan) => {
|
|
4799
|
+
const failures = [];
|
|
4800
|
+
const leftovers = [];
|
|
4801
|
+
if (plan.home?.signedIn !== null && plan.home?.signedIn?.deviceId != null) {
|
|
4802
|
+
const failure2 = await runtime.revokeDevice();
|
|
4803
|
+
if (failure2 === null) {
|
|
4804
|
+
runtime.server.writeLine(`revoked this terminal's device on ${plan.home.signedIn.url}`);
|
|
4805
|
+
} else {
|
|
4806
|
+
leftovers.push(
|
|
4807
|
+
`device token on ${plan.home.signedIn.url}: ${failure2} (end it under Settings \u2192 Devices)`
|
|
4808
|
+
);
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
if (plan.server !== null && plan.server !== "none") {
|
|
4812
|
+
await removeServer(runtime, failures, leftovers);
|
|
4813
|
+
}
|
|
4814
|
+
if (plan.home !== null) await removeHome(runtime, plan.home, failures, leftovers);
|
|
4815
|
+
return { failures, leftovers };
|
|
4816
|
+
};
|
|
4817
|
+
|
|
4356
4818
|
// src/main.ts
|
|
4357
4819
|
var mendCliHome = () => {
|
|
4358
4820
|
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
4359
|
-
const preferred =
|
|
4360
|
-
xdg === void 0 || xdg === "" ?
|
|
4821
|
+
const preferred = path10.join(
|
|
4822
|
+
xdg === void 0 || xdg === "" ? path10.join(os6.homedir(), ".config") : xdg,
|
|
4361
4823
|
"mend"
|
|
4362
4824
|
);
|
|
4363
|
-
const legacy =
|
|
4364
|
-
return !
|
|
4825
|
+
const legacy = path10.join(os6.homedir(), ".mend");
|
|
4826
|
+
return !fs9.existsSync(preferred) && fs9.existsSync(legacy) ? legacy : preferred;
|
|
4365
4827
|
};
|
|
4366
|
-
var CONFIG_PATH =
|
|
4828
|
+
var CONFIG_PATH = path10.join(mendCliHome(), "cli.json");
|
|
4367
4829
|
var loadConfig = () => {
|
|
4368
4830
|
let fileConfig = {};
|
|
4369
|
-
if (
|
|
4831
|
+
if (fs9.existsSync(CONFIG_PATH)) {
|
|
4370
4832
|
try {
|
|
4371
|
-
fileConfig = JSON.parse(
|
|
4833
|
+
fileConfig = JSON.parse(fs9.readFileSync(CONFIG_PATH, "utf8"));
|
|
4372
4834
|
} catch {
|
|
4373
4835
|
fail4(`could not parse ${CONFIG_PATH}`);
|
|
4374
4836
|
}
|
|
@@ -4505,7 +4967,7 @@ var adopt = async (config, args) => {
|
|
|
4505
4967
|
}
|
|
4506
4968
|
const sourceIssue = repositoryCloneUrlIssue(source);
|
|
4507
4969
|
if (sourceIssue !== null) return fail4(sourceIssue);
|
|
4508
|
-
const projectName = name ?? normalizeProjectName(
|
|
4970
|
+
const projectName = name ?? normalizeProjectName(path10.basename(source, ".git"));
|
|
4509
4971
|
const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
|
|
4510
4972
|
if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
|
|
4511
4973
|
return fail4(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
|
|
@@ -4550,7 +5012,7 @@ var findProject = async (config, explicit, adoptCwd = false) => {
|
|
|
4550
5012
|
const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
|
|
4551
5013
|
if (sourceIssue !== null) return fail4(sourceIssue);
|
|
4552
5014
|
const adopted = await api(config, "POST", "/projects", {
|
|
4553
|
-
name: normalizeProjectName(
|
|
5015
|
+
name: normalizeProjectName(path10.basename(facts.repoRoot)),
|
|
4554
5016
|
source: facts.originUrl
|
|
4555
5017
|
});
|
|
4556
5018
|
say4(`${green4("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim4(adopted.storePath)}`);
|
|
@@ -5341,13 +5803,13 @@ var serviceRestart = async (config, args) => {
|
|
|
5341
5803
|
};
|
|
5342
5804
|
var serviceInit = async (args) => {
|
|
5343
5805
|
const root = gitTopLevel(process.cwd()) ?? process.cwd();
|
|
5344
|
-
const target =
|
|
5345
|
-
if (
|
|
5806
|
+
const target = path10.join(root, "mend.toml");
|
|
5807
|
+
if (fs9.existsSync(target)) {
|
|
5346
5808
|
return fail4(`${target} already exists \u2014 edit it directly (init never merges)`);
|
|
5347
5809
|
}
|
|
5348
|
-
const rootFiles =
|
|
5810
|
+
const rootFiles = fs9.readdirSync(root);
|
|
5349
5811
|
const proposals = [];
|
|
5350
|
-
const readRoot = (name) =>
|
|
5812
|
+
const readRoot = (name) => fs9.readFileSync(path10.join(root, name), "utf8");
|
|
5351
5813
|
if (rootFiles.includes("package.json")) {
|
|
5352
5814
|
proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
|
|
5353
5815
|
}
|
|
@@ -5357,13 +5819,13 @@ var serviceInit = async (args) => {
|
|
|
5357
5819
|
);
|
|
5358
5820
|
for (const glob of globs) {
|
|
5359
5821
|
const parent = glob.replace(/\/?\*+$/, "");
|
|
5360
|
-
const dirs = glob.endsWith("*") ?
|
|
5822
|
+
const dirs = glob.endsWith("*") ? fs9.existsSync(path10.join(root, parent)) ? fs9.readdirSync(path10.join(root, parent)).map((dir) => path10.join(parent, dir)) : [] : [glob];
|
|
5361
5823
|
for (const dir of dirs) {
|
|
5362
|
-
const manifest =
|
|
5363
|
-
if (!
|
|
5824
|
+
const manifest = path10.join(root, dir, "package.json");
|
|
5825
|
+
if (!fs9.existsSync(manifest)) continue;
|
|
5364
5826
|
for (const proposal of proposeFromWorkspacePackage(
|
|
5365
|
-
|
|
5366
|
-
|
|
5827
|
+
path10.basename(dir),
|
|
5828
|
+
fs9.readFileSync(manifest, "utf8"),
|
|
5367
5829
|
rootFiles
|
|
5368
5830
|
)) {
|
|
5369
5831
|
if (!proposals.some((existing) => existing.name === proposal.name)) {
|
|
@@ -5407,7 +5869,7 @@ var serviceInit = async (args) => {
|
|
|
5407
5869
|
return;
|
|
5408
5870
|
}
|
|
5409
5871
|
}
|
|
5410
|
-
|
|
5872
|
+
fs9.writeFileSync(target, toml);
|
|
5411
5873
|
say4(`${green4("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
|
|
5412
5874
|
};
|
|
5413
5875
|
var tunnelServices = async (config, services, portOverride) => {
|
|
@@ -5500,18 +5962,18 @@ var serviceCommand = async (config, args) => {
|
|
|
5500
5962
|
}
|
|
5501
5963
|
};
|
|
5502
5964
|
var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
|
|
5503
|
-
var readIfExists = (file) =>
|
|
5965
|
+
var readIfExists = (file) => fs9.existsSync(file) ? fs9.readFileSync(file, "utf8") : null;
|
|
5504
5966
|
var localCredential = (provider) => {
|
|
5505
5967
|
const home = os6.homedir();
|
|
5506
5968
|
switch (provider) {
|
|
5507
5969
|
case "codex":
|
|
5508
5970
|
return readIfExists(
|
|
5509
|
-
|
|
5971
|
+
path10.join(process.env["CODEX_HOME"] ?? path10.join(home, ".codex"), "auth.json")
|
|
5510
5972
|
);
|
|
5511
5973
|
case "claude":
|
|
5512
5974
|
return readIfExists(
|
|
5513
|
-
|
|
5514
|
-
process.env["CLAUDE_CONFIG_DIR"] ??
|
|
5975
|
+
path10.join(
|
|
5976
|
+
process.env["CLAUDE_CONFIG_DIR"] ?? path10.join(home, ".claude"),
|
|
5515
5977
|
".credentials.json"
|
|
5516
5978
|
)
|
|
5517
5979
|
);
|
|
@@ -5565,7 +6027,7 @@ var connectCommand = async (config, args) => {
|
|
|
5565
6027
|
}
|
|
5566
6028
|
let secret;
|
|
5567
6029
|
if (flags.includes("--from-stdin")) {
|
|
5568
|
-
secret =
|
|
6030
|
+
secret = fs9.readFileSync(0, "utf8").trim();
|
|
5569
6031
|
if (secret === "") return fail4("nothing on stdin");
|
|
5570
6032
|
} else {
|
|
5571
6033
|
secret = localCredential(provider);
|
|
@@ -5586,14 +6048,14 @@ var takeFlagValue3 = (args, flag) => {
|
|
|
5586
6048
|
return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
|
|
5587
6049
|
};
|
|
5588
6050
|
var saveCliConfig = (next) => {
|
|
5589
|
-
|
|
5590
|
-
|
|
6051
|
+
fs9.mkdirSync(path10.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
|
|
6052
|
+
fs9.writeFileSync(
|
|
5591
6053
|
CONFIG_PATH,
|
|
5592
6054
|
`${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
|
|
5593
6055
|
`,
|
|
5594
6056
|
{ mode: 384 }
|
|
5595
6057
|
);
|
|
5596
|
-
|
|
6058
|
+
fs9.chmodSync(CONFIG_PATH, 384);
|
|
5597
6059
|
};
|
|
5598
6060
|
var login = async (config, args) => {
|
|
5599
6061
|
await loginCommand(args, {
|
|
@@ -5606,7 +6068,7 @@ var login = async (config, args) => {
|
|
|
5606
6068
|
});
|
|
5607
6069
|
};
|
|
5608
6070
|
var logout = async (config) => {
|
|
5609
|
-
if (!
|
|
6071
|
+
if (!fs9.existsSync(CONFIG_PATH) && config.token === null) {
|
|
5610
6072
|
say4(dim4("nothing saved \u2014 already signed out"));
|
|
5611
6073
|
return;
|
|
5612
6074
|
}
|
|
@@ -5621,6 +6083,80 @@ var logout = async (config) => {
|
|
|
5621
6083
|
saveCliConfig({ ...config, token: null, deviceId: null });
|
|
5622
6084
|
say4(`${green4("\u2713")} signed out \xB7 ${dim4(`token removed from ${CONFIG_PATH}`)}`);
|
|
5623
6085
|
};
|
|
6086
|
+
var UNINSTALL_CHOICES = [
|
|
6087
|
+
{ scope: "all", text: "everything \xB7 the server on this machine and this machine's Mend files" },
|
|
6088
|
+
{ scope: "server", text: "the server only \xB7 containers, volumes, configuration, backups" },
|
|
6089
|
+
{
|
|
6090
|
+
scope: "home",
|
|
6091
|
+
text: "this machine's files only \xB7 sign-in, workspace ssh key, ~/.ssh/config block"
|
|
6092
|
+
}
|
|
6093
|
+
];
|
|
6094
|
+
var askUninstallScope = async () => {
|
|
6095
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
6096
|
+
return fail4(`${UNINSTALL_USAGE} \xB7 no terminal to ask on`);
|
|
6097
|
+
}
|
|
6098
|
+
say4("what should go?");
|
|
6099
|
+
UNINSTALL_CHOICES.forEach((choice, index) => say4(` ${index + 1}. ${choice.text}`));
|
|
6100
|
+
const readline = await import("node:readline/promises");
|
|
6101
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6102
|
+
const answer = (await rl.question(" 1, 2 or 3: ")).trim();
|
|
6103
|
+
rl.close();
|
|
6104
|
+
const chosen = UNINSTALL_CHOICES[Number(answer) - 1];
|
|
6105
|
+
if (chosen === void 0) return fail4(`"${answer}" is not one of the choices`);
|
|
6106
|
+
return chosen.scope;
|
|
6107
|
+
};
|
|
6108
|
+
var confirmUninstall = async (word) => {
|
|
6109
|
+
if (process.stdin.isTTY !== true) return fail4("non-interactive \u2014 pass --yes to remove");
|
|
6110
|
+
const readline = await import("node:readline/promises");
|
|
6111
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6112
|
+
const answer = (await rl.question(word === "y" ? "remove? [y/N] " : `type ${word} to remove: `)).trim().toLowerCase();
|
|
6113
|
+
rl.close();
|
|
6114
|
+
return answer === word;
|
|
6115
|
+
};
|
|
6116
|
+
var uninstallCommand = async (config, args) => {
|
|
6117
|
+
const parsed = parseUninstallArgs(args);
|
|
6118
|
+
if ("error" in parsed) return fail4(parsed.error);
|
|
6119
|
+
const scope = parsed.scope ?? await askUninstallScope();
|
|
6120
|
+
const server = nodeServerRuntime();
|
|
6121
|
+
const runtime = {
|
|
6122
|
+
server,
|
|
6123
|
+
cliHome: mendCliHome(),
|
|
6124
|
+
sshConfigFile: path10.join(os6.homedir(), ".ssh", "config"),
|
|
6125
|
+
signedIn: config.token === null || !fs9.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
|
|
6126
|
+
revokeDevice: async () => {
|
|
6127
|
+
if (config.deviceId === null) return "no device id saved";
|
|
6128
|
+
try {
|
|
6129
|
+
await request(config, "DELETE", `/me/devices/${config.deviceId}`);
|
|
6130
|
+
return null;
|
|
6131
|
+
} catch (error) {
|
|
6132
|
+
return error instanceof Error ? error.message : String(error);
|
|
6133
|
+
}
|
|
6134
|
+
}
|
|
6135
|
+
};
|
|
6136
|
+
const plan = await describeUninstall(runtime, scope);
|
|
6137
|
+
say4(dim4(`mend uninstall \xB7 ${scope === "all" ? "everything" : scope}`));
|
|
6138
|
+
for (const line of planLines(plan, server.configDir)) say4(` ${line}`);
|
|
6139
|
+
const nothing = (plan.server === null || plan.server === "none") && (plan.home === null || plan.home.cliConfig === null && plan.home.sshDirectory === null && plan.home.managedSshBlocks === 0);
|
|
6140
|
+
if (nothing) {
|
|
6141
|
+
say4(dim4("nothing to remove"));
|
|
6142
|
+
return;
|
|
6143
|
+
}
|
|
6144
|
+
if (planDeletesData(plan)) {
|
|
6145
|
+
say4("repositories, worktrees, the database and its backups are deleted with the server");
|
|
6146
|
+
}
|
|
6147
|
+
if (!parsed.yes && !await confirmUninstall(planDeletesData(plan) ? "delete" : "y")) {
|
|
6148
|
+
say4(dim4("nothing removed"));
|
|
6149
|
+
return;
|
|
6150
|
+
}
|
|
6151
|
+
const outcome = await executeUninstall(runtime, plan);
|
|
6152
|
+
for (const line of outcome.leftovers) say4(dim4(` kept \xB7 ${line}`));
|
|
6153
|
+
if (outcome.failures.length > 0) {
|
|
6154
|
+
return fail4(outcome.failures.join("\n"));
|
|
6155
|
+
}
|
|
6156
|
+
say4(
|
|
6157
|
+
`${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"}`
|
|
6158
|
+
);
|
|
6159
|
+
};
|
|
5624
6160
|
var printGitKey = (key) => {
|
|
5625
6161
|
if (key.publicKey === null) return;
|
|
5626
6162
|
say4(key.publicKey);
|
|
@@ -5795,10 +6331,10 @@ var envLoad = async (config, args) => {
|
|
|
5795
6331
|
const positional = args.filter(
|
|
5796
6332
|
(arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
|
|
5797
6333
|
);
|
|
5798
|
-
const file =
|
|
6334
|
+
const file = path10.resolve(positional[0] ?? ".env");
|
|
5799
6335
|
let contents;
|
|
5800
6336
|
try {
|
|
5801
|
-
contents =
|
|
6337
|
+
contents = fs9.readFileSync(file, "utf8");
|
|
5802
6338
|
} catch {
|
|
5803
6339
|
return fail4(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
|
|
5804
6340
|
}
|
|
@@ -5814,10 +6350,10 @@ var envLoad = async (config, args) => {
|
|
|
5814
6350
|
{ contents, allSecret, secretNames }
|
|
5815
6351
|
);
|
|
5816
6352
|
if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
|
|
5817
|
-
say4(dim4(`${
|
|
6353
|
+
say4(dim4(`${path10.basename(file)} has no variables`));
|
|
5818
6354
|
return;
|
|
5819
6355
|
}
|
|
5820
|
-
say4(`${green4("\u2713")} loaded ${
|
|
6356
|
+
say4(`${green4("\u2713")} loaded ${path10.basename(file)} into ${project2.name}`);
|
|
5821
6357
|
for (const line of formatLoadReport(report, { dim: dim4, warn: amber })) say4(line);
|
|
5822
6358
|
const plaintextUrls = report.loaded.filter(
|
|
5823
6359
|
(entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
|
|
@@ -6006,7 +6542,7 @@ var skillsList = async (config, args) => {
|
|
|
6006
6542
|
};
|
|
6007
6543
|
var skillsPush = async (config, args) => {
|
|
6008
6544
|
const dirFlag = takeFlagValue3(args, "--dir");
|
|
6009
|
-
const root = dirFlag === null ?
|
|
6545
|
+
const root = dirFlag === null ? path10.join(os6.homedir(), DEFAULT_SKILLS_DIR) : path10.resolve(dirFlag);
|
|
6010
6546
|
const scanned = scanSkillLibrary(root);
|
|
6011
6547
|
if ("error" in scanned) return fail4(scanned.error);
|
|
6012
6548
|
for (const note of scanned.notes) say4(dim4(` ${note.skill}: ${note.message}`));
|
|
@@ -6076,6 +6612,7 @@ _mend() {
|
|
|
6076
6612
|
'shell:open a shell in a live session workspace'
|
|
6077
6613
|
'service:reachable ports \u2014 add, list, stop'
|
|
6078
6614
|
'server:local server setup, lifecycle and upgrades'
|
|
6615
|
+
'uninstall:remove the server, local Mend files, or both'
|
|
6079
6616
|
'keys:the machine Mend deploy key \u2014 init, show, share'
|
|
6080
6617
|
'skills:skill libraries \u2014 list, push'
|
|
6081
6618
|
'accounts:your connected accounts on the platform'
|
|
@@ -6115,7 +6652,7 @@ _mend "$@"
|
|
|
6115
6652
|
var BASH_COMPLETIONS = `_mend() {
|
|
6116
6653
|
local cur=\${COMP_WORDS[COMP_CWORD]}
|
|
6117
6654
|
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
6118
|
-
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") )
|
|
6655
|
+
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") )
|
|
6119
6656
|
return
|
|
6120
6657
|
fi
|
|
6121
6658
|
case \${COMP_WORDS[1]} in
|
|
@@ -6650,7 +7187,7 @@ var dashboard = async (config) => {
|
|
|
6650
7187
|
);
|
|
6651
7188
|
process.exit(rerun.status ?? 0);
|
|
6652
7189
|
}
|
|
6653
|
-
const { runDashboard } = await import("./dashboard-
|
|
7190
|
+
const { runDashboard } = await import("./dashboard-5CBGTPSJ.js");
|
|
6654
7191
|
const agentShare = await startShareIfBridge(config);
|
|
6655
7192
|
try {
|
|
6656
7193
|
await runDashboard({
|
|
@@ -6720,10 +7257,10 @@ var manCommand = (words) => {
|
|
|
6720
7257
|
}
|
|
6721
7258
|
const version = cliVersion();
|
|
6722
7259
|
const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
|
|
6723
|
-
const file =
|
|
6724
|
-
|
|
7260
|
+
const file = path10.join(os6.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
|
|
7261
|
+
fs9.writeFileSync(file, page);
|
|
6725
7262
|
const result = spawnSync2("man", ["-l", file], { stdio: "inherit" });
|
|
6726
|
-
|
|
7263
|
+
fs9.rmSync(file, { force: true });
|
|
6727
7264
|
if (result.error !== void 0 || result.status !== 0) {
|
|
6728
7265
|
say4(doc === null ? renderIndex() : renderCommand(doc));
|
|
6729
7266
|
}
|
|
@@ -6760,6 +7297,8 @@ var main = async () => {
|
|
|
6760
7297
|
return login(config, rest);
|
|
6761
7298
|
case "logout":
|
|
6762
7299
|
return logout(config);
|
|
7300
|
+
case "uninstall":
|
|
7301
|
+
return uninstallCommand(config, rest);
|
|
6763
7302
|
case "keys":
|
|
6764
7303
|
return keysCommand(config, rest);
|
|
6765
7304
|
case "dotfiles":
|