@sealant/mend 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard-model.js +8 -0
- package/dist/dashboard.js +12 -3
- package/dist/help.js +824 -0
- package/dist/main.js +129 -127
- package/dist/version.js +48 -0
- package/man/mend-accounts.1 +11 -0
- package/man/mend-adopt.1 +27 -0
- package/man/mend-attach.1 +13 -0
- package/man/mend-codex.1 +60 -0
- package/man/mend-completions.1 +14 -0
- package/man/mend-connect.1 +25 -0
- package/man/mend-continue.1 +11 -0
- package/man/mend-doctor.1 +11 -0
- package/man/mend-dotfiles-sync.1 +15 -0
- package/man/mend-dotfiles.1 +11 -0
- package/man/mend-env-cluster.1 +26 -0
- package/man/mend-env-load.1 +21 -0
- package/man/mend-env-show.1 +15 -0
- package/man/mend-help.1 +11 -0
- package/man/mend-keys-init.1 +11 -0
- package/man/mend-keys-share.1 +11 -0
- package/man/mend-keys-show.1 +11 -0
- package/man/mend-login.1 +20 -0
- package/man/mend-logout.1 +11 -0
- package/man/mend-man.1 +11 -0
- package/man/mend-pair.1 +15 -0
- package/man/mend-projects.1 +11 -0
- package/man/mend-refresh.1 +11 -0
- package/man/mend-rejoin.1 +15 -0
- package/man/mend-resume.1 +15 -0
- package/man/mend-run.1 +18 -0
- package/man/mend-service-add.1 +21 -0
- package/man/mend-service-connect.1 +15 -0
- package/man/mend-service-init.1 +15 -0
- package/man/mend-service-list.1 +11 -0
- package/man/mend-service-logs.1 +15 -0
- package/man/mend-service-restart.1 +11 -0
- package/man/mend-service-run.1 +38 -0
- package/man/mend-service-stop.1 +11 -0
- package/man/mend-sessions.1 +27 -0
- package/man/mend-shell.1 +13 -0
- package/man/mend-skills-push.1 +21 -0
- package/man/mend-skills.1 +15 -0
- package/man/mend-ssh-setup.1 +15 -0
- package/man/mend-ssh.1 +11 -0
- package/man/mend-stop.1 +22 -0
- package/man/mend-ui.1 +13 -0
- package/man/mend-version.1 +11 -0
- package/man/mend-worktrees.1 +18 -0
- package/man/mend.1 +165 -0
- 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(
|
|
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" ? "
|
|
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
|
|
@@ -618,16 +620,41 @@ const exitAfterSessionEnd = (config, sessionId) => {
|
|
|
618
620
|
/** Reattach a terminal to a running session (full scrollback replay, then live). */
|
|
619
621
|
const attach = async (config, args) => {
|
|
620
622
|
const prefix = args.find((a) => !a.startsWith("--"));
|
|
621
|
-
|
|
622
|
-
|
|
623
|
+
// No id: the picker IS the selection surface (the resolution `mend shell`
|
|
624
|
+
// already uses), so attaching never demands an id the user must go look up.
|
|
625
|
+
if (prefix === undefined) {
|
|
626
|
+
const picked = await resolveLiveSession(config, undefined, "attach");
|
|
627
|
+
return attachPicked(config, picked);
|
|
628
|
+
}
|
|
623
629
|
const sessions = await api(config, "GET", "/sessions");
|
|
624
630
|
const match = sessions.find((s) => s.id.startsWith(prefix));
|
|
625
631
|
if (match === undefined)
|
|
626
632
|
return fail(`no active session matches "${prefix}"`);
|
|
627
|
-
|
|
633
|
+
return attachPicked(config, match);
|
|
634
|
+
};
|
|
635
|
+
/** Attach to one resolved session — the tail both `mend attach` paths share. */
|
|
636
|
+
const attachPicked = async (config, session) => {
|
|
637
|
+
say(`${green("✓")} attaching to ${sessionDisplayName(session)} · ${session.harness} ${dim(session.id.slice(0, 8))}${detachHint()}`);
|
|
628
638
|
say("");
|
|
629
|
-
await
|
|
630
|
-
|
|
639
|
+
const outcome = await attachTty(config, session.id, session.harness, 0n, undefined, {
|
|
640
|
+
handleSignals: true,
|
|
641
|
+
});
|
|
642
|
+
// A live protocol agent (a phone pickup) has no PTY behind it — the attach
|
|
643
|
+
// reports "unavailable" with nothing wrong. Take the session over: end the
|
|
644
|
+
// protocol agent, resume the same conversation as a TUI, then attach to it.
|
|
645
|
+
if (outcome === "unavailable") {
|
|
646
|
+
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
647
|
+
if (detail.currentAgent?.kind === "agent-protocol" &&
|
|
648
|
+
agentIsLive(detail.session, detail.currentAgent)) {
|
|
649
|
+
say(`${amber("taking over")} from the protocol session`);
|
|
650
|
+
await withSpinner("reopening as a terminal — same conversation…", api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" }));
|
|
651
|
+
say("");
|
|
652
|
+
await attachOrExit(config, session.id, session.harness);
|
|
653
|
+
return exitAfterSessionEnd(config, session.id);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
await finishAttach(config, session.id, outcome);
|
|
657
|
+
return exitAfterSessionEnd(config, session.id);
|
|
631
658
|
};
|
|
632
659
|
/**
|
|
633
660
|
* Explicit stop — the one intent detaching never carries. Ends the agent; the
|
|
@@ -640,8 +667,15 @@ const stopCommand = async (config, args) => {
|
|
|
640
667
|
? String(args[projectFlag + 1])
|
|
641
668
|
: null;
|
|
642
669
|
const prefix = args.find((arg, index) => !arg.startsWith("--") && index !== projectFlag + 1);
|
|
670
|
+
// Neither an id nor --all: pick the session to close, the same resolution
|
|
671
|
+
// `mend attach` uses. --all and --project keep their bulk meaning.
|
|
672
|
+
if (!all && prefix === undefined && projectName === null) {
|
|
673
|
+
const picked = await resolveLiveSession(config, undefined, "stop");
|
|
674
|
+
await stopSessions(config, [picked], false);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
643
677
|
if (!all && prefix === undefined) {
|
|
644
|
-
return fail("
|
|
678
|
+
return fail(usageOf("stop"));
|
|
645
679
|
}
|
|
646
680
|
const [sessions, projects] = await Promise.all([
|
|
647
681
|
api(config, "GET", "/sessions"),
|
|
@@ -671,19 +705,24 @@ const stopCommand = async (config, args) => {
|
|
|
671
705
|
}
|
|
672
706
|
targets = matches;
|
|
673
707
|
}
|
|
708
|
+
await stopSessions(config, targets, all);
|
|
709
|
+
};
|
|
710
|
+
/** Stop each target and print its record link — the tail every stop path shares. */
|
|
711
|
+
const stopSessions = async (config, targets, summarise) => {
|
|
674
712
|
for (const session of targets) {
|
|
675
713
|
await api(config, "POST", `/sessions/${session.id}/stop`);
|
|
676
714
|
say(`${green("✓")} stopped · ${session.harness} · ${dim(session.id.slice(0, 8))} · ${session.branch}`);
|
|
677
715
|
say(`${cobalt(" review")} · ${config.url}/sessions/${session.id}`);
|
|
678
716
|
}
|
|
679
|
-
if (
|
|
717
|
+
if (summarise) {
|
|
680
718
|
say(`${green("✓")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
|
|
719
|
+
}
|
|
681
720
|
};
|
|
682
721
|
/** Compact picker: numbered live sessions, one keystroke of typing. */
|
|
683
722
|
const pickSessionInteractively = async (rows) => {
|
|
684
723
|
say(dim("more than one live session — pick one:"));
|
|
685
724
|
rows.forEach((row, index) => {
|
|
686
|
-
say(` ${index + 1}. ${row.session.harness.padEnd(8)} ${dim(row.session.id.slice(0, 8))} ${row.projectName} ${dim(row.session.branch)}`);
|
|
725
|
+
say(` ${index + 1}. ${row.session.harness.padEnd(8)} ${dim(row.session.id.slice(0, 8))} ${row.projectName} ${dim(row.session.branch)} ${dim(row.session.status)}`);
|
|
687
726
|
});
|
|
688
727
|
const readline = await import("node:readline/promises");
|
|
689
728
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -699,7 +738,7 @@ const pickSessionInteractively = async (rows) => {
|
|
|
699
738
|
* the cwd's project narrows, one candidate is taken, several go to the
|
|
700
739
|
* picker, and a non-interactive caller never gets a silent guess.
|
|
701
740
|
*/
|
|
702
|
-
const resolveLiveSession = async (config, prefix) => {
|
|
741
|
+
const resolveLiveSession = async (config, prefix, command) => {
|
|
703
742
|
const sessions = await api(config, "GET", "/sessions?retained=1");
|
|
704
743
|
if (prefix !== undefined) {
|
|
705
744
|
const matches = sessions.filter((s) => s.id.startsWith(prefix));
|
|
@@ -721,7 +760,7 @@ const resolveLiveSession = async (config, prefix) => {
|
|
|
721
760
|
if (candidates.length === 1)
|
|
722
761
|
return only;
|
|
723
762
|
if (process.stdin.isTTY !== true) {
|
|
724
|
-
return fail(
|
|
763
|
+
return fail(`several live sessions — name one: mend ${command} <session-id-prefix>`);
|
|
725
764
|
}
|
|
726
765
|
const nameById = new Map(projects.map((p) => [p.id, p.name]));
|
|
727
766
|
return pickSessionInteractively(candidates.map((session) => ({
|
|
@@ -737,7 +776,7 @@ const resolveLiveSession = async (config, prefix) => {
|
|
|
737
776
|
*/
|
|
738
777
|
const shellCommand = async (config, args) => {
|
|
739
778
|
const prefix = args.find((a) => !a.startsWith("--"));
|
|
740
|
-
const session = await resolveLiveSession(config, prefix);
|
|
779
|
+
const session = await resolveLiveSession(config, prefix, "shell");
|
|
741
780
|
say(`${green("✓")} shell in ${session.harness} session ${dim(session.id.slice(0, 8))} · ${dim(session.branch)}${detachHint()}`);
|
|
742
781
|
const shellProcess = await withSpinner("opening a shell in the session workspace…", api(config, "POST", `/sessions/${session.id}/shell`));
|
|
743
782
|
say("");
|
|
@@ -865,14 +904,14 @@ const serviceAdd = async (config, args) => {
|
|
|
865
904
|
const positional = args.filter((a, i) => !a.startsWith("--") && (nameFlag === -1 || i !== nameFlag + 1));
|
|
866
905
|
const portRaw = positional.find((a) => /^\d+$/.test(a));
|
|
867
906
|
if (portRaw === undefined) {
|
|
868
|
-
return fail("
|
|
907
|
+
return fail(usageOf("service add"));
|
|
869
908
|
}
|
|
870
909
|
const port = Number(portRaw);
|
|
871
910
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
872
911
|
return fail(`"${portRaw}" is not a port`);
|
|
873
912
|
}
|
|
874
913
|
const prefix = positional.find((a) => a !== portRaw);
|
|
875
|
-
const session = await resolveLiveSession(config, prefix);
|
|
914
|
+
const session = await resolveLiveSession(config, prefix, "service add");
|
|
876
915
|
const service = await mutateService(config, "POST", `/sessions/${session.id}/services`, {
|
|
877
916
|
port,
|
|
878
917
|
name,
|
|
@@ -900,7 +939,7 @@ const serviceList = async (config) => {
|
|
|
900
939
|
const serviceStop = async (config, args) => {
|
|
901
940
|
const needle = args.find((a) => !a.startsWith("--"));
|
|
902
941
|
if (needle === undefined)
|
|
903
|
-
return fail("
|
|
942
|
+
return fail(usageOf("service stop"));
|
|
904
943
|
const services = await fetchServices(config);
|
|
905
944
|
const matches = services.filter((service) => service.label === needle || service.id.startsWith(needle));
|
|
906
945
|
if (matches.length === 0)
|
|
@@ -943,8 +982,7 @@ const autoConnect = async (config, service) => {
|
|
|
943
982
|
};
|
|
944
983
|
const serviceRun = async (config, args) => {
|
|
945
984
|
const dashdash = args.indexOf("--");
|
|
946
|
-
const usage = "
|
|
947
|
-
" mend service run [session] <name> [--no-connect] (a declared recipe)";
|
|
985
|
+
const usage = usageOf("service run");
|
|
948
986
|
// No explicit command = a DECLARED Service: resolve the name against the
|
|
949
987
|
// session worktree's mend.toml and start (or adopt) its recipe.
|
|
950
988
|
if (dashdash === -1) {
|
|
@@ -953,7 +991,7 @@ const serviceRun = async (config, args) => {
|
|
|
953
991
|
if (name === undefined)
|
|
954
992
|
return fail(usage);
|
|
955
993
|
const prefix = positionals.length > 1 ? positionals[0] : undefined;
|
|
956
|
-
const session = await resolveLiveSession(config, prefix);
|
|
994
|
+
const session = await resolveLiveSession(config, prefix, "service run");
|
|
957
995
|
const recipes = await api(config, "GET", `/sessions/${session.id}/recipes`);
|
|
958
996
|
const recipe = recipes.find((entry) => entry.name === name);
|
|
959
997
|
if (recipe === undefined) {
|
|
@@ -996,7 +1034,7 @@ const serviceRun = async (config, args) => {
|
|
|
996
1034
|
if (protocol === "udp" && browserScheme !== null)
|
|
997
1035
|
return fail(usage);
|
|
998
1036
|
const prefix = head.find((a, i) => !a.startsWith("--") && i !== portFlag + 1 && i !== nameFlag + 1);
|
|
999
|
-
const session = await resolveLiveSession(config, prefix);
|
|
1037
|
+
const session = await resolveLiveSession(config, prefix, "service run");
|
|
1000
1038
|
const service = await withSpinner(protocol === "udp"
|
|
1001
1039
|
? `starting ${name ?? argv[0]} (udp :${port})…`
|
|
1002
1040
|
: `starting ${name ?? argv[0]} — waiting for :${port} to answer…`, mutateService(config, "POST", `/sessions/${session.id}/services/run`, {
|
|
@@ -1019,7 +1057,7 @@ const serviceLogs = async (config, args) => {
|
|
|
1019
1057
|
const from = fromFlag === -1 ? "0" : args[fromFlag + 1];
|
|
1020
1058
|
const needle = args.find((argument, index) => !argument.startsWith("--") && index !== fromFlag + 1);
|
|
1021
1059
|
if (needle === undefined || from === undefined || !/^(0|[1-9]\d*)$/.test(from)) {
|
|
1022
|
-
return fail("
|
|
1060
|
+
return fail(usageOf("service logs"));
|
|
1023
1061
|
}
|
|
1024
1062
|
const everything = await fetchServiceViews(config, true);
|
|
1025
1063
|
const matches = everything.filter((view) => view.service.name === needle ||
|
|
@@ -1075,7 +1113,7 @@ const serviceLogs = async (config, args) => {
|
|
|
1075
1113
|
const serviceRestart = async (config, args) => {
|
|
1076
1114
|
const needle = args.find((a) => !a.startsWith("--"));
|
|
1077
1115
|
if (needle === undefined)
|
|
1078
|
-
return fail("
|
|
1116
|
+
return fail(usageOf("service restart"));
|
|
1079
1117
|
const service = await findLiveService(config, needle);
|
|
1080
1118
|
const restarted = await withSpinner(`restarting ${service.label ?? service.id.slice(0, 8)}…`, mutateService(config, "POST", `/services/${service.id}/restart`));
|
|
1081
1119
|
say(`${green("✓")} restarted · ${restarted.status}`);
|
|
@@ -1321,7 +1359,7 @@ const accountsCommand = async (config) => {
|
|
|
1321
1359
|
const connectCommand = async (config, args) => {
|
|
1322
1360
|
const [providerArg, ...flags] = args;
|
|
1323
1361
|
if (!isProvider(providerArg)) {
|
|
1324
|
-
return fail("
|
|
1362
|
+
return fail(usageOf("connect"));
|
|
1325
1363
|
}
|
|
1326
1364
|
const provider = providerArg;
|
|
1327
1365
|
if (flags.includes("--remove")) {
|
|
@@ -1554,7 +1592,7 @@ const keysCommand = async (config, args) => {
|
|
|
1554
1592
|
case undefined:
|
|
1555
1593
|
return keysShow(config);
|
|
1556
1594
|
default:
|
|
1557
|
-
return fail(`unknown keys command "${verb}"
|
|
1595
|
+
return fail(`unknown keys command "${verb}" · mend help keys lists them`);
|
|
1558
1596
|
}
|
|
1559
1597
|
};
|
|
1560
1598
|
const formatDotfileBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -1717,14 +1755,14 @@ const envCluster = async (config, args) => {
|
|
|
1717
1755
|
const explicitProject = takeFlagValue(args, "--project");
|
|
1718
1756
|
const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
|
|
1719
1757
|
const [verb, ...rest] = positional;
|
|
1720
|
-
const usage = "
|
|
1758
|
+
const usage = usageOf("env cluster");
|
|
1721
1759
|
const project = await findProject(config, explicitProject);
|
|
1722
1760
|
const route = `/projects/${project.id}/cluster-bindings`;
|
|
1723
1761
|
const appliesLine = () => say(dim(" applies from the next workspace launch; running workspaces keep what they started with"));
|
|
1724
1762
|
if (verb === "add") {
|
|
1725
1763
|
const [kind, objectName] = rest;
|
|
1726
1764
|
if ((kind !== "secret" && kind !== "configmap") || objectName === undefined) {
|
|
1727
|
-
return fail(`env cluster add takes a kind and an object name
|
|
1765
|
+
return fail(`env cluster add takes a kind and an object name\n${usage}`);
|
|
1728
1766
|
}
|
|
1729
1767
|
const result = await api(config, "POST", route, {
|
|
1730
1768
|
kind,
|
|
@@ -1738,7 +1776,7 @@ const envCluster = async (config, args) => {
|
|
|
1738
1776
|
const [ref] = rest;
|
|
1739
1777
|
const [kind, objectName] = ref?.split("/", 2) ?? [];
|
|
1740
1778
|
if ((kind !== "secret" && kind !== "configmap") || objectName === undefined) {
|
|
1741
|
-
return fail(`env cluster remove takes <kind>/<name>, e.g. secret/app-env
|
|
1779
|
+
return fail(`env cluster remove takes <kind>/<name>, e.g. secret/app-env\n${usage}`);
|
|
1742
1780
|
}
|
|
1743
1781
|
const snapshot = await api(config, "GET", route);
|
|
1744
1782
|
const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
|
|
@@ -1752,7 +1790,7 @@ const envCluster = async (config, args) => {
|
|
|
1752
1790
|
const clear = args.includes("--clear");
|
|
1753
1791
|
const [name] = rest;
|
|
1754
1792
|
if (!clear && name === undefined)
|
|
1755
|
-
return fail(`env cluster sa takes a name or --clear
|
|
1793
|
+
return fail(`env cluster sa takes a name or --clear\n${usage}`);
|
|
1756
1794
|
const result = await api(config, "PUT", `${route}/service-account`, { serviceAccount: clear ? null : name });
|
|
1757
1795
|
say(result.serviceAccount === null
|
|
1758
1796
|
? `${green("✓")} cleared the workspace service account ${dim(`· cluster r${result.revision}`)}`
|
|
@@ -1762,7 +1800,7 @@ const envCluster = async (config, args) => {
|
|
|
1762
1800
|
}
|
|
1763
1801
|
return appliesLine();
|
|
1764
1802
|
}
|
|
1765
|
-
return fail(`unknown env cluster command "${verb ?? ""}"
|
|
1803
|
+
return fail(`unknown env cluster command "${verb ?? ""}"\n${usage}`);
|
|
1766
1804
|
};
|
|
1767
1805
|
const envCommand = async (config, args) => {
|
|
1768
1806
|
const [verb, ...rest] = args;
|
|
@@ -1775,7 +1813,7 @@ const envCommand = async (config, args) => {
|
|
|
1775
1813
|
case undefined:
|
|
1776
1814
|
return envShow(config, rest);
|
|
1777
1815
|
default:
|
|
1778
|
-
return fail(`unknown env command "${verb}"
|
|
1816
|
+
return fail(`unknown env command "${verb}" · mend help env lists them`);
|
|
1779
1817
|
}
|
|
1780
1818
|
};
|
|
1781
1819
|
const dotfilesCommand = async (config, args) => {
|
|
@@ -1787,7 +1825,7 @@ const dotfilesCommand = async (config, args) => {
|
|
|
1787
1825
|
case undefined:
|
|
1788
1826
|
return dotfilesShow(config);
|
|
1789
1827
|
default:
|
|
1790
|
-
return fail(`unknown dotfiles command "${verb}"
|
|
1828
|
+
return fail(`unknown dotfiles command "${verb}" · mend help dotfiles lists them`);
|
|
1791
1829
|
}
|
|
1792
1830
|
};
|
|
1793
1831
|
/** With `--project` the project's library; bare, your own. */
|
|
@@ -1856,7 +1894,7 @@ const skillsCommand = async (config, args) => {
|
|
|
1856
1894
|
// Bare flags (`mend skills --project web`) read as the list.
|
|
1857
1895
|
if (verb.startsWith("--"))
|
|
1858
1896
|
return skillsList(config, args);
|
|
1859
|
-
return fail(`unknown skills command "${verb}"
|
|
1897
|
+
return fail(`unknown skills command "${verb}" · mend help skills lists them`);
|
|
1860
1898
|
}
|
|
1861
1899
|
};
|
|
1862
1900
|
// ─── completions: live sessions under TAB ───────────────────────────────────
|
|
@@ -1941,7 +1979,7 @@ const completionsCommand = (args) => {
|
|
|
1941
1979
|
process.stdout.write(BASH_COMPLETIONS);
|
|
1942
1980
|
return;
|
|
1943
1981
|
default:
|
|
1944
|
-
return fail(
|
|
1982
|
+
return fail(`${usageOf("completions")} · e.g. mend completions zsh > "$fpath[1]/_mend"`);
|
|
1945
1983
|
}
|
|
1946
1984
|
};
|
|
1947
1985
|
// ─── supervised run: platform workspace + PTY + record ──────────────────────
|
|
@@ -2482,7 +2520,7 @@ const hasNodeFfi = () => {
|
|
|
2482
2520
|
*/
|
|
2483
2521
|
const dashboard = async (config) => {
|
|
2484
2522
|
if (process.stdout.isTTY !== true) {
|
|
2485
|
-
say(
|
|
2523
|
+
say(renderIndex());
|
|
2486
2524
|
return;
|
|
2487
2525
|
}
|
|
2488
2526
|
if (!hasNodeFfi()) {
|
|
@@ -2503,98 +2541,54 @@ const dashboard = async (config) => {
|
|
|
2503
2541
|
});
|
|
2504
2542
|
};
|
|
2505
2543
|
// ─── entry ──────────────────────────────────────────────────────────────────
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
named ones, e.g. DATABASE_URL) to secrets
|
|
2545
|
-
mend env show what the project store holds — names only, never values
|
|
2546
|
-
mend accounts your connected accounts on the platform (claude, codex, github)
|
|
2547
|
-
mend dotfiles your dotfiles on the server: repo + synced home files
|
|
2548
|
-
mend dotfiles sync [--all | paths…] capture config files from THIS machine into your store
|
|
2549
|
-
mend skills [--project [p]] your skill library on the server (or a project's)
|
|
2550
|
-
mend skills push [--project [p]] [--prune] [--dir <path>]
|
|
2551
|
-
upload ~/.agents/skills bundles into the library; sessions
|
|
2552
|
-
receive them at launch (--prune removes what's gone)
|
|
2553
|
-
mend keys share relay THIS machine's ssh-agent to the server (bridge mode:
|
|
2554
|
-
hardware keys sign here; Ctrl-C stops sharing)
|
|
2555
|
-
mend run -- <command...> same, with an arbitrary command
|
|
2556
|
-
mend worktrees [--project <p>] [--json]
|
|
2557
|
-
every worktree and the sessions inside it (mend sessions
|
|
2558
|
-
--json stays the v1 flat list; --json=v2 groups like this)
|
|
2559
|
-
mend attach <session-id-prefix> reattach this terminal to a running session
|
|
2560
|
-
mend stop <session-id-prefix> | --all [--project <p>]
|
|
2561
|
-
stop the agent — the workspace harvests and closes; the
|
|
2562
|
-
record and review remain
|
|
2563
|
-
mend shell [session-id-prefix] open a shell in a live session's workspace
|
|
2564
|
-
mend service run [session] --port <p> [--name <n>] [--udp] -- <command...>
|
|
2565
|
-
start + supervise a server in the session workspace
|
|
2566
|
-
mend service run [session] <name> start a declared Service (mend.toml recipe)
|
|
2567
|
-
mend service <name> shorthand for the above
|
|
2568
|
-
mend service init [--yes] scaffold mend.toml from package.json + compose ports
|
|
2569
|
-
mend service add [session] <port> [--name <n>] [--udp]
|
|
2570
|
-
adopt a listening workspace port — reachable on this machine
|
|
2571
|
-
mend service connect [name…] [--port <p>]
|
|
2572
|
-
bring live Services to THIS machine's loopback — each
|
|
2573
|
-
connection tunnels through the server, authenticated as you
|
|
2574
|
-
mend service list every live service and its observed state
|
|
2575
|
-
mend service logs <name-or-id> follow a supervised service's output (replay, then live)
|
|
2576
|
-
mend service restart <name-or-id> re-run its recorded command — same URL
|
|
2577
|
-
mend service stop <name-or-id> stop a service (closes its host port)
|
|
2578
|
-
mend continue [session-id] resume a session with its pending review follow-up
|
|
2579
|
-
mend resume [session-id] [--with h] rejoin a settled session (state restored; --with switches harness)
|
|
2580
|
-
mend rejoin [session-id] [--harness h] attach if live, otherwise resume; newest live wins
|
|
2581
|
-
mend refresh [project] fetch origin's branches into the store — new sessions
|
|
2582
|
-
base on current tips (default: the cwd's project)
|
|
2583
|
-
mend projects adopted projects and their live sessions
|
|
2584
|
-
mend sessions [--all] [--project p] [--json]
|
|
2585
|
-
sessions with review facts; JSON is stable for integrations
|
|
2586
|
-
mend status active sessions (alias of mend sessions)
|
|
2587
|
-
mend ssh workspace SSH status: gateway, registered keys, ssh config
|
|
2588
|
-
mend ssh setup [--key <path>] make this machine ready once: offer a key (ssh-agent
|
|
2589
|
-
preferred — nothing new created), write Host mend-ws
|
|
2590
|
-
mend completions zsh|bash print the TAB-completion hook (live session ids under TAB)
|
|
2591
|
-
|
|
2592
|
-
server: MEND_URL (default http://localhost:3105) · auth: MEND_TOKEN
|
|
2593
|
-
detach key: Ctrl+] (set MEND_DETACH_KEY=none when an outer multiplexer owns detaching)
|
|
2594
|
-
config file: ~/.config/mend/cli.json { "url": ..., "token": ..., "deviceId": ... }
|
|
2595
|
-
`;
|
|
2544
|
+
/** `mend help [command...]`: the index, a group, or one page. */
|
|
2545
|
+
const helpCommand = (words) => {
|
|
2546
|
+
if (words.length === 0) {
|
|
2547
|
+
say(renderIndex());
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
const doc = findCommand(words);
|
|
2551
|
+
if (doc !== null && (doc.name.split(" ").length > 1 || words.length === 1)) {
|
|
2552
|
+
say(renderCommand(doc));
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
const group = renderGroup(words[0] ?? "");
|
|
2556
|
+
if (doc !== null) {
|
|
2557
|
+
say(renderCommand(doc));
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (group !== null) {
|
|
2561
|
+
say(group);
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
return fail(`no command "${words.join(" ")}" · mend help lists them`);
|
|
2565
|
+
};
|
|
2566
|
+
/** `mend man [command...]`: the same page through man(1), or the text page without it. */
|
|
2567
|
+
const manCommand = (words) => {
|
|
2568
|
+
const doc = words.length === 0 ? null : findCommand(words);
|
|
2569
|
+
if (words.length > 0 && doc === null) {
|
|
2570
|
+
return fail(`no command "${words.join(" ")}" · mend help lists them`);
|
|
2571
|
+
}
|
|
2572
|
+
const version = cliVersion();
|
|
2573
|
+
const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
|
|
2574
|
+
const file = path.join(os.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
|
|
2575
|
+
fs.writeFileSync(file, page);
|
|
2576
|
+
const result = spawnSync("man", ["-l", file], { stdio: "inherit" });
|
|
2577
|
+
fs.rmSync(file, { force: true });
|
|
2578
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
2579
|
+
say(doc === null ? renderIndex() : renderCommand(doc));
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2596
2582
|
const main = async () => {
|
|
2597
2583
|
const [command, ...rest] = process.argv.slice(2);
|
|
2584
|
+
// `mend <command> --help` (or -h) before the separator is that command's page,
|
|
2585
|
+
// never an argument: `mend run -- cmd --help` keeps its --help for cmd.
|
|
2586
|
+
const ownArgs = rest.slice(0, rest.indexOf("--") === -1 ? rest.length : rest.indexOf("--"));
|
|
2587
|
+
if (command !== undefined &&
|
|
2588
|
+
command !== "help" &&
|
|
2589
|
+
ownArgs.some((a) => a === "--help" || a === "-h")) {
|
|
2590
|
+
return helpCommand([command, ...ownArgs.filter((a) => !a.startsWith("-"))]);
|
|
2591
|
+
}
|
|
2598
2592
|
const config = loadConfig();
|
|
2599
2593
|
switch (command) {
|
|
2600
2594
|
case "adopt":
|
|
@@ -2628,7 +2622,7 @@ const main = async () => {
|
|
|
2628
2622
|
return connectCommand(config, rest);
|
|
2629
2623
|
case "pair":
|
|
2630
2624
|
return pairCommand(rest, boundApi(config));
|
|
2631
|
-
//
|
|
2625
|
+
// Hidden in the catalog: the installer renders its own QR through this.
|
|
2632
2626
|
case "qr":
|
|
2633
2627
|
return qrCommand(rest);
|
|
2634
2628
|
case "doctor":
|
|
@@ -2661,10 +2655,18 @@ const main = async () => {
|
|
|
2661
2655
|
return dashboard(config);
|
|
2662
2656
|
case "help":
|
|
2663
2657
|
case "--help":
|
|
2664
|
-
|
|
2658
|
+
case "-h":
|
|
2659
|
+
return helpCommand(rest);
|
|
2660
|
+
case "man":
|
|
2661
|
+
return manCommand(rest);
|
|
2662
|
+
case "version":
|
|
2663
|
+
case "--version":
|
|
2664
|
+
case "-v":
|
|
2665
|
+
for (const line of versionLines(cliVersion(), await fetchServerVersion(config.url)))
|
|
2666
|
+
say(line);
|
|
2665
2667
|
return;
|
|
2666
2668
|
default:
|
|
2667
|
-
return fail(`unknown command "${command}"
|
|
2669
|
+
return fail(`unknown command "${command}" · mend help lists them`);
|
|
2668
2670
|
}
|
|
2669
2671
|
};
|
|
2670
2672
|
await main();
|
package/dist/version.js
ADDED
|
@@ -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.18.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)
|
package/man/mend-adopt.1
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.TH MEND-ADOPT 1 "" "mend 0.18.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.18.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)
|