@sealant/mend 0.17.0 → 0.19.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 (51) hide show
  1. package/dist/dashboard-model.js +9 -1
  2. package/dist/dashboard.js +45 -1
  3. package/dist/help.js +824 -0
  4. package/dist/main.js +82 -116
  5. package/dist/version.js +48 -0
  6. package/man/mend-accounts.1 +11 -0
  7. package/man/mend-adopt.1 +27 -0
  8. package/man/mend-attach.1 +13 -0
  9. package/man/mend-codex.1 +60 -0
  10. package/man/mend-completions.1 +14 -0
  11. package/man/mend-connect.1 +25 -0
  12. package/man/mend-continue.1 +11 -0
  13. package/man/mend-doctor.1 +11 -0
  14. package/man/mend-dotfiles-sync.1 +15 -0
  15. package/man/mend-dotfiles.1 +11 -0
  16. package/man/mend-env-cluster.1 +26 -0
  17. package/man/mend-env-load.1 +21 -0
  18. package/man/mend-env-show.1 +15 -0
  19. package/man/mend-help.1 +11 -0
  20. package/man/mend-keys-init.1 +11 -0
  21. package/man/mend-keys-share.1 +11 -0
  22. package/man/mend-keys-show.1 +11 -0
  23. package/man/mend-login.1 +20 -0
  24. package/man/mend-logout.1 +11 -0
  25. package/man/mend-man.1 +11 -0
  26. package/man/mend-pair.1 +15 -0
  27. package/man/mend-projects.1 +11 -0
  28. package/man/mend-refresh.1 +11 -0
  29. package/man/mend-rejoin.1 +15 -0
  30. package/man/mend-resume.1 +15 -0
  31. package/man/mend-run.1 +18 -0
  32. package/man/mend-service-add.1 +21 -0
  33. package/man/mend-service-connect.1 +15 -0
  34. package/man/mend-service-init.1 +15 -0
  35. package/man/mend-service-list.1 +11 -0
  36. package/man/mend-service-logs.1 +15 -0
  37. package/man/mend-service-restart.1 +11 -0
  38. package/man/mend-service-run.1 +38 -0
  39. package/man/mend-service-stop.1 +11 -0
  40. package/man/mend-sessions.1 +27 -0
  41. package/man/mend-shell.1 +13 -0
  42. package/man/mend-skills-push.1 +21 -0
  43. package/man/mend-skills.1 +15 -0
  44. package/man/mend-ssh-setup.1 +15 -0
  45. package/man/mend-ssh.1 +11 -0
  46. package/man/mend-stop.1 +22 -0
  47. package/man/mend-ui.1 +13 -0
  48. package/man/mend-version.1 +11 -0
  49. package/man/mend-worktrees.1 +18 -0
  50. package/man/mend.1 +165 -0
  51. package/package.json +8 -3
package/dist/main.js CHANGED
@@ -8,12 +8,14 @@ import * as path from "node:path";
8
8
  import { doctorCommand } from "./doctor.js";
9
9
  import { readSyncFiles, scanDotfileCandidates } from "./dotfiles.js";
10
10
  import { formatLoadReport } from "./env.js";
11
+ import { findCommand, manFileName, renderCommand, renderGroup, renderIndex, renderManIndex, renderManPage, usageOf, } from "./help.js";
11
12
  import { loginCommand } from "./login.js";
12
13
  import { pairCommand, qrCommand } from "./pair.js";
13
14
  import { isComposeFile, proposeFromCompose, proposeFromPackageJson, proposeFromWorkspacePackage, renderMendToml, workspaceGlobs, } from "./service-init.js";
14
15
  import { agentIsLive, agentOutcome, cwdFacts, gitTopLevel, HARNESS_COMMANDS, isDetachChunk, LIVE_STATUSES, sessionDisplayName, matchProjectByCwd, normalizeProjectName, gitCurrentBranch, parseLaunchArgs, } from "./shared.js";
15
16
  import { DEFAULT_SKILLS_DIR, scanSkillLibrary } from "./skills.js";
16
17
  import { sshCommand } from "./ssh-setup.js";
18
+ import { cliVersion, fetchServerVersion, versionLines } from "./version.js";
17
19
  // `$XDG_CONFIG_HOME/mend`, default `~/.config/mend`; a pre-XDG `~/.mend` stays authoritative
18
20
  // when it is the only one present (mirrors @mend/store's resolver — the CLI stays dependency-light).
19
21
  const mendCliHome = () => {
@@ -270,14 +272,14 @@ const launch = async (config, harness, args) => {
270
272
  parsed.ask ||
271
273
  parsed.fast;
272
274
  if (harness === "run" && structured) {
273
- return fail("mend run takes no prompt or harness flags — usage: mend run [--project p] -- <command...>");
275
+ return fail(`mend run takes no prompt or harness flags · ${usageOf("run")}`);
274
276
  }
275
277
  if (harness === "run" && (parsed.detach || parsed.foreground)) {
276
278
  return fail("mend run takes no lifecycle flags — it tails the record; Ctrl+C stops watching, not the command");
277
279
  }
278
280
  const argv = harness === "run" ? parsed.custom : (HARNESS_COMMANDS[harness] ?? []);
279
281
  if (argv.length === 0) {
280
- return fail(harness === "run" ? "usage: mend run -- <command...>" : `unknown harness ${harness}`);
282
+ return fail(harness === "run" ? usageOf("run") : `unknown harness ${harness}`);
281
283
  }
282
284
  const project = await findProject(config, parsed.project, true);
283
285
  // Say which project the cwd resolved to before anything is created — a
@@ -673,7 +675,7 @@ const stopCommand = async (config, args) => {
673
675
  return;
674
676
  }
675
677
  if (!all && prefix === undefined) {
676
- return fail("usage: mend stop [session-id-prefix] | --all [--project <p>]");
678
+ return fail(usageOf("stop"));
677
679
  }
678
680
  const [sessions, projects] = await Promise.all([
679
681
  api(config, "GET", "/sessions"),
@@ -902,7 +904,7 @@ const serviceAdd = async (config, args) => {
902
904
  const positional = args.filter((a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1));
903
905
  const portRaw = positional.find((a) => /^\d+$/.test(a));
904
906
  if (portRaw === undefined) {
905
- return fail("usage: mend service add [session] <port> [--name <n>] [--udp] [--http|--https]");
907
+ return fail(usageOf("service add"));
906
908
  }
907
909
  const port = Number(portRaw);
908
910
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
@@ -937,7 +939,7 @@ const serviceList = async (config) => {
937
939
  const serviceStop = async (config, args) => {
938
940
  const needle = args.find((a) => !a.startsWith("--"));
939
941
  if (needle === undefined)
940
- return fail("usage: mend service stop <name-or-id-prefix>");
942
+ return fail(usageOf("service stop"));
941
943
  const services = await fetchServices(config);
942
944
  const matches = services.filter((service) => service.label === needle || service.id.startsWith(needle));
943
945
  if (matches.length === 0)
@@ -980,8 +982,7 @@ const autoConnect = async (config, service) => {
980
982
  };
981
983
  const serviceRun = async (config, args) => {
982
984
  const dashdash = args.indexOf("--");
983
- const usage = "usage: mend service run [session] --port <port> [--name <n>] [--udp] [--http|--https] [--no-connect] -- <command...>\n" +
984
- " mend service run [session] <name> [--no-connect] (a declared recipe)";
985
+ const usage = usageOf("service run");
985
986
  // No explicit command = a DECLARED Service: resolve the name against the
986
987
  // session worktree's mend.toml and start (or adopt) its recipe.
987
988
  if (dashdash === -1) {
@@ -1056,7 +1057,7 @@ const serviceLogs = async (config, args) => {
1056
1057
  const from = fromFlag === -1 ? "0" : args[fromFlag + 1];
1057
1058
  const needle = args.find((argument, index) => !argument.startsWith("--") && index !== fromFlag + 1);
1058
1059
  if (needle === undefined || from === undefined || !/^(0|[1-9]\d*)$/.test(from)) {
1059
- return fail("usage: mend service logs <name-or-id-prefix> [--from <decimal-sequence>]");
1060
+ return fail(usageOf("service logs"));
1060
1061
  }
1061
1062
  const everything = await fetchServiceViews(config, true);
1062
1063
  const matches = everything.filter((view) => view.service.name === needle ||
@@ -1112,7 +1113,7 @@ const serviceLogs = async (config, args) => {
1112
1113
  const serviceRestart = async (config, args) => {
1113
1114
  const needle = args.find((a) => !a.startsWith("--"));
1114
1115
  if (needle === undefined)
1115
- return fail("usage: mend service restart <name-or-id-prefix>");
1116
+ return fail(usageOf("service restart"));
1116
1117
  const service = await findLiveService(config, needle);
1117
1118
  const restarted = await withSpinner(`restarting ${service.label ?? service.id.slice(0, 8)}…`, mutateService(config, "POST", `/services/${service.id}/restart`));
1118
1119
  say(`${green("✓")} restarted · ${restarted.status}`);
@@ -1358,7 +1359,7 @@ const accountsCommand = async (config) => {
1358
1359
  const connectCommand = async (config, args) => {
1359
1360
  const [providerArg, ...flags] = args;
1360
1361
  if (!isProvider(providerArg)) {
1361
- return fail("usage: mend connect claude|codex|github [--from-stdin] [--remove]");
1362
+ return fail(usageOf("connect"));
1362
1363
  }
1363
1364
  const provider = providerArg;
1364
1365
  if (flags.includes("--remove")) {
@@ -1591,7 +1592,7 @@ const keysCommand = async (config, args) => {
1591
1592
  case undefined:
1592
1593
  return keysShow(config);
1593
1594
  default:
1594
- return fail(`unknown keys command "${verb}" — try: mend keys init | show | share`);
1595
+ return fail(`unknown keys command "${verb}" · mend help keys lists them`);
1595
1596
  }
1596
1597
  };
1597
1598
  const formatDotfileBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
@@ -1754,14 +1755,14 @@ const envCluster = async (config, args) => {
1754
1755
  const explicitProject = takeFlagValue(args, "--project");
1755
1756
  const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
1756
1757
  const [verb, ...rest] = positional;
1757
- const usage = "try: mend env cluster add secret <name> | add configmap <name> | remove <kind>/<name> | sa <name> | sa --clear";
1758
+ const usage = usageOf("env cluster");
1758
1759
  const project = await findProject(config, explicitProject);
1759
1760
  const route = `/projects/${project.id}/cluster-bindings`;
1760
1761
  const appliesLine = () => say(dim(" applies from the next workspace launch; running workspaces keep what they started with"));
1761
1762
  if (verb === "add") {
1762
1763
  const [kind, objectName] = rest;
1763
1764
  if ((kind !== "secret" && kind !== "configmap") || objectName === undefined) {
1764
- return fail(`env cluster add takes a kind and an object name — ${usage}`);
1765
+ return fail(`env cluster add takes a kind and an object name\n${usage}`);
1765
1766
  }
1766
1767
  const result = await api(config, "POST", route, {
1767
1768
  kind,
@@ -1775,7 +1776,7 @@ const envCluster = async (config, args) => {
1775
1776
  const [ref] = rest;
1776
1777
  const [kind, objectName] = ref?.split("/", 2) ?? [];
1777
1778
  if ((kind !== "secret" && kind !== "configmap") || objectName === undefined) {
1778
- return fail(`env cluster remove takes <kind>/<name>, e.g. secret/app-env — ${usage}`);
1779
+ return fail(`env cluster remove takes <kind>/<name>, e.g. secret/app-env\n${usage}`);
1779
1780
  }
1780
1781
  const snapshot = await api(config, "GET", route);
1781
1782
  const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
@@ -1789,7 +1790,7 @@ const envCluster = async (config, args) => {
1789
1790
  const clear = args.includes("--clear");
1790
1791
  const [name] = rest;
1791
1792
  if (!clear && name === undefined)
1792
- return fail(`env cluster sa takes a name or --clear — ${usage}`);
1793
+ return fail(`env cluster sa takes a name or --clear\n${usage}`);
1793
1794
  const result = await api(config, "PUT", `${route}/service-account`, { serviceAccount: clear ? null : name });
1794
1795
  say(result.serviceAccount === null
1795
1796
  ? `${green("✓")} cleared the workspace service account ${dim(`· cluster r${result.revision}`)}`
@@ -1799,7 +1800,7 @@ const envCluster = async (config, args) => {
1799
1800
  }
1800
1801
  return appliesLine();
1801
1802
  }
1802
- return fail(`unknown env cluster command "${verb ?? ""}" — ${usage}`);
1803
+ return fail(`unknown env cluster command "${verb ?? ""}"\n${usage}`);
1803
1804
  };
1804
1805
  const envCommand = async (config, args) => {
1805
1806
  const [verb, ...rest] = args;
@@ -1812,7 +1813,7 @@ const envCommand = async (config, args) => {
1812
1813
  case undefined:
1813
1814
  return envShow(config, rest);
1814
1815
  default:
1815
- return fail(`unknown env command "${verb}" — try: mend env load | show | cluster`);
1816
+ return fail(`unknown env command "${verb}" · mend help env lists them`);
1816
1817
  }
1817
1818
  };
1818
1819
  const dotfilesCommand = async (config, args) => {
@@ -1824,7 +1825,7 @@ const dotfilesCommand = async (config, args) => {
1824
1825
  case undefined:
1825
1826
  return dotfilesShow(config);
1826
1827
  default:
1827
- return fail(`unknown dotfiles command "${verb}" — try: mend dotfiles sync | show`);
1828
+ return fail(`unknown dotfiles command "${verb}" · mend help dotfiles lists them`);
1828
1829
  }
1829
1830
  };
1830
1831
  /** With `--project` the project's library; bare, your own. */
@@ -1893,7 +1894,7 @@ const skillsCommand = async (config, args) => {
1893
1894
  // Bare flags (`mend skills --project web`) read as the list.
1894
1895
  if (verb.startsWith("--"))
1895
1896
  return skillsList(config, args);
1896
- return fail(`unknown skills command "${verb}" — try: mend skills push | list`);
1897
+ return fail(`unknown skills command "${verb}" · mend help skills lists them`);
1897
1898
  }
1898
1899
  };
1899
1900
  // ─── completions: live sessions under TAB ───────────────────────────────────
@@ -1978,7 +1979,7 @@ const completionsCommand = (args) => {
1978
1979
  process.stdout.write(BASH_COMPLETIONS);
1979
1980
  return;
1980
1981
  default:
1981
- return fail('usage: mend completions zsh|bash — e.g. mend completions zsh > "$fpath[1]/_mend"');
1982
+ return fail(`${usageOf("completions")} · e.g. mend completions zsh > "$fpath[1]/_mend"`);
1982
1983
  }
1983
1984
  };
1984
1985
  // ─── supervised run: platform workspace + PTY + record ──────────────────────
@@ -2327,7 +2328,9 @@ const sessionsCommand = async (config, args) => {
2327
2328
  }
2328
2329
  let rows;
2329
2330
  if (all || projectName !== null) {
2330
- const details = await Promise.all(scope.map((p) => api(config, "GET", `/projects/${p.id}`)));
2331
+ // --all means all: dead ends (settled, no conversation) included, which the server hides
2332
+ // otherwise for every client.
2333
+ const details = await Promise.all(scope.map((p) => api(config, "GET", `/projects/${p.id}${all ? "?deadEnds=include" : ""}`)));
2331
2334
  rows = details.flatMap((detail) => detail.sessions.map((session) => ({
2332
2335
  session,
2333
2336
  projectName: detail.project.name,
@@ -2519,7 +2522,7 @@ const hasNodeFfi = () => {
2519
2522
  */
2520
2523
  const dashboard = async (config) => {
2521
2524
  if (process.stdout.isTTY !== true) {
2522
- say(HELP);
2525
+ say(renderIndex());
2523
2526
  return;
2524
2527
  }
2525
2528
  if (!hasNodeFfi()) {
@@ -2540,99 +2543,54 @@ const dashboard = async (config) => {
2540
2543
  });
2541
2544
  };
2542
2545
  // ─── entry ──────────────────────────────────────────────────────────────────
2543
- const HELP = `mend — the agent workbench
2544
-
2545
- start
2546
- mend login [--url <server>] sign this terminal in through the browser: opens
2547
- <server>/authorize, you press Authorize there, and the
2548
- CLI saves a revocable device token (0600); no password
2549
- is ever typed here
2550
- mend connect <provider> [--from-stdin] [--remove]
2551
- send THIS machine's claude/codex/github credential to the
2552
- platform under your own user (reads the file the provider's
2553
- CLI wrote at login; --from-stdin pastes one instead)
2554
- mend adopt [source] [--name <name>] [--auth ambient|mend-key|bridge]
2555
- adopt a repository into the store (default: cwd; any git
2556
- URL — GitHub, GitLab, self-hosted, ssh://, a local path)
2557
- mend codex|claude|opencode ["prompt"] [--name <worktree>] [--worktree <existing>]
2558
- [--model <id>] [--effort low|medium|high|xhigh|max]
2559
- [--base <ref>] [--ask] [--fast] [--detach|-d] [--foreground]
2560
- launch the harness in a worktree; the worktree's name is
2561
- asked first (--name skips the ask — an EXISTING name joins
2562
- that worktree as a new session; --worktree joins only,
2563
- failing if absent), a
2564
- quoted prompt becomes its first message,
2565
- --ask restores the harness's permission prompts, --fast
2566
- requests priority processing (codex service tier),
2567
- --detach launches without attaching (reattach anywhere),
2568
- --foreground stops the session when this CLI exits
2569
- mend pair [--url <base url>] pair a phone or a second machine: prints a QR, the code, and
2570
- the URL to reach this server (one device, once, 10 minutes)
2571
- mend doctor read-only checklist of this machine's setup — one line per
2572
- fact, each unfinished one ending in the command that fixes it
2573
-
2574
- everything else
2575
- mend the dashboard: every project and session, live
2576
- mend logout revoke this terminal's device token and forget it
2577
- mend keys init generate the machine's Mend deploy key (ed25519)
2578
- mend keys show print the public key — add it as a deploy key on your git host
2579
- mend env load [file] [--secret [A,B]] load a .env into the project: ordinary names → configuration,
2580
- secret-shaped names → secrets; --secret sends all (or the
2581
- named ones, e.g. DATABASE_URL) to secrets
2582
- mend env show what the project store holds — names only, never values
2583
- mend accounts your connected accounts on the platform (claude, codex, github)
2584
- mend dotfiles your dotfiles on the server: repo + synced home files
2585
- mend dotfiles sync [--all | paths…] capture config files from THIS machine into your store
2586
- mend skills [--project [p]] your skill library on the server (or a project's)
2587
- mend skills push [--project [p]] [--prune] [--dir <path>]
2588
- upload ~/.agents/skills bundles into the library; sessions
2589
- receive them at launch (--prune removes what's gone)
2590
- mend keys share relay THIS machine's ssh-agent to the server (bridge mode:
2591
- hardware keys sign here; Ctrl-C stops sharing)
2592
- mend run -- <command...> same, with an arbitrary command
2593
- mend worktrees [--project <p>] [--json]
2594
- every worktree and the sessions inside it (mend sessions
2595
- --json stays the v1 flat list; --json=v2 groups like this)
2596
- mend attach [session-id-prefix] reattach this terminal to a running session
2597
- (no id: pick one from the live sessions)
2598
- mend stop [session-id-prefix] | --all [--project <p>]
2599
- stop the agent — the workspace harvests and closes; the
2600
- record and review remain
2601
- mend shell [session-id-prefix] open a shell in a live session's workspace
2602
- mend service run [session] --port <p> [--name <n>] [--udp] -- <command...>
2603
- start + supervise a server in the session workspace
2604
- mend service run [session] <name> start a declared Service (mend.toml recipe)
2605
- mend service <name> shorthand for the above
2606
- mend service init [--yes] scaffold mend.toml from package.json + compose ports
2607
- mend service add [session] <port> [--name <n>] [--udp]
2608
- adopt a listening workspace port — reachable on this machine
2609
- mend service connect [name…] [--port <p>]
2610
- bring live Services to THIS machine's loopback — each
2611
- connection tunnels through the server, authenticated as you
2612
- mend service list every live service and its observed state
2613
- mend service logs <name-or-id> follow a supervised service's output (replay, then live)
2614
- mend service restart <name-or-id> re-run its recorded command — same URL
2615
- mend service stop <name-or-id> stop a service (closes its host port)
2616
- mend continue [session-id] resume a session with its pending review follow-up
2617
- mend resume [session-id] [--with h] rejoin a settled session (state restored; --with switches harness)
2618
- mend rejoin [session-id] [--harness h] attach if live, otherwise resume; newest live wins
2619
- mend refresh [project] fetch origin's branches into the store — new sessions
2620
- base on current tips (default: the cwd's project)
2621
- mend projects adopted projects and their live sessions
2622
- mend sessions [--all] [--project p] [--json]
2623
- sessions with review facts; JSON is stable for integrations
2624
- mend status active sessions (alias of mend sessions)
2625
- mend ssh workspace SSH status: gateway, registered keys, ssh config
2626
- mend ssh setup [--key <path>] make this machine ready once: offer a key (ssh-agent
2627
- preferred — nothing new created), write Host mend-ws
2628
- mend completions zsh|bash print the TAB-completion hook (live session ids under TAB)
2629
-
2630
- server: MEND_URL (default http://localhost:3105) · auth: MEND_TOKEN
2631
- detach key: Ctrl+] (set MEND_DETACH_KEY=none when an outer multiplexer owns detaching)
2632
- config file: ~/.config/mend/cli.json { "url": ..., "token": ..., "deviceId": ... }
2633
- `;
2546
+ /** `mend help [command...]`: the index, a group, or one page. */
2547
+ const helpCommand = (words) => {
2548
+ if (words.length === 0) {
2549
+ say(renderIndex());
2550
+ return;
2551
+ }
2552
+ const doc = findCommand(words);
2553
+ if (doc !== null && (doc.name.split(" ").length > 1 || words.length === 1)) {
2554
+ say(renderCommand(doc));
2555
+ return;
2556
+ }
2557
+ const group = renderGroup(words[0] ?? "");
2558
+ if (doc !== null) {
2559
+ say(renderCommand(doc));
2560
+ return;
2561
+ }
2562
+ if (group !== null) {
2563
+ say(group);
2564
+ return;
2565
+ }
2566
+ return fail(`no command "${words.join(" ")}" · mend help lists them`);
2567
+ };
2568
+ /** `mend man [command...]`: the same page through man(1), or the text page without it. */
2569
+ const manCommand = (words) => {
2570
+ const doc = words.length === 0 ? null : findCommand(words);
2571
+ if (words.length > 0 && doc === null) {
2572
+ return fail(`no command "${words.join(" ")}" · mend help lists them`);
2573
+ }
2574
+ const version = cliVersion();
2575
+ const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
2576
+ const file = path.join(os.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
2577
+ fs.writeFileSync(file, page);
2578
+ const result = spawnSync("man", ["-l", file], { stdio: "inherit" });
2579
+ fs.rmSync(file, { force: true });
2580
+ if (result.error !== undefined || result.status !== 0) {
2581
+ say(doc === null ? renderIndex() : renderCommand(doc));
2582
+ }
2583
+ };
2634
2584
  const main = async () => {
2635
2585
  const [command, ...rest] = process.argv.slice(2);
2586
+ // `mend <command> --help` (or -h) before the separator is that command's page,
2587
+ // never an argument: `mend run -- cmd --help` keeps its --help for cmd.
2588
+ const ownArgs = rest.slice(0, rest.indexOf("--") === -1 ? rest.length : rest.indexOf("--"));
2589
+ if (command !== undefined &&
2590
+ command !== "help" &&
2591
+ ownArgs.some((a) => a === "--help" || a === "-h")) {
2592
+ return helpCommand([command, ...ownArgs.filter((a) => !a.startsWith("-"))]);
2593
+ }
2636
2594
  const config = loadConfig();
2637
2595
  switch (command) {
2638
2596
  case "adopt":
@@ -2666,7 +2624,7 @@ const main = async () => {
2666
2624
  return connectCommand(config, rest);
2667
2625
  case "pair":
2668
2626
  return pairCommand(rest, boundApi(config));
2669
- // Deliberately absent from HELP: the installer renders its own QR through this.
2627
+ // Hidden in the catalog: the installer renders its own QR through this.
2670
2628
  case "qr":
2671
2629
  return qrCommand(rest);
2672
2630
  case "doctor":
@@ -2699,10 +2657,18 @@ const main = async () => {
2699
2657
  return dashboard(config);
2700
2658
  case "help":
2701
2659
  case "--help":
2702
- say(HELP);
2660
+ case "-h":
2661
+ return helpCommand(rest);
2662
+ case "man":
2663
+ return manCommand(rest);
2664
+ case "version":
2665
+ case "--version":
2666
+ case "-v":
2667
+ for (const line of versionLines(cliVersion(), await fetchServerVersion(config.url)))
2668
+ say(line);
2703
2669
  return;
2704
2670
  default:
2705
- return fail(`unknown command "${command}" — try: mend help`);
2671
+ return fail(`unknown command "${command}" · mend help lists them`);
2706
2672
  }
2707
2673
  };
2708
2674
  await main();
@@ -0,0 +1,48 @@
1
+ import * as fs from "node:fs";
2
+ /**
3
+ * `mend version`: this CLI's version, then the server's when it answers.
4
+ * The two drift apart in practice — an npm install on one machine, a cluster
5
+ * roll on another — and "which one am I on" is the first question any bug
6
+ * report needs answered. The server line is best-effort: a missing server is
7
+ * a fact to print, never an error.
8
+ */
9
+ /** The published version, read from the package this file ships in (dist/ and src/ sit one level under it). */
10
+ export const cliVersion = () => {
11
+ const raw = fs.readFileSync(new URL("../package.json", import.meta.url), "utf8");
12
+ const parsed = JSON.parse(raw);
13
+ if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
14
+ const version = parsed.version;
15
+ if (typeof version === "string")
16
+ return version;
17
+ }
18
+ return "unknown";
19
+ };
20
+ /** Ask the server's health route, briefly; a slow or absent server is `null`. */
21
+ export const fetchServerVersion = async (url, timeoutMs = 2_000) => {
22
+ try {
23
+ const response = await fetch(`${url}/api/health`, {
24
+ signal: AbortSignal.timeout(timeoutMs),
25
+ });
26
+ if (!response.ok)
27
+ return { url, version: null };
28
+ const body = await response.json();
29
+ const version = typeof body === "object" && body !== null && "version" in body ? body.version : null;
30
+ return { url, version: typeof version === "string" ? version : null };
31
+ }
32
+ catch {
33
+ return { url, version: null };
34
+ }
35
+ };
36
+ /** The lines `mend version` prints — terse facts, no verdict beyond the mismatch note. */
37
+ export const versionLines = (cli, server) => {
38
+ const lines = [`mend ${cli}`];
39
+ if (server.version === null) {
40
+ lines.push(`server · unreachable · ${server.url}`);
41
+ }
42
+ else {
43
+ lines.push(`server ${server.version} · ${server.url}`);
44
+ if (server.version !== cli)
45
+ lines.push("versions differ — the server's API wins");
46
+ }
47
+ return lines;
48
+ };
@@ -0,0 +1,11 @@
1
+ .TH MEND-ACCOUNTS 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-accounts \- your connected accounts on the platform
4
+ .SH SYNOPSIS
5
+ .B mend accounts
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ claude, codex, and github: connected, invalid, or archived, and when each was last used.
10
+ .SH SEE ALSO
11
+ mend(1), mend\-connect(1)
@@ -0,0 +1,27 @@
1
+ .TH MEND-ADOPT 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-adopt \- adopt a repository into the store
4
+ .SH SYNOPSIS
5
+ .B mend adopt [source] [\-\-name <name>] [\-\-auth ambient|mend\-key|bridge]
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ Clones the repository into Mend's store. Every session then gets its own worktree of it. The default source is the current directory; any git URL works, GitHub, GitLab, self\-hosted, ssh://, or a local path.
10
+ .PP
11
+ \-\-auth says how the store fetches from the remote. ambient uses the server's own credentials. mend\-key uses this machine's deploy key (see mend keys). bridge relays this machine's ssh\-agent while mend keys share runs, so hardware keys stay here.
12
+ .SH OPTIONS
13
+ .TP
14
+ .B \-\-name <name>
15
+ the project's name in Mend. Default: the repository's
16
+ .TP
17
+ .B \-\-auth <mode>
18
+ ambient, mend\-key, or bridge. Default: ambient
19
+ .SH EXAMPLES
20
+ .PP
21
+ .B mend adopt
22
+ .br
23
+ the repository you are standing in
24
+ .PP
25
+ .B mend adopt git@github.com:acme/api.git \-\-auth mend\-key
26
+ .SH SEE ALSO
27
+ mend(1), mend\-keys\-init(1), mend\-keys\-share(1), mend\-refresh(1)
@@ -0,0 +1,13 @@
1
+ .TH MEND-ATTACH 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-attach \- reattach this terminal to a running session
4
+ .SH SYNOPSIS
5
+ .B mend attach [session\-id\-prefix]
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ With no id, the one live session is taken; with several, a picker opens. A prefix of the id is enough.
10
+ .PP
11
+ A session that was picked up on the phone is taken back into this terminal: the phone's agent ends and the same conversation continues here.
12
+ .SH SEE ALSO
13
+ mend(1), mend\-stop(1), mend\-rejoin(1), mend\-shell(1)
@@ -0,0 +1,60 @@
1
+ .TH MEND-CODEX 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-codex \- launch codex, claude, or opencode in a recorded worktree
4
+ .SH SYNOPSIS
5
+ .B mend codex ["prompt"] [\-\-name <worktree>] [\-\-worktree <existing>] [\-\-model <id>] [\-\-effort <level>] [\-\-base <ref>] [\-\-ask] [\-\-fast] [\-\-detach|\-d] [\-\-foreground] [\-\-project <p>]
6
+ .br
7
+ also mend claude, mend opencode, with the same options
8
+ .SH DESCRIPTION
9
+ .PP
10
+ mend codex, mend claude, and mend opencode are the same command with a different harness. The session runs in a workspace on the platform, in its own git worktree, and everything it does is recorded. This terminal attaches to it.
11
+ .PP
12
+ The worktree's name is asked first. \-\-name skips the ask; an existing name joins that worktree as a new session. \-\-worktree joins only and fails if the name is unknown. A quoted prompt becomes the first message.
13
+ .PP
14
+ Detach with Ctrl+] and the session keeps running. Reattach from any terminal with mend attach, or from the phone.
15
+ .SH OPTIONS
16
+ .TP
17
+ .B \-\-name <worktree>
18
+ name the worktree; an existing name joins it
19
+ .TP
20
+ .B \-\-worktree <existing>
21
+ join an existing worktree only
22
+ .TP
23
+ .B \-\-model <id>
24
+ the harness's model id
25
+ .TP
26
+ .B \-\-effort <level>
27
+ low, medium, high, xhigh, or max
28
+ .TP
29
+ .B \-\-base <ref>
30
+ the branch or sha the worktree starts from
31
+ .TP
32
+ .B \-\-ask
33
+ keep the harness's permission prompts
34
+ .TP
35
+ .B \-\-fast
36
+ priority processing where the harness offers it (codex)
37
+ .TP
38
+ .B \-\-detach, \-d
39
+ launch without attaching
40
+ .TP
41
+ .B \-\-foreground
42
+ stop the session when this CLI exits
43
+ .TP
44
+ .B \-\-project <p>
45
+ the project by name, when the current directory is not inside it
46
+ .SH EXAMPLES
47
+ .PP
48
+ .B mend claude "add a health endpoint"
49
+ .br
50
+ asks for a worktree name, then runs
51
+ .PP
52
+ .B mend codex \-\-name auth\-rework \-d
53
+ .br
54
+ start in the background
55
+ .PP
56
+ .B mend codex \-\-worktree auth\-rework
57
+ .br
58
+ a second session in the same worktree
59
+ .SH SEE ALSO
60
+ mend(1), mend\-attach(1), mend\-stop(1), mend\-run(1), mend\-sessions(1)
@@ -0,0 +1,14 @@
1
+ .TH MEND-COMPLETIONS 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-completions \- print the TAB\-completion hook
4
+ .SH SYNOPSIS
5
+ .B mend completions zsh|bash
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ Commands complete, and live session ids complete under TAB by asking the server.
10
+ .SH EXAMPLES
11
+ .PP
12
+ .B mend completions zsh > "$fpath[1]/_mend"
13
+ .SH SEE ALSO
14
+ mend(1)
@@ -0,0 +1,25 @@
1
+ .TH MEND-CONNECT 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-connect \- send this machine's provider credential to the platform
4
+ .SH SYNOPSIS
5
+ .B mend connect <claude|codex|github> [\-\-from\-stdin] [\-\-remove]
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ Sessions run on the platform, so the platform needs your provider login. This reads the file the provider's own CLI wrote when you logged in (claude, codex) or asks gh for its token (github) and stores it under your own user. Nothing is shared with other users.
10
+ .SH OPTIONS
11
+ .TP
12
+ .B \-\-from\-stdin
13
+ paste a credential instead of reading the provider's file
14
+ .TP
15
+ .B \-\-remove
16
+ disconnect the provider
17
+ .SH EXAMPLES
18
+ .PP
19
+ .B mend connect claude
20
+ .br
21
+ after `claude login` on this machine
22
+ .PP
23
+ .B mend connect github \-\-remove
24
+ .SH SEE ALSO
25
+ mend(1), mend\-accounts(1)
@@ -0,0 +1,11 @@
1
+ .TH MEND-CONTINUE 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-continue \- resume a session with its pending review follow\-up
4
+ .SH SYNOPSIS
5
+ .B mend continue [session\-id]
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ The review comments you sent to the session become its first message. With no id, the newest session with a pending follow\-up is taken.
10
+ .SH SEE ALSO
11
+ mend(1), mend\-resume(1), mend\-sessions(1)
@@ -0,0 +1,11 @@
1
+ .TH MEND-DOCTOR 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-doctor \- check this machine's setup
4
+ .SH SYNOPSIS
5
+ .B mend doctor
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ Read\-only. One line per fact: the server, the sign\-in, the platform connection, the provider accounts, the git key. Each unfinished line ends with the command that fixes it.
10
+ .SH SEE ALSO
11
+ mend(1), mend\-login(1), mend\-connect(1), mend\-keys\-init(1)
@@ -0,0 +1,15 @@
1
+ .TH MEND-DOTFILES-SYNC 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-dotfiles\-sync \- capture config files from this machine into your store
4
+ .SH SYNOPSIS
5
+ .B mend dotfiles sync [\-\-all | paths...]
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ Copies the named files from your home directory into your store on the server. \-\-all takes the known shell, git, and editor files. Setups that rely on ZDOTDIR do not transfer.
10
+ .SH OPTIONS
11
+ .TP
12
+ .B \-\-all
13
+ every known config file
14
+ .SH SEE ALSO
15
+ mend(1), mend\-dotfiles(1)
@@ -0,0 +1,11 @@
1
+ .TH MEND-DOTFILES 1 "" "mend 0.19.0" "mend manual"
2
+ .SH NAME
3
+ mend\-dotfiles \- your dotfiles on the server: repo and synced files
4
+ .SH SYNOPSIS
5
+ .B mend dotfiles [show]
6
+ .br
7
+ .SH DESCRIPTION
8
+ .PP
9
+ Workspaces apply your dotfiles at launch, from a repository you point the server at or from files mend dotfiles sync captured. This shows what is set.
10
+ .SH SEE ALSO
11
+ mend(1), mend\-dotfiles\-sync(1)