@sealant/mend 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/main.js +453 -39
  2. package/man/mend-accounts.1 +1 -1
  3. package/man/mend-adopt.1 +1 -1
  4. package/man/mend-attach.1 +1 -1
  5. package/man/mend-codex.1 +1 -1
  6. package/man/mend-completions.1 +1 -1
  7. package/man/mend-connect.1 +1 -1
  8. package/man/mend-continue.1 +1 -1
  9. package/man/mend-doctor.1 +1 -1
  10. package/man/mend-dotfiles-sync.1 +1 -1
  11. package/man/mend-dotfiles.1 +1 -1
  12. package/man/mend-env-cluster.1 +1 -1
  13. package/man/mend-env-load.1 +1 -1
  14. package/man/mend-env-show.1 +1 -1
  15. package/man/mend-help.1 +1 -1
  16. package/man/mend-keys-init.1 +1 -1
  17. package/man/mend-keys-mode.1 +1 -1
  18. package/man/mend-keys-share.1 +1 -1
  19. package/man/mend-keys-show.1 +1 -1
  20. package/man/mend-login.1 +1 -1
  21. package/man/mend-logout.1 +1 -1
  22. package/man/mend-man.1 +1 -1
  23. package/man/mend-pair.1 +1 -1
  24. package/man/mend-projects.1 +1 -1
  25. package/man/mend-refresh.1 +1 -1
  26. package/man/mend-rejoin.1 +1 -1
  27. package/man/mend-resume.1 +1 -1
  28. package/man/mend-run.1 +1 -1
  29. package/man/mend-server-logs.1 +1 -1
  30. package/man/mend-server-restart.1 +1 -1
  31. package/man/mend-server-setup.1 +1 -1
  32. package/man/mend-server-start.1 +1 -1
  33. package/man/mend-server-status.1 +1 -1
  34. package/man/mend-server-stop.1 +1 -1
  35. package/man/mend-server-upgrade.1 +1 -1
  36. package/man/mend-server.1 +1 -1
  37. package/man/mend-service-add.1 +1 -1
  38. package/man/mend-service-connect.1 +1 -1
  39. package/man/mend-service-init.1 +1 -1
  40. package/man/mend-service-list.1 +1 -1
  41. package/man/mend-service-logs.1 +1 -1
  42. package/man/mend-service-restart.1 +1 -1
  43. package/man/mend-service-run.1 +1 -1
  44. package/man/mend-service-stop.1 +1 -1
  45. package/man/mend-sessions.1 +1 -1
  46. package/man/mend-shell.1 +1 -1
  47. package/man/mend-skills-push.1 +1 -1
  48. package/man/mend-skills.1 +1 -1
  49. package/man/mend-ssh-setup.1 +1 -1
  50. package/man/mend-ssh.1 +1 -1
  51. package/man/mend-stop.1 +1 -1
  52. package/man/mend-ui.1 +1 -1
  53. package/man/mend-uninstall.1 +35 -0
  54. package/man/mend-version.1 +1 -1
  55. package/man/mend-worktrees.1 +1 -1
  56. package/man/mend.1 +5 -2
  57. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -21,11 +21,11 @@ import {
21
21
 
22
22
  // src/main.ts
23
23
  import { spawn as spawn3, spawnSync as spawnSync2 } from "node:child_process";
24
- import * as fs8 from "node:fs";
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 path9 from "node:path";
28
+ import * as path10 from "node:path";
29
29
 
30
30
  // src/agent-share.ts
31
31
  import * as net from "node:net";
@@ -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",
@@ -4009,6 +4033,26 @@ var removeManagedBlock = (config, beginMarker, endMarker) => {
4009
4033
  }
4010
4034
  return success(`${config.slice(0, begin)}${after}`);
4011
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
+ };
4012
4056
  var containsExactHost = (config, alias) => config.split("\n").some((line) => {
4013
4057
  const match = /^\s*Host\s+(.+?)\s*$/i.exec(line);
4014
4058
  return match?.[1]?.split(/\s+/).includes(alias) === true;
@@ -4353,22 +4397,315 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
4353
4397
  }
4354
4398
  };
4355
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
+
4356
4693
  // src/main.ts
4357
4694
  var mendCliHome = () => {
4358
4695
  const xdg = process.env["XDG_CONFIG_HOME"];
4359
- const preferred = path9.join(
4360
- xdg === void 0 || xdg === "" ? path9.join(os6.homedir(), ".config") : xdg,
4696
+ const preferred = path10.join(
4697
+ xdg === void 0 || xdg === "" ? path10.join(os6.homedir(), ".config") : xdg,
4361
4698
  "mend"
4362
4699
  );
4363
- const legacy = path9.join(os6.homedir(), ".mend");
4364
- return !fs8.existsSync(preferred) && fs8.existsSync(legacy) ? legacy : preferred;
4700
+ const legacy = path10.join(os6.homedir(), ".mend");
4701
+ return !fs9.existsSync(preferred) && fs9.existsSync(legacy) ? legacy : preferred;
4365
4702
  };
4366
- var CONFIG_PATH = path9.join(mendCliHome(), "cli.json");
4703
+ var CONFIG_PATH = path10.join(mendCliHome(), "cli.json");
4367
4704
  var loadConfig = () => {
4368
4705
  let fileConfig = {};
4369
- if (fs8.existsSync(CONFIG_PATH)) {
4706
+ if (fs9.existsSync(CONFIG_PATH)) {
4370
4707
  try {
4371
- fileConfig = JSON.parse(fs8.readFileSync(CONFIG_PATH, "utf8"));
4708
+ fileConfig = JSON.parse(fs9.readFileSync(CONFIG_PATH, "utf8"));
4372
4709
  } catch {
4373
4710
  fail4(`could not parse ${CONFIG_PATH}`);
4374
4711
  }
@@ -4505,7 +4842,7 @@ var adopt = async (config, args) => {
4505
4842
  }
4506
4843
  const sourceIssue = repositoryCloneUrlIssue(source);
4507
4844
  if (sourceIssue !== null) return fail4(sourceIssue);
4508
- const projectName = name ?? normalizeProjectName(path9.basename(source, ".git"));
4845
+ const projectName = name ?? normalizeProjectName(path10.basename(source, ".git"));
4509
4846
  const auth = authFlagIndex !== -1 && args[authFlagIndex + 1] !== void 0 ? String(args[authFlagIndex + 1]) : null;
4510
4847
  if (auth !== null && auth !== "ambient" && auth !== "mend-key" && auth !== "bridge") {
4511
4848
  return fail4(`--auth takes "ambient", "mend-key", or "bridge", not "${auth}"`);
@@ -4550,7 +4887,7 @@ var findProject = async (config, explicit, adoptCwd = false) => {
4550
4887
  const sourceIssue = repositoryCloneUrlIssue(facts.originUrl);
4551
4888
  if (sourceIssue !== null) return fail4(sourceIssue);
4552
4889
  const adopted = await api(config, "POST", "/projects", {
4553
- name: normalizeProjectName(path9.basename(facts.repoRoot)),
4890
+ name: normalizeProjectName(path10.basename(facts.repoRoot)),
4554
4891
  source: facts.originUrl
4555
4892
  });
4556
4893
  say4(`${green4("\u2713")} adopted \xB7 ${adopted.name} \xB7 ${dim4(adopted.storePath)}`);
@@ -5341,13 +5678,13 @@ var serviceRestart = async (config, args) => {
5341
5678
  };
5342
5679
  var serviceInit = async (args) => {
5343
5680
  const root = gitTopLevel(process.cwd()) ?? process.cwd();
5344
- const target = path9.join(root, "mend.toml");
5345
- if (fs8.existsSync(target)) {
5681
+ const target = path10.join(root, "mend.toml");
5682
+ if (fs9.existsSync(target)) {
5346
5683
  return fail4(`${target} already exists \u2014 edit it directly (init never merges)`);
5347
5684
  }
5348
- const rootFiles = fs8.readdirSync(root);
5685
+ const rootFiles = fs9.readdirSync(root);
5349
5686
  const proposals = [];
5350
- const readRoot = (name) => fs8.readFileSync(path9.join(root, name), "utf8");
5687
+ const readRoot = (name) => fs9.readFileSync(path10.join(root, name), "utf8");
5351
5688
  if (rootFiles.includes("package.json")) {
5352
5689
  proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
5353
5690
  }
@@ -5357,13 +5694,13 @@ var serviceInit = async (args) => {
5357
5694
  );
5358
5695
  for (const glob of globs) {
5359
5696
  const parent = glob.replace(/\/?\*+$/, "");
5360
- const dirs = glob.endsWith("*") ? fs8.existsSync(path9.join(root, parent)) ? fs8.readdirSync(path9.join(root, parent)).map((dir) => path9.join(parent, dir)) : [] : [glob];
5697
+ const dirs = glob.endsWith("*") ? fs9.existsSync(path10.join(root, parent)) ? fs9.readdirSync(path10.join(root, parent)).map((dir) => path10.join(parent, dir)) : [] : [glob];
5361
5698
  for (const dir of dirs) {
5362
- const manifest = path9.join(root, dir, "package.json");
5363
- if (!fs8.existsSync(manifest)) continue;
5699
+ const manifest = path10.join(root, dir, "package.json");
5700
+ if (!fs9.existsSync(manifest)) continue;
5364
5701
  for (const proposal of proposeFromWorkspacePackage(
5365
- path9.basename(dir),
5366
- fs8.readFileSync(manifest, "utf8"),
5702
+ path10.basename(dir),
5703
+ fs9.readFileSync(manifest, "utf8"),
5367
5704
  rootFiles
5368
5705
  )) {
5369
5706
  if (!proposals.some((existing) => existing.name === proposal.name)) {
@@ -5407,7 +5744,7 @@ var serviceInit = async (args) => {
5407
5744
  return;
5408
5745
  }
5409
5746
  }
5410
- fs8.writeFileSync(target, toml);
5747
+ fs9.writeFileSync(target, toml);
5411
5748
  say4(`${green4("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
5412
5749
  };
5413
5750
  var tunnelServices = async (config, services, portOverride) => {
@@ -5500,18 +5837,18 @@ var serviceCommand = async (config, args) => {
5500
5837
  }
5501
5838
  };
5502
5839
  var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
5503
- var readIfExists = (file) => fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : null;
5840
+ var readIfExists = (file) => fs9.existsSync(file) ? fs9.readFileSync(file, "utf8") : null;
5504
5841
  var localCredential = (provider) => {
5505
5842
  const home = os6.homedir();
5506
5843
  switch (provider) {
5507
5844
  case "codex":
5508
5845
  return readIfExists(
5509
- path9.join(process.env["CODEX_HOME"] ?? path9.join(home, ".codex"), "auth.json")
5846
+ path10.join(process.env["CODEX_HOME"] ?? path10.join(home, ".codex"), "auth.json")
5510
5847
  );
5511
5848
  case "claude":
5512
5849
  return readIfExists(
5513
- path9.join(
5514
- process.env["CLAUDE_CONFIG_DIR"] ?? path9.join(home, ".claude"),
5850
+ path10.join(
5851
+ process.env["CLAUDE_CONFIG_DIR"] ?? path10.join(home, ".claude"),
5515
5852
  ".credentials.json"
5516
5853
  )
5517
5854
  );
@@ -5565,7 +5902,7 @@ var connectCommand = async (config, args) => {
5565
5902
  }
5566
5903
  let secret;
5567
5904
  if (flags.includes("--from-stdin")) {
5568
- secret = fs8.readFileSync(0, "utf8").trim();
5905
+ secret = fs9.readFileSync(0, "utf8").trim();
5569
5906
  if (secret === "") return fail4("nothing on stdin");
5570
5907
  } else {
5571
5908
  secret = localCredential(provider);
@@ -5586,14 +5923,14 @@ var takeFlagValue3 = (args, flag) => {
5586
5923
  return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
5587
5924
  };
5588
5925
  var saveCliConfig = (next) => {
5589
- fs8.mkdirSync(path9.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
5590
- fs8.writeFileSync(
5926
+ fs9.mkdirSync(path10.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
5927
+ fs9.writeFileSync(
5591
5928
  CONFIG_PATH,
5592
5929
  `${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
5593
5930
  `,
5594
5931
  { mode: 384 }
5595
5932
  );
5596
- fs8.chmodSync(CONFIG_PATH, 384);
5933
+ fs9.chmodSync(CONFIG_PATH, 384);
5597
5934
  };
5598
5935
  var login = async (config, args) => {
5599
5936
  await loginCommand(args, {
@@ -5606,7 +5943,7 @@ var login = async (config, args) => {
5606
5943
  });
5607
5944
  };
5608
5945
  var logout = async (config) => {
5609
- if (!fs8.existsSync(CONFIG_PATH) && config.token === null) {
5946
+ if (!fs9.existsSync(CONFIG_PATH) && config.token === null) {
5610
5947
  say4(dim4("nothing saved \u2014 already signed out"));
5611
5948
  return;
5612
5949
  }
@@ -5621,6 +5958,80 @@ var logout = async (config) => {
5621
5958
  saveCliConfig({ ...config, token: null, deviceId: null });
5622
5959
  say4(`${green4("\u2713")} signed out \xB7 ${dim4(`token removed from ${CONFIG_PATH}`)}`);
5623
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
+ };
5624
6035
  var printGitKey = (key) => {
5625
6036
  if (key.publicKey === null) return;
5626
6037
  say4(key.publicKey);
@@ -5795,10 +6206,10 @@ var envLoad = async (config, args) => {
5795
6206
  const positional = args.filter(
5796
6207
  (arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project" && !consumed.has(i)
5797
6208
  );
5798
- const file = path9.resolve(positional[0] ?? ".env");
6209
+ const file = path10.resolve(positional[0] ?? ".env");
5799
6210
  let contents;
5800
6211
  try {
5801
- contents = fs8.readFileSync(file, "utf8");
6212
+ contents = fs9.readFileSync(file, "utf8");
5802
6213
  } catch {
5803
6214
  return fail4(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
5804
6215
  }
@@ -5814,10 +6225,10 @@ var envLoad = async (config, args) => {
5814
6225
  { contents, allSecret, secretNames }
5815
6226
  );
5816
6227
  if (report.loaded.length === 0 && report.rejected.length === 0 && report.malformedLines.length === 0) {
5817
- say4(dim4(`${path9.basename(file)} has no variables`));
6228
+ say4(dim4(`${path10.basename(file)} has no variables`));
5818
6229
  return;
5819
6230
  }
5820
- say4(`${green4("\u2713")} loaded ${path9.basename(file)} into ${project2.name}`);
6231
+ say4(`${green4("\u2713")} loaded ${path10.basename(file)} into ${project2.name}`);
5821
6232
  for (const line of formatLoadReport(report, { dim: dim4, warn: amber })) say4(line);
5822
6233
  const plaintextUrls = report.loaded.filter(
5823
6234
  (entry) => entry.lane === "configuration" && /_(URL|URI|DSN)$/.test(entry.name)
@@ -6006,7 +6417,7 @@ var skillsList = async (config, args) => {
6006
6417
  };
6007
6418
  var skillsPush = async (config, args) => {
6008
6419
  const dirFlag = takeFlagValue3(args, "--dir");
6009
- const root = dirFlag === null ? path9.join(os6.homedir(), DEFAULT_SKILLS_DIR) : path9.resolve(dirFlag);
6420
+ const root = dirFlag === null ? path10.join(os6.homedir(), DEFAULT_SKILLS_DIR) : path10.resolve(dirFlag);
6010
6421
  const scanned = scanSkillLibrary(root);
6011
6422
  if ("error" in scanned) return fail4(scanned.error);
6012
6423
  for (const note of scanned.notes) say4(dim4(` ${note.skill}: ${note.message}`));
@@ -6076,6 +6487,7 @@ _mend() {
6076
6487
  'shell:open a shell in a live session workspace'
6077
6488
  'service:reachable ports \u2014 add, list, stop'
6078
6489
  'server:local server setup, lifecycle and upgrades'
6490
+ 'uninstall:remove the server, local Mend files, or both'
6079
6491
  'keys:the machine Mend deploy key \u2014 init, show, share'
6080
6492
  'skills:skill libraries \u2014 list, push'
6081
6493
  'accounts:your connected accounts on the platform'
@@ -6115,7 +6527,7 @@ _mend "$@"
6115
6527
  var BASH_COMPLETIONS = `_mend() {
6116
6528
  local cur=\${COMP_WORDS[COMP_CWORD]}
6117
6529
  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") )
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") )
6119
6531
  return
6120
6532
  fi
6121
6533
  case \${COMP_WORDS[1]} in
@@ -6720,10 +7132,10 @@ var manCommand = (words) => {
6720
7132
  }
6721
7133
  const version = cliVersion();
6722
7134
  const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
6723
- const file = path9.join(os6.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
6724
- fs8.writeFileSync(file, page);
7135
+ const file = path10.join(os6.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
7136
+ fs9.writeFileSync(file, page);
6725
7137
  const result = spawnSync2("man", ["-l", file], { stdio: "inherit" });
6726
- fs8.rmSync(file, { force: true });
7138
+ fs9.rmSync(file, { force: true });
6727
7139
  if (result.error !== void 0 || result.status !== 0) {
6728
7140
  say4(doc === null ? renderIndex() : renderCommand(doc));
6729
7141
  }
@@ -6760,6 +7172,8 @@ var main = async () => {
6760
7172
  return login(config, rest);
6761
7173
  case "logout":
6762
7174
  return logout(config);
7175
+ case "uninstall":
7176
+ return uninstallCommand(config, rest);
6763
7177
  case "keys":
6764
7178
  return keysCommand(config, rest);
6765
7179
  case "dotfiles":
@@ -1,4 +1,4 @@
1
- .TH MEND-ACCOUNTS 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-ACCOUNTS 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-accounts \- your connected accounts on the platform
4
4
  .SH SYNOPSIS
package/man/mend-adopt.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-ADOPT 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-ADOPT 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-adopt \- adopt a repository into the store
4
4
  .SH SYNOPSIS
package/man/mend-attach.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-ATTACH 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-ATTACH 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-attach \- reattach this terminal to a running session
4
4
  .SH SYNOPSIS
package/man/mend-codex.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-CODEX 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-CODEX 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-codex \- launch codex, claude, or opencode in a recorded worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-COMPLETIONS 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-COMPLETIONS 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-completions \- print the TAB\-completion hook
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-CONNECT 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-CONNECT 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-connect \- send this machine's provider credential to the platform
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-CONTINUE 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-CONTINUE 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-continue \- resume a session with its pending review follow\-up
4
4
  .SH SYNOPSIS
package/man/mend-doctor.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-DOCTOR 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-DOCTOR 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-doctor \- check this machine's setup
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-DOTFILES-SYNC 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-DOTFILES-SYNC 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-dotfiles\-sync \- capture config files from this machine into your store
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-DOTFILES 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-DOTFILES 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-dotfiles \- your dotfiles on the server: repo and synced files
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-ENV-CLUSTER 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-ENV-CLUSTER 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-env\-cluster \- bind Kubernetes secrets and configmaps to the workspaces
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-ENV-LOAD 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-ENV-LOAD 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-env\-load \- load a .env into the project
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-ENV-SHOW 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-ENV-SHOW 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-env\-show \- what the project store holds: names only, never values
4
4
  .SH SYNOPSIS
package/man/mend-help.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-HELP 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-HELP 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-help \- this index, or one command's page
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-KEYS-INIT 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-KEYS-INIT 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-keys\-init \- create your Mend key (ed25519) on the server
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-KEYS-MODE 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-KEYS-MODE 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-keys\-mode \- how your remotes are reached: mend\-key or bridge
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-KEYS-SHARE 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-KEYS-SHARE 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-keys\-share \- relay this machine's ssh\-agent to the server
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-KEYS-SHOW 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-KEYS-SHOW 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-keys\-show \- print your Mend public key
4
4
  .SH SYNOPSIS
package/man/mend-login.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-LOGIN 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-LOGIN 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-login \- sign this terminal in through the browser
4
4
  .SH SYNOPSIS
package/man/mend-logout.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-LOGOUT 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-LOGOUT 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-logout \- revoke this terminal's device token and forget it
4
4
  .SH SYNOPSIS
package/man/mend-man.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-MAN 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-MAN 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-man \- one command's page, or the whole manual, in man
4
4
  .SH SYNOPSIS
package/man/mend-pair.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-PAIR 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-PAIR 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-pair \- pair a phone or a second machine
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-PROJECTS 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-PROJECTS 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-projects \- adopted projects and their live sessions
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-REFRESH 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-REFRESH 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-refresh \- fetch origin's branches into the store
4
4
  .SH SYNOPSIS
package/man/mend-rejoin.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-REJOIN 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-REJOIN 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-rejoin \- attach if live, otherwise resume
4
4
  .SH SYNOPSIS
package/man/mend-resume.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-RESUME 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-RESUME 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-resume \- rejoin a settled session with its state restored
4
4
  .SH SYNOPSIS
package/man/mend-run.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-RUN 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-RUN 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-run \- run any command as a session
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVER-LOGS 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVER-LOGS 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-server\-logs \- print a bounded tail of the installation's container logs
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVER-RESTART 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVER-RESTART 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-server\-restart \- restart Mend using the same generation and pin
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVER-SETUP 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVER-SETUP 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-server\-setup \- install or repair the local Mend server
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVER-START 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVER-START 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-server\-start \- start the selected server generation
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVER-STATUS 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVER-STATUS 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-server\-status \- show the saved pin, generation and container state
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVER-STOP 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVER-STOP 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-server\-stop \- stop Mend and Postgres without deleting data
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVER-UPGRADE 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVER-UPGRADE 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-server\-upgrade \- upgrade to an explicit version with a database backup
4
4
  .SH SYNOPSIS
package/man/mend-server.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVER 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVER 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-server \- install and manage this machine's Mend server
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVICE-ADD 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVICE-ADD 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-service\-add \- adopt a port something in the workspace already listens on
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVICE-CONNECT 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVICE-CONNECT 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-service\-connect \- bring live Services to this machine's loopback
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVICE-INIT 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVICE-INIT 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-service\-init \- scaffold mend.toml from package.json and compose ports
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVICE-LIST 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVICE-LIST 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-service\-list \- every live service and its observed state
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVICE-LOGS 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVICE-LOGS 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-service\-logs \- follow a supervised service's output
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVICE-RESTART 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVICE-RESTART 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-service\-restart \- re\-run a service's recorded command, same URL
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVICE-RUN 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVICE-RUN 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-service\-run \- start and supervise a server in the session's workspace
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SERVICE-STOP 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SERVICE-STOP 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-service\-stop \- stop a service and close its host port
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SESSIONS 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SESSIONS 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-sessions \- sessions with their review facts
4
4
  .SH SYNOPSIS
package/man/mend-shell.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-SHELL 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SHELL 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-shell \- open a shell in a live session's workspace
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SKILLS-PUSH 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SKILLS-PUSH 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-skills\-push \- upload ~/.agents/skills into the library
4
4
  .SH SYNOPSIS
package/man/mend-skills.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-SKILLS 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SKILLS 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-skills \- your skill library on the server, or a project's
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-SSH-SETUP 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SSH-SETUP 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-ssh\-setup \- make this machine ready to ssh into workspaces, once
4
4
  .SH SYNOPSIS
package/man/mend-ssh.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-SSH 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-SSH 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-ssh \- workspace SSH status: gateway, registered keys, ssh config
4
4
  .SH SYNOPSIS
package/man/mend-stop.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-STOP 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-STOP 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-stop \- stop the agent; the record and the review remain
4
4
  .SH SYNOPSIS
package/man/mend-ui.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-UI 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-UI 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-ui \- the dashboard: every project and session, live
4
4
  .SH SYNOPSIS
@@ -0,0 +1,35 @@
1
+ .TH MEND-UNINSTALL 1 "" "mend 0.26.0" "mend manual"
2
+ .SH NAME
3
+ mend\-uninstall \- remove the server, this machine's Mend files, or both
4
+ .SH SYNOPSIS
5
+ .B mend uninstall [\-\-all | \-\-server | \-\-home] [\-\-yes]
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ 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.
10
+ .PP
11
+ 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.
12
+ .SH OPTIONS
13
+ .TP
14
+ .B \-\-all
15
+ the server and this machine's Mend files
16
+ .TP
17
+ .B \-\-server
18
+ only the local server installation
19
+ .TP
20
+ .B \-\-home
21
+ only this machine's sign\-in, ssh key and config block
22
+ .TP
23
+ .B \-\-yes
24
+ skip the confirmation; required without a terminal
25
+ .SH EXAMPLES
26
+ .PP
27
+ .B mend uninstall
28
+ .br
29
+ choose a scope, then confirm
30
+ .PP
31
+ .B mend uninstall \-\-home \-\-yes
32
+ .br
33
+ forget this machine's sign\-in without asking
34
+ .SH SEE ALSO
35
+ mend(1), mend\-server\-stop(1), mend\-logout(1)
@@ -1,4 +1,4 @@
1
- .TH MEND-VERSION 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-VERSION 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-version \- this CLI's version, and the server's when it answers
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-WORKTREES 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND-WORKTREES 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend\-worktrees \- every worktree and the sessions inside it
4
4
  .SH SYNOPSIS
package/man/mend.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND 1 "" "mend 0.25.0" "mend manual"
1
+ .TH MEND 1 "" "mend 0.26.0" "mend manual"
2
2
  .SH NAME
3
3
  mend \- the agent workbench
4
4
  .SH SYNOPSIS
@@ -160,6 +160,9 @@ your connected accounts on the platform
160
160
  .B mend logout
161
161
  revoke this terminal's device token and forget it
162
162
  .TP
163
+ .B mend uninstall
164
+ remove the server, this machine's Mend files, or both
165
+ .TP
163
166
  .B mend completions
164
167
  print the TAB\-completion hook
165
168
  .TP
@@ -189,4 +192,4 @@ url, token, and device id from mend login
189
192
  .B ~/.config/mend/keys/
190
193
  the deploy key from mend keys init
191
194
  .SH SEE ALSO
192
- mend\-login(1), mend\-connect(1), mend\-adopt(1), mend\-codex(1), mend\-pair(1), mend\-doctor(1), mend\-ui(1), mend\-attach(1), mend\-stop(1), mend\-shell(1), mend\-run(1), mend\-continue(1), mend\-resume(1), mend\-rejoin(1), mend\-sessions(1), mend\-worktrees(1), mend\-projects(1), mend\-refresh(1), mend\-service\-run(1), mend\-service\-add(1), mend\-service\-connect(1), mend\-service\-list(1), mend\-service\-logs(1), mend\-service\-restart(1), mend\-service\-stop(1), mend\-service\-init(1), mend\-env\-show(1), mend\-env\-load(1), mend\-env\-cluster(1), mend\-skills(1), mend\-skills\-push(1), mend\-dotfiles(1), mend\-dotfiles\-sync(1), mend\-keys\-init(1), mend\-keys\-show(1), mend\-keys\-mode(1), mend\-keys\-share(1), mend\-server(1), mend\-server\-setup(1), mend\-server\-status(1), mend\-server\-start(1), mend\-server\-stop(1), mend\-server\-restart(1), mend\-server\-logs(1), mend\-server\-upgrade(1), mend\-ssh(1), mend\-ssh\-setup(1), mend\-accounts(1), mend\-logout(1), mend\-completions(1), mend\-version(1), mend\-help(1), mend\-man(1)
195
+ mend\-login(1), mend\-connect(1), mend\-adopt(1), mend\-codex(1), mend\-pair(1), mend\-doctor(1), mend\-ui(1), mend\-attach(1), mend\-stop(1), mend\-shell(1), mend\-run(1), mend\-continue(1), mend\-resume(1), mend\-rejoin(1), mend\-sessions(1), mend\-worktrees(1), mend\-projects(1), mend\-refresh(1), mend\-service\-run(1), mend\-service\-add(1), mend\-service\-connect(1), mend\-service\-list(1), mend\-service\-logs(1), mend\-service\-restart(1), mend\-service\-stop(1), mend\-service\-init(1), mend\-env\-show(1), mend\-env\-load(1), mend\-env\-cluster(1), mend\-skills(1), mend\-skills\-push(1), mend\-dotfiles(1), mend\-dotfiles\-sync(1), mend\-keys\-init(1), mend\-keys\-show(1), mend\-keys\-mode(1), mend\-keys\-share(1), mend\-server(1), mend\-server\-setup(1), mend\-server\-status(1), mend\-server\-start(1), mend\-server\-stop(1), mend\-server\-restart(1), mend\-server\-logs(1), mend\-server\-upgrade(1), mend\-ssh(1), mend\-ssh\-setup(1), mend\-accounts(1), mend\-logout(1), mend\-uninstall(1), mend\-completions(1), mend\-version(1), mend\-help(1), mend\-man(1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/mend",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "The mend CLI: adopt a repository into Mend's store, launch your coding agent in a recorded session worktree, detach and reattach from any terminal",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -48,8 +48,8 @@
48
48
  "typescript": "^5.7.2",
49
49
  "vitest": "^3.0.5",
50
50
  "@mend/domain": "0.0.0",
51
- "@mend/workspace-ssh": "0.0.0",
52
- "@mend/typescript": "0.0.0"
51
+ "@mend/typescript": "0.0.0",
52
+ "@mend/workspace-ssh": "0.0.0"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=22"