@sporhq/spor 0.2.9 → 0.3.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/.claude-plugin/plugin.json +1 -1
- package/bin/spor.js +485 -192
- package/package.json +1 -1
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"name": "spor",
|
|
3
3
|
"displayName": "Spor Context Compiler",
|
|
4
4
|
"description": "Maintains a typed, versioned knowledge graph and compiles compact briefings from it: session-start injection, per-prompt relevance digests, capture at discovery, end-of-session distillation, decision queue.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.0",
|
|
6
6
|
"author": { "name": "losthammer" }
|
|
7
7
|
}
|
package/bin/spor.js
CHANGED
|
@@ -22,65 +22,23 @@ const os = require("os");
|
|
|
22
22
|
const path = require("path");
|
|
23
23
|
const https = require("https");
|
|
24
24
|
const { spawnSync } = require("child_process");
|
|
25
|
+
const { parseArgs } = require("util");
|
|
25
26
|
|
|
26
27
|
const ROOT = path.resolve(__dirname, "..");
|
|
27
28
|
const { loadConfig } = require(path.join(ROOT, "lib", "config.js"));
|
|
28
29
|
const remote = require(path.join(ROOT, "lib", "remote.js"));
|
|
29
30
|
const u = require(path.join(ROOT, "scripts", "engines", "util.js"));
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
copilot cursor (no host => list detected). --scope
|
|
39
|
-
user|repo, --all, --print, --server/--token to configure
|
|
40
|
-
upgrade [host...] refresh wired spor to the installed package version after
|
|
41
|
-
an npm bump (claude: marketplace+plugin update). No host
|
|
42
|
-
=> every detected wired host. Also flags a newer release
|
|
43
|
-
published to npm. --scope user|repo, --print, --no-net
|
|
44
|
-
status resolved mode, graph, project, identity, health
|
|
45
|
-
join <url> <token> point the client at a graph (writes user config)
|
|
46
|
-
migrate <url> push the local graph to a remote you own (solo-remote)
|
|
47
|
-
whoami who the team graph thinks you are (remote)
|
|
48
|
-
|
|
49
|
-
Team admin (remote, admin token)
|
|
50
|
-
invite --person <id> | --name <n> --email <e> mint a teammate token
|
|
51
|
-
token list | token revoke <prefix> manage tokens
|
|
52
|
-
|
|
53
|
-
Graph
|
|
54
|
-
add "<text>" capture a node (local: typed file; remote: /v1/capture)
|
|
55
|
-
next [--project S] the decision queue (local: lib/queue; remote: /v1/queue)
|
|
56
|
-
get <id> a node by id (local: file; remote: /v1/nodes/<id>)
|
|
57
|
-
lens [<id>] render a saved view (remote). No id => list the lens
|
|
58
|
-
catalog; <id> renders it. Flags: --format text|json
|
|
59
|
-
(default text), --PARAM VALUE to pass lens params,
|
|
60
|
-
--json (machine output of the catalog/JSON tree)
|
|
61
|
-
|
|
62
|
-
Repo scoping
|
|
63
|
-
disable | enable turn Spor off/on for this repo (.spor.json)
|
|
64
|
-
link <slug> set this repo's canonical project slug (.spor marker)
|
|
65
|
-
compile <args> full neighborhood / digest (local; byte-identical)
|
|
66
|
-
brief <id> compile a briefing for a node (alias: compile --root <id>)
|
|
67
|
-
validate lint the local graph (byte-identical)
|
|
68
|
-
|
|
69
|
-
Dispatch (Claude Code background agents)
|
|
70
|
-
dispatch "<task>" compile a briefing + launch 'claude --bg' in the repo.
|
|
71
|
-
Also: dispatch <node-id> | --node <id> | --from-queue |
|
|
72
|
-
--backfill. Flags: --dir P, --full, --no-brief, --model M,
|
|
73
|
-
--permission-mode P, --agent A, --name N, --print,
|
|
74
|
-
--template F (own prompt; {{brief}}/{{task}}/{{node}}/
|
|
75
|
-
{{title}}/{{slug}}/{{dir}}/{{default}} placeholders)
|
|
76
|
-
repos show the local slug->repo-dir map used to pick the
|
|
77
|
-
directory (repos add <slug> <path> | repos rm <slug>)
|
|
78
|
-
|
|
79
|
-
Other
|
|
80
|
-
cost [--since D] LLM spend summary from journal/llm-calls (local)
|
|
81
|
-
version print version
|
|
82
|
-
help this message
|
|
32
|
+
// The CLI surface is a single declarative table (COMMANDS, defined below): it is
|
|
33
|
+
// the one source of truth for dispatch, flag parsing (Node's built-in
|
|
34
|
+
// util.parseArgs), and help — top-level AND per-command (`spor <verb> --help`).
|
|
35
|
+
// Adding a verb or a flag means editing one table entry; the help can't drift
|
|
36
|
+
// from the parser because both read the same spec. The header/footer frame the
|
|
37
|
+
// generated top-level listing.
|
|
38
|
+
const HELP_HEADER = `spor — Spor client CLI
|
|
83
39
|
|
|
40
|
+
Usage: spor <command> [args]`;
|
|
41
|
+
const HELP_FOOTER = `Run 'spor <command> --help' for a command's flags and detail.
|
|
84
42
|
Mode is set by config/env (SPOR_SERVER ⇒ remote). See 'spor status'.`;
|
|
85
43
|
|
|
86
44
|
// A consumer that closes the pipe early (`spor next | head`) makes stdout emit
|
|
@@ -268,8 +226,8 @@ function renderQueue(q) {
|
|
|
268
226
|
}
|
|
269
227
|
}
|
|
270
228
|
|
|
271
|
-
async function cmdGet(cfg,
|
|
272
|
-
const id =
|
|
229
|
+
async function cmdGet(cfg, { positionals }) {
|
|
230
|
+
const id = positionals[0];
|
|
273
231
|
if (!id) {
|
|
274
232
|
err("usage: spor get <id>");
|
|
275
233
|
return 1;
|
|
@@ -429,13 +387,13 @@ function today() {
|
|
|
429
387
|
return new Date().toISOString().slice(0, 10);
|
|
430
388
|
}
|
|
431
389
|
|
|
432
|
-
async function cmdAdd(cfg,
|
|
433
|
-
const prose =
|
|
390
|
+
async function cmdAdd(cfg, { values, positionals }) {
|
|
391
|
+
const prose = positionals[0];
|
|
434
392
|
if (!prose) {
|
|
435
393
|
err('usage: spor add "<text>" [--type T] [--title ...] [--project S]');
|
|
436
394
|
return 1;
|
|
437
395
|
}
|
|
438
|
-
const project =
|
|
396
|
+
const project = values.project || safeSlug();
|
|
439
397
|
|
|
440
398
|
if (cfg.mode() === "remote") {
|
|
441
399
|
const r = await remote.post(cfg, "/v1/capture", { text: prose, context: { project } });
|
|
@@ -459,8 +417,8 @@ async function cmdAdd(cfg, args) {
|
|
|
459
417
|
err(`no graph at ${nodesDir} — run 'spor init' first`);
|
|
460
418
|
return 1;
|
|
461
419
|
}
|
|
462
|
-
const type =
|
|
463
|
-
const title =
|
|
420
|
+
const type = values.type || "task";
|
|
421
|
+
const title = values.title || prose.split(/\s+/).slice(0, 10).join(" ");
|
|
464
422
|
const summary = prose.length > 500 ? prose.slice(0, 497) + "..." : prose;
|
|
465
423
|
|
|
466
424
|
let g;
|
|
@@ -472,7 +430,7 @@ async function cmdAdd(cfg, args) {
|
|
|
472
430
|
}
|
|
473
431
|
const prefixes = (g.registry && g.registry.prefixesFor(type)) || null;
|
|
474
432
|
const prefix = prefixes && prefixes[0] ? prefixes[0] : `${type}-`;
|
|
475
|
-
let id =
|
|
433
|
+
let id = values.id || `${prefix}${kebab(title) || today()}`;
|
|
476
434
|
// uniquify against existing files
|
|
477
435
|
let n = 1;
|
|
478
436
|
let base = id;
|
|
@@ -520,10 +478,9 @@ function writeServerToken(home, server, token) {
|
|
|
520
478
|
// Write server+token to USER config (never a committable repo config), then
|
|
521
479
|
// confirm immediately — the upgrade research found no one-step way to point a
|
|
522
480
|
// client at a graph and know it took.
|
|
523
|
-
async function cmdJoin(cfg,
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
const token = optVal(args, "token") || positional[1];
|
|
481
|
+
async function cmdJoin(cfg, { values, positionals }) {
|
|
482
|
+
const server = values.server || positionals[0];
|
|
483
|
+
const token = values.token || positionals[1];
|
|
527
484
|
if (!server) {
|
|
528
485
|
err("usage: spor join <server-url> <token>");
|
|
529
486
|
return 1;
|
|
@@ -556,15 +513,15 @@ function notAdminHint(r) {
|
|
|
556
513
|
return false;
|
|
557
514
|
}
|
|
558
515
|
|
|
559
|
-
async function cmdInvite(cfg,
|
|
516
|
+
async function cmdInvite(cfg, { values }) {
|
|
560
517
|
if (cfg.mode() !== "remote") {
|
|
561
518
|
err("invite needs a team graph — set SPOR_SERVER/SPOR_TOKEN (see 'spor join').");
|
|
562
519
|
return 1;
|
|
563
520
|
}
|
|
564
|
-
let person =
|
|
565
|
-
const name =
|
|
566
|
-
const email =
|
|
567
|
-
const expires =
|
|
521
|
+
let person = values.person;
|
|
522
|
+
const name = values.name;
|
|
523
|
+
const email = values.email;
|
|
524
|
+
const expires = values.expires;
|
|
568
525
|
|
|
569
526
|
// create the person node first when only name/email is given (the mint
|
|
570
527
|
// endpoint binds to an EXISTING node, it cannot conjure a subject).
|
|
@@ -574,7 +531,7 @@ async function cmdInvite(cfg, args) {
|
|
|
574
531
|
err(" or: spor invite --name <name> --email <email> [--id person-x] [--expires <Nd>]");
|
|
575
532
|
return 1;
|
|
576
533
|
}
|
|
577
|
-
person =
|
|
534
|
+
person = values.id || `person-${kebab(name)}`;
|
|
578
535
|
const md = `---\nid: ${person}\ntype: person\ntitle: ${name.replace(/\n/g, " ")}\nsummary: Team member ${name}.\nemail: ${email}\ndate: ${today()}\n---\n\nTeam member ${name} <${email}>.\n`;
|
|
579
536
|
const pr = await remote.post(cfg, "/v1/nodes", { nodes: [{ node: md, if_exists: "skip" }] });
|
|
580
537
|
if (pr.transport) {
|
|
@@ -694,7 +651,7 @@ function hasGit() {
|
|
|
694
651
|
// route for BYO-repo registration, and the GitHub-App write grant of
|
|
695
652
|
// dec-spor-solo-remote-write-credential-custody is unbuilt server-side; both
|
|
696
653
|
// are tracked separately, so this verb stops at "your graph is on your remote".
|
|
697
|
-
function cmdMigrate(cfg,
|
|
654
|
+
function cmdMigrate(cfg, { positionals }) {
|
|
698
655
|
const home = cfg.graphHome();
|
|
699
656
|
const nodesDir = cfg.nodesDir();
|
|
700
657
|
if (!fs.existsSync(nodesDir)) {
|
|
@@ -732,7 +689,7 @@ function cmdMigrate(cfg, args) {
|
|
|
732
689
|
}
|
|
733
690
|
// 3. wire the remote. An explicit URL sets/updates origin; otherwise reuse an
|
|
734
691
|
// existing origin, or explain that one is required.
|
|
735
|
-
const url =
|
|
692
|
+
const url = positionals[0];
|
|
736
693
|
const haveOrigin = git(home, ["remote", "get-url", "origin"]).status === 0;
|
|
737
694
|
if (url) {
|
|
738
695
|
const r = haveOrigin ? git(home, ["remote", "set-url", "origin", url]) : git(home, ["remote", "add", "origin", url]);
|
|
@@ -780,8 +737,8 @@ function cmdScope(enabled) {
|
|
|
780
737
|
// --- spor link <slug>: write the .spor identity marker --------------------
|
|
781
738
|
// Fixes a wrong inferred slug (basename != canonical) deterministically,
|
|
782
739
|
// instead of waiting for the server's fingerprint-alias proposal to be approved.
|
|
783
|
-
function cmdLink(
|
|
784
|
-
const slug =
|
|
740
|
+
function cmdLink(cfg, { positionals }) {
|
|
741
|
+
const slug = positionals[0] || safeSlug();
|
|
785
742
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
786
743
|
err(`invalid slug '${slug}' — must match ^[a-z0-9][a-z0-9-]*$`);
|
|
787
744
|
return 1;
|
|
@@ -982,20 +939,6 @@ async function npmLatest(timeoutMs = 4000) {
|
|
|
982
939
|
});
|
|
983
940
|
}
|
|
984
941
|
|
|
985
|
-
// Options that consume the following token, so positional parsing can skip it.
|
|
986
|
-
const INSTALL_VALUE_OPTS = new Set(["scope", "server", "token"]);
|
|
987
|
-
function positionals(args) {
|
|
988
|
-
const pos = [];
|
|
989
|
-
for (let i = 0; i < args.length; i++) {
|
|
990
|
-
const a = args[i];
|
|
991
|
-
if (a.startsWith("--")) {
|
|
992
|
-
if (INSTALL_VALUE_OPTS.has(a.slice(2))) i++; // skip its value
|
|
993
|
-
continue;
|
|
994
|
-
}
|
|
995
|
-
pos.push(a);
|
|
996
|
-
}
|
|
997
|
-
return pos;
|
|
998
|
-
}
|
|
999
942
|
|
|
1000
943
|
function deepReplace(v, from, to) {
|
|
1001
944
|
if (typeof v === "string") return v.split(from).join(to);
|
|
@@ -1171,8 +1114,8 @@ function installPluginHost(spec, scope, dryRun) {
|
|
|
1171
1114
|
return 0;
|
|
1172
1115
|
}
|
|
1173
1116
|
|
|
1174
|
-
async function cmdInstall(cfg,
|
|
1175
|
-
const dryRun =
|
|
1117
|
+
async function cmdInstall(cfg, { values, positionals: pos }) {
|
|
1118
|
+
const dryRun = !!(values.print || values["dry-run"]);
|
|
1176
1119
|
// Node prerequisite (issue-spor-onboarding-no-node-silent-fail-open). The
|
|
1177
1120
|
// hooks fail open on a box with no/old Node (every one silently no-ops), so
|
|
1178
1121
|
// make the requirement loud HERE, at wire-up time. A too-old interpreter is a
|
|
@@ -1183,25 +1126,24 @@ async function cmdInstall(cfg, args) {
|
|
|
1183
1126
|
err(` Spor's hooks fail open, so on this Node they install but every hook silently no-ops — upgrade Node first.`);
|
|
1184
1127
|
return 1;
|
|
1185
1128
|
}
|
|
1186
|
-
let scope =
|
|
1129
|
+
let scope = values.scope || "user";
|
|
1187
1130
|
if (scope === "project") scope = "repo";
|
|
1188
1131
|
if (scope !== "user" && scope !== "repo") {
|
|
1189
1132
|
err(`invalid --scope '${scope}' — use 'user' or 'repo'`);
|
|
1190
1133
|
return 1;
|
|
1191
1134
|
}
|
|
1192
1135
|
|
|
1193
|
-
const pos = positionals(args);
|
|
1194
1136
|
const bad = pos.find((a) => !HOSTS[a]);
|
|
1195
1137
|
if (bad) {
|
|
1196
1138
|
err(`unknown host '${bad}' — known: ${Object.keys(HOSTS).join(", ")}`);
|
|
1197
1139
|
return 1;
|
|
1198
1140
|
}
|
|
1199
1141
|
let hosts = pos.slice();
|
|
1200
|
-
if (
|
|
1142
|
+
if (values.all) hosts = detectHosts();
|
|
1201
1143
|
|
|
1202
1144
|
// The "configure" half: persist server/token to user config when given.
|
|
1203
|
-
const server =
|
|
1204
|
-
const token =
|
|
1145
|
+
const server = values.server;
|
|
1146
|
+
const token = values.token;
|
|
1205
1147
|
if ((server || token) && !dryRun) {
|
|
1206
1148
|
try {
|
|
1207
1149
|
const f = writeServerToken(cfg.userConfigHome(), server, token);
|
|
@@ -1301,15 +1243,14 @@ function hostHasSpor(host, scope) {
|
|
|
1301
1243
|
}
|
|
1302
1244
|
}
|
|
1303
1245
|
|
|
1304
|
-
async function cmdUpgrade(cfg,
|
|
1305
|
-
const dryRun =
|
|
1306
|
-
let scope =
|
|
1246
|
+
async function cmdUpgrade(cfg, { values, positionals: pos }) {
|
|
1247
|
+
const dryRun = !!(values.print || values["dry-run"]);
|
|
1248
|
+
let scope = values.scope || "user";
|
|
1307
1249
|
if (scope === "project") scope = "repo";
|
|
1308
1250
|
if (scope !== "user" && scope !== "repo") {
|
|
1309
1251
|
err(`invalid --scope '${scope}' — use 'user' or 'repo'`);
|
|
1310
1252
|
return 1;
|
|
1311
1253
|
}
|
|
1312
|
-
const pos = positionals(args);
|
|
1313
1254
|
const bad = pos.find((a) => !HOSTS[a]);
|
|
1314
1255
|
if (bad) {
|
|
1315
1256
|
err(`unknown host '${bad}' — known: ${Object.keys(HOSTS).join(", ")}`);
|
|
@@ -1341,7 +1282,7 @@ async function cmdUpgrade(cfg, args) {
|
|
|
1341
1282
|
// The refresh above closes the loaded-vs-installed gap; this closes the
|
|
1342
1283
|
// installed-vs-published one — if npm has a newer release, the package on
|
|
1343
1284
|
// disk itself is behind, so point the user at the npm bump (then re-upgrade).
|
|
1344
|
-
if (!
|
|
1285
|
+
if (!values["no-net"]) {
|
|
1345
1286
|
const latest = await npmLatest();
|
|
1346
1287
|
const installed = version();
|
|
1347
1288
|
if (latest && verCmp(installed, latest) < 0) {
|
|
@@ -1503,36 +1444,26 @@ function onboardRepo(cfg, dir) {
|
|
|
1503
1444
|
}
|
|
1504
1445
|
}
|
|
1505
1446
|
|
|
1506
|
-
async function cmdDispatch(cfg,
|
|
1507
|
-
const dryRun =
|
|
1508
|
-
const full =
|
|
1509
|
-
const noBrief =
|
|
1510
|
-
const backfill =
|
|
1511
|
-
const fromQueue =
|
|
1512
|
-
const dirOpt =
|
|
1513
|
-
const model =
|
|
1514
|
-
const permMode =
|
|
1515
|
-
const agent =
|
|
1447
|
+
async function cmdDispatch(cfg, { values, positionals: pos }) {
|
|
1448
|
+
const dryRun = !!(values.print || values["dry-run"]);
|
|
1449
|
+
const full = !!values.full;
|
|
1450
|
+
const noBrief = !!values["no-brief"];
|
|
1451
|
+
const backfill = !!values.backfill;
|
|
1452
|
+
const fromQueue = !!values["from-queue"];
|
|
1453
|
+
const dirOpt = values.dir || null;
|
|
1454
|
+
const model = values.model || null;
|
|
1455
|
+
const permMode = values["permission-mode"] || null;
|
|
1456
|
+
const agent = values.agent || null;
|
|
1516
1457
|
// A user-supplied prompt template (task-spor-dispatch-user-prompt-templates):
|
|
1517
1458
|
// --template wins, else a personal default in the config cascade
|
|
1518
1459
|
// (dispatch.template — an absolute path, like dispatch.repos). Empty until we
|
|
1519
1460
|
// resolve the file below, so an absent option leaves the prompt byte-identical.
|
|
1520
|
-
const templateOpt =
|
|
1521
|
-
let nodeId =
|
|
1522
|
-
let targetSlug =
|
|
1523
|
-
let name =
|
|
1524
|
-
|
|
1525
|
-
// Positional task text:
|
|
1526
|
-
const VALUE_OPTS = new Set(["dir", "node", "slug", "model", "permission-mode", "agent", "name", "template"]);
|
|
1527
|
-
const pos = [];
|
|
1528
|
-
for (let i = 0; i < args.length; i++) {
|
|
1529
|
-
const a = args[i];
|
|
1530
|
-
if (a.startsWith("--")) {
|
|
1531
|
-
if (VALUE_OPTS.has(a.slice(2))) i++;
|
|
1532
|
-
continue;
|
|
1533
|
-
}
|
|
1534
|
-
pos.push(a);
|
|
1535
|
-
}
|
|
1461
|
+
const templateOpt = values.template || cfg.get("dispatch.template", null);
|
|
1462
|
+
let nodeId = values.node || null;
|
|
1463
|
+
let targetSlug = values.slug || null;
|
|
1464
|
+
let name = values.name || null;
|
|
1465
|
+
|
|
1466
|
+
// Positional task text: parseArgs already split flags from positionals.
|
|
1536
1467
|
let taskText = pos.join(" ").trim();
|
|
1537
1468
|
|
|
1538
1469
|
// Load the template now (before any briefing compile) so a bad path fails fast.
|
|
@@ -1736,79 +1667,441 @@ function version() {
|
|
|
1736
1667
|
}
|
|
1737
1668
|
}
|
|
1738
1669
|
|
|
1670
|
+
// ---------------------------------------------------------------------------
|
|
1671
|
+
// The command table — the single source of truth for dispatch, parsing, and help
|
|
1672
|
+
// ---------------------------------------------------------------------------
|
|
1673
|
+
// Each entry declares its group (for the top-level listing), a positional usage
|
|
1674
|
+
// hint (`args`), a one-line `summary`, a longer `help` body, an `options` map in
|
|
1675
|
+
// util.parseArgs shape (each carrying a help-only `desc`/`value`), optional
|
|
1676
|
+
// `examples`, and `aliases`. `parse` picks the front-door behavior:
|
|
1677
|
+
// "strict" — central util.parseArgs validates flags (unknown-flag errors with
|
|
1678
|
+
// a suggestion) and the run() gets { values, positionals }.
|
|
1679
|
+
// "raw" — run() gets the raw argv array; for commands that own their parsing
|
|
1680
|
+
// (subcommands like token/repos), forward open --PARAM sets (lens),
|
|
1681
|
+
// or must stay byte-identical passthrough to lib/*.js (compile,
|
|
1682
|
+
// validate, next-local, cost). norm-cc-byte-identical-refactor.
|
|
1683
|
+
// "meta" — listing-only (help/version are intercepted in main before dispatch).
|
|
1684
|
+
const SCOPE_OPT = { type: "string", value: "user|repo", desc: "where to write — 'user' (default) or 'repo' (this checkout)" };
|
|
1685
|
+
const PRINT_OPT = { type: "boolean", desc: "dry run — show what would change, write nothing" };
|
|
1686
|
+
const DRYRUN_OPT = { type: "boolean", desc: "alias for --print" };
|
|
1687
|
+
|
|
1688
|
+
const COMMANDS = {
|
|
1689
|
+
// --- Getting started ---
|
|
1690
|
+
init: {
|
|
1691
|
+
group: "Getting started", parse: "strict", args: "", options: {},
|
|
1692
|
+
summary: "create the local graph home (nodes/, git, .gitignore)",
|
|
1693
|
+
help:
|
|
1694
|
+
"Idempotently create the local graph home: a nodes/ directory, a git repo\n" +
|
|
1695
|
+
"to version it, and a .gitignore for machine-local state. Safe to re-run —\n" +
|
|
1696
|
+
"an existing graph is reported, never clobbered.",
|
|
1697
|
+
run: (cfg) => cmdInit(cfg),
|
|
1698
|
+
},
|
|
1699
|
+
install: {
|
|
1700
|
+
group: "Getting started", parse: "strict", args: "[host...]", aliases: ["setup"],
|
|
1701
|
+
summary: "wire spor into an agent (claude codex gemini opencode copilot cursor)",
|
|
1702
|
+
help:
|
|
1703
|
+
"Wire the spor hooks/plugin into one or more host agents. With no host, lists\n" +
|
|
1704
|
+
"the hosts detected on this machine and touches nothing. Claude Code is wired\n" +
|
|
1705
|
+
"via its plugin CLI; the others receive a merged hooks manifest.\n\n" +
|
|
1706
|
+
"--server/--token also persist remote-graph credentials to your user config.",
|
|
1707
|
+
options: {
|
|
1708
|
+
scope: SCOPE_OPT,
|
|
1709
|
+
all: { type: "boolean", desc: "install into every detected host" },
|
|
1710
|
+
print: PRINT_OPT,
|
|
1711
|
+
"dry-run": DRYRUN_OPT,
|
|
1712
|
+
server: { type: "string", value: "url", desc: "persist a team-graph server URL to user config" },
|
|
1713
|
+
token: { type: "string", value: "tok", desc: "persist an auth token to user config" },
|
|
1714
|
+
},
|
|
1715
|
+
examples: ["spor install claude", "spor install codex gemini --scope repo", "spor install --all --print"],
|
|
1716
|
+
run: (cfg, p) => cmdInstall(cfg, p),
|
|
1717
|
+
},
|
|
1718
|
+
upgrade: {
|
|
1719
|
+
group: "Getting started", parse: "strict", args: "[host...]", aliases: ["update"],
|
|
1720
|
+
summary: "refresh wired spor to the installed package version (after an npm bump)",
|
|
1721
|
+
help:
|
|
1722
|
+
"Refresh wired hosts to the package version on disk. A bumped npm package does\n" +
|
|
1723
|
+
"not change what an agent already loaded — Claude Code runs its own cached copy\n" +
|
|
1724
|
+
"until 'plugin update' swaps it. With no host, refreshes every detected host\n" +
|
|
1725
|
+
"that has spor wired. Also flags a newer release published to npm.",
|
|
1726
|
+
options: { scope: SCOPE_OPT, print: PRINT_OPT, "dry-run": DRYRUN_OPT, "no-net": { type: "boolean", desc: "skip the npm 'newer version published' check" } },
|
|
1727
|
+
examples: ["spor upgrade", "spor upgrade claude --print"],
|
|
1728
|
+
run: (cfg, p) => cmdUpgrade(cfg, p),
|
|
1729
|
+
},
|
|
1730
|
+
status: {
|
|
1731
|
+
group: "Getting started", parse: "strict", args: "", options: {},
|
|
1732
|
+
summary: "resolved mode, graph, project, identity, health",
|
|
1733
|
+
help: "Print the resolved mode (local/remote), graph home, project slug, identity,\nand a health probe. In local mode it also warns of a split-brain claude.ai\nSpor MCP connector; it always surfaces the Node prerequisite line.",
|
|
1734
|
+
run: (cfg) => cmdStatus(cfg),
|
|
1735
|
+
},
|
|
1736
|
+
join: {
|
|
1737
|
+
group: "Getting started", parse: "strict", args: "<url> <token>", aliases: ["login"],
|
|
1738
|
+
summary: "point the client at a graph (writes user config)",
|
|
1739
|
+
help: "Write a team-graph server URL and token to your USER config (never a\ncommittable repo config), then confirm the connection immediately. The URL and\ntoken may be given positionally or as --server/--token.",
|
|
1740
|
+
options: {
|
|
1741
|
+
server: { type: "string", value: "url", desc: "server URL (else the first positional)" },
|
|
1742
|
+
token: { type: "string", value: "tok", desc: "auth token (else the second positional)" },
|
|
1743
|
+
},
|
|
1744
|
+
examples: ["spor join https://graph.example.com tok_abc123"],
|
|
1745
|
+
run: (cfg, p) => cmdJoin(cfg, p),
|
|
1746
|
+
},
|
|
1747
|
+
migrate: {
|
|
1748
|
+
group: "Getting started", parse: "strict", args: "<url>", aliases: ["push"],
|
|
1749
|
+
summary: "push the local graph to a remote you own (solo-remote)",
|
|
1750
|
+
help: "Commit the local graph home and push it to a git remote you own (e.g. a\nprivate GitHub repo). The URL is remembered as 'origin', so later pushes need\nno argument. Pure git plumbing — no server route is involved.",
|
|
1751
|
+
options: {},
|
|
1752
|
+
examples: ["spor migrate git@github.com:you/my-graph.git", "spor push"],
|
|
1753
|
+
run: (cfg, p) => cmdMigrate(cfg, p),
|
|
1754
|
+
},
|
|
1755
|
+
whoami: {
|
|
1756
|
+
group: "Getting started", parse: "strict", args: "", options: {},
|
|
1757
|
+
summary: "who the team graph thinks you are (remote)",
|
|
1758
|
+
help: "Echo the identity the server binds to your token (remote mode). In local\nmode it explains there is no server identity.",
|
|
1759
|
+
run: (cfg) => cmdWhoami(cfg),
|
|
1760
|
+
},
|
|
1761
|
+
|
|
1762
|
+
// --- Team admin ---
|
|
1763
|
+
invite: {
|
|
1764
|
+
group: "Team admin (remote, admin token)", parse: "strict",
|
|
1765
|
+
args: "--person <id> | --name <n> --email <e>",
|
|
1766
|
+
summary: "mint a teammate token (creates the person node if needed)",
|
|
1767
|
+
help:
|
|
1768
|
+
"Mint a person-bound token and print a paste-ready 'spor join' line. Remote +\n" +
|
|
1769
|
+
"admin only. Pass --person to bind an existing person node, or --name/--email\n" +
|
|
1770
|
+
"to create the node first.",
|
|
1771
|
+
options: {
|
|
1772
|
+
person: { type: "string", value: "id", desc: "bind to an existing person node" },
|
|
1773
|
+
name: { type: "string", value: "name", desc: "create a person node with this name" },
|
|
1774
|
+
email: { type: "string", value: "email", desc: "the new person's email" },
|
|
1775
|
+
id: { type: "string", value: "id", desc: "explicit id for the created person node" },
|
|
1776
|
+
expires: { type: "string", value: "Nd", desc: "token lifetime, e.g. 30d" },
|
|
1777
|
+
},
|
|
1778
|
+
examples: ["spor invite --person person-jo", "spor invite --name 'Jo Diaz' --email jo@x.io --expires 30d"],
|
|
1779
|
+
run: (cfg, p) => cmdInvite(cfg, p),
|
|
1780
|
+
},
|
|
1781
|
+
token: {
|
|
1782
|
+
group: "Team admin (remote, admin token)", parse: "raw", args: "list | revoke <prefix>",
|
|
1783
|
+
summary: "manage tokens (list, revoke)",
|
|
1784
|
+
help: "List or revoke team tokens. Remote + admin only.\n\n spor token list show all tokens (hash prefix, person, expiry)\n spor token revoke <prefix> revoke the token with that hash prefix",
|
|
1785
|
+
examples: ["spor token list", "spor token revoke a1b2c3"],
|
|
1786
|
+
run: (cfg, args) => cmdToken(cfg, args),
|
|
1787
|
+
},
|
|
1788
|
+
|
|
1789
|
+
// --- Graph ---
|
|
1790
|
+
add: {
|
|
1791
|
+
group: "Graph", parse: "strict", args: '"<text>"', aliases: ["capture"],
|
|
1792
|
+
summary: "capture a node (local: typed file; remote: /v1/capture)",
|
|
1793
|
+
help:
|
|
1794
|
+
"Capture a node from prose. In remote mode the server's ingestion model types\n" +
|
|
1795
|
+
"and links it; in local mode a well-formed, validated node file is written so\n" +
|
|
1796
|
+
"you never hand-author frontmatter. --type/--title/--id apply to local mode.",
|
|
1797
|
+
options: {
|
|
1798
|
+
type: { type: "string", value: "T", desc: "node type (local only; default: task)" },
|
|
1799
|
+
title: { type: "string", value: "...", desc: "title (default: first 10 words)" },
|
|
1800
|
+
project: { type: "string", value: "S", desc: "project slug (default: inferred from cwd)" },
|
|
1801
|
+
id: { type: "string", value: "id", desc: "explicit node id (local only)" },
|
|
1802
|
+
},
|
|
1803
|
+
examples: ['spor add "Cache tf-idf norms across compiles for speed" --type task'],
|
|
1804
|
+
run: (cfg, p) => cmdAdd(cfg, p),
|
|
1805
|
+
},
|
|
1806
|
+
next: {
|
|
1807
|
+
group: "Graph", parse: "raw", args: "[--project S]", aliases: ["queue"],
|
|
1808
|
+
summary: "the decision queue (local: lib/queue; remote: /v1/queue)",
|
|
1809
|
+
help: "Show the ranked decision queue. Remote mode reads /v1/queue; local mode is a\nbyte-identical passthrough to lib/queue.js, so it also accepts that script's\nflags (--days, --no-front, --limit, --name-only, --nodes).",
|
|
1810
|
+
options: {
|
|
1811
|
+
project: { type: "string", value: "S", desc: "scope to a project slug (default: inferred)" },
|
|
1812
|
+
json: { type: "boolean", desc: "machine-readable JSON output" },
|
|
1813
|
+
},
|
|
1814
|
+
examples: ["spor next", "spor next --project spor --json"],
|
|
1815
|
+
run: (cfg, args) => cmdNext(cfg, args),
|
|
1816
|
+
},
|
|
1817
|
+
get: {
|
|
1818
|
+
group: "Graph", parse: "strict", args: "<id>", options: {},
|
|
1819
|
+
summary: "a node by id (local: file; remote: /v1/nodes/<id>)",
|
|
1820
|
+
help: "Print one node's raw markdown by id. Remote mode reads /v1/nodes/<id>; local\nmode reads the node file. A missing node exits 1.",
|
|
1821
|
+
examples: ["spor get dec-cc-zero-dep-client"],
|
|
1822
|
+
run: (cfg, p) => cmdGet(cfg, p),
|
|
1823
|
+
},
|
|
1824
|
+
lens: {
|
|
1825
|
+
group: "Graph", parse: "raw", args: "[<id>]", aliases: ["render-lens"],
|
|
1826
|
+
summary: "render a saved view (remote)",
|
|
1827
|
+
help:
|
|
1828
|
+
"Render a saved lens (remote only — lenses render server-side). With no id,\n" +
|
|
1829
|
+
"lists the lens catalog; with an id, renders it. Any extra --PARAM VALUE flags\n" +
|
|
1830
|
+
"beyond --format/--json are forwarded to the lens as render parameters.",
|
|
1831
|
+
options: {
|
|
1832
|
+
format: { type: "string", value: "text|json", desc: "server rendering format (default: text)" },
|
|
1833
|
+
json: { type: "boolean", desc: "force JSON: the raw catalog / view tree" },
|
|
1834
|
+
},
|
|
1835
|
+
examples: ["spor lens", "spor lens lens-roadmap", "spor lens lens-roadmap --project spor"],
|
|
1836
|
+
run: (cfg, args) => cmdLens(cfg, args),
|
|
1837
|
+
},
|
|
1838
|
+
|
|
1839
|
+
// --- Repo scoping ---
|
|
1840
|
+
disable: {
|
|
1841
|
+
group: "Repo scoping", parse: "strict", args: "", options: {},
|
|
1842
|
+
summary: "turn Spor off for this repo (.spor.json)",
|
|
1843
|
+
help: "Set { enabled: false } in this repo's committable .spor.json. The hooks then\nno-op here until re-enabled. Commit the file to share the setting.",
|
|
1844
|
+
run: (cfg) => cmdScope(false),
|
|
1845
|
+
},
|
|
1846
|
+
enable: {
|
|
1847
|
+
group: "Repo scoping", parse: "strict", args: "", options: {},
|
|
1848
|
+
summary: "turn Spor back on for this repo (.spor.json)",
|
|
1849
|
+
help: "Set { enabled: true } in this repo's committable .spor.json, undoing a prior\n'spor disable'.",
|
|
1850
|
+
run: (cfg) => cmdScope(true),
|
|
1851
|
+
},
|
|
1852
|
+
link: {
|
|
1853
|
+
group: "Repo scoping", parse: "strict", args: "<slug>", options: {},
|
|
1854
|
+
summary: "set this repo's canonical project slug (.spor marker)",
|
|
1855
|
+
help: "Write a .spor identity marker (repo: <slug>) at the repo root, fixing a wrong\ninferred slug deterministically. The slug must be canonical (^[a-z0-9][a-z0-9-]*$).\nWith no slug it uses the inferred one. Commit the marker to share the identity.",
|
|
1856
|
+
examples: ["spor link my-repo"],
|
|
1857
|
+
run: (cfg, p) => cmdLink(cfg, p),
|
|
1858
|
+
},
|
|
1859
|
+
compile: {
|
|
1860
|
+
group: "Repo scoping", parse: "raw", args: "<args>",
|
|
1861
|
+
summary: "full neighborhood / digest (local; byte-identical)",
|
|
1862
|
+
help: "Compile a node neighborhood or a prompt-time digest from the local graph.\nByte-identical passthrough to lib/compile.js (norm-cc-byte-identical-refactor).",
|
|
1863
|
+
options: {
|
|
1864
|
+
root: { type: "string", value: "id", desc: "compile a node's neighborhood" },
|
|
1865
|
+
query: { type: "string", value: "text", desc: "compile from free-text (query mode)" },
|
|
1866
|
+
project: { type: "string", value: "slug", desc: "session slug (scopes project: corrections)" },
|
|
1867
|
+
nodes: { type: "string", value: "dir", desc: "graph nodes dir (default: $SPOR_HOME/nodes)" },
|
|
1868
|
+
digest: { type: "boolean", desc: "emit a compact prompt-time digest" },
|
|
1869
|
+
skeleton: { type: "boolean", desc: "write a versioned briefing-node skeleton (root mode)" },
|
|
1870
|
+
"min-sim": { type: "string", value: "n", desc: "query-mode relevance gate (default: 0.08)" },
|
|
1871
|
+
out: { type: "string", value: "file", desc: "write to a file instead of stdout" },
|
|
1872
|
+
quiet: { type: "boolean", desc: "suppress the stderr stats / no-graph lines" },
|
|
1873
|
+
},
|
|
1874
|
+
examples: ['spor compile --root dec-x', 'spor compile --query "auth token rotation" --digest'],
|
|
1875
|
+
run: (cfg, args) => cmdCompile(cfg, "compile", args),
|
|
1876
|
+
},
|
|
1877
|
+
brief: {
|
|
1878
|
+
group: "Repo scoping", parse: "raw", args: "<id>",
|
|
1879
|
+
summary: "compile a briefing for a node (sugar for compile --root <id>)",
|
|
1880
|
+
help: "Compile a briefing for one node — sugar for 'compile --root <id>'. Byte-\nidentical passthrough to lib/compile.js.",
|
|
1881
|
+
examples: ["spor brief dec-cc-zero-dep-client"],
|
|
1882
|
+
run: (cfg, args) => cmdCompile(cfg, "brief", args),
|
|
1883
|
+
},
|
|
1884
|
+
validate: {
|
|
1885
|
+
group: "Repo scoping", parse: "raw", args: "",
|
|
1886
|
+
summary: "lint the local graph (byte-identical)",
|
|
1887
|
+
help: "Lint the local graph and exit 1 on errors. Byte-identical passthrough to\nlib/validate.js.",
|
|
1888
|
+
options: { nodes: { type: "string", value: "dir", desc: "graph nodes dir to lint" } },
|
|
1889
|
+
run: (cfg, args) => passthrough("validate.js", args),
|
|
1890
|
+
},
|
|
1891
|
+
|
|
1892
|
+
// --- Dispatch ---
|
|
1893
|
+
dispatch: {
|
|
1894
|
+
group: "Dispatch (Claude Code background agents)", parse: "strict", args: '"<task>" | <node-id>', aliases: ["bg"],
|
|
1895
|
+
summary: "compile a briefing + launch 'claude --bg' in the repo",
|
|
1896
|
+
help:
|
|
1897
|
+
"Compile a briefing for a task and launch a Claude Code background agent in the\n" +
|
|
1898
|
+
"right repo. Give free-text, a <node-id>, --node <id>, --from-queue (the top\n" +
|
|
1899
|
+
"ranked item), or --backfill (onboard/repair a repo). The target dir is the\n" +
|
|
1900
|
+
"slug->path map ('spor repos'), overridable with --dir.\n\n" +
|
|
1901
|
+
"--template supplies your own prompt with {{brief}}/{{task}}/{{node}}/{{title}}/\n" +
|
|
1902
|
+
"{{slug}}/{{dir}}/{{default}} placeholders.",
|
|
1903
|
+
options: {
|
|
1904
|
+
dir: { type: "string", value: "path", desc: "launch directory (overrides the slug map)" },
|
|
1905
|
+
node: { type: "string", value: "id", desc: "dispatch a specific node id" },
|
|
1906
|
+
slug: { type: "string", value: "slug", desc: "target project slug (cross-repo resolution)" },
|
|
1907
|
+
model: { type: "string", value: "M", desc: "claude --model" },
|
|
1908
|
+
"permission-mode": { type: "string", value: "P", desc: "claude --permission-mode" },
|
|
1909
|
+
agent: { type: "string", value: "A", desc: "claude --agent" },
|
|
1910
|
+
name: { type: "string", value: "N", desc: "claude --name (session name)" },
|
|
1911
|
+
template: { type: "string", value: "F", desc: "prompt template file (placeholders above)" },
|
|
1912
|
+
full: { type: "boolean", desc: "full briefing instead of the digest" },
|
|
1913
|
+
"no-brief": { type: "boolean", desc: "raw task prompt, no briefing block" },
|
|
1914
|
+
"from-queue": { type: "boolean", desc: "dispatch the top-ranked queue item" },
|
|
1915
|
+
backfill: { type: "boolean", desc: "onboard/repair this repo (runs /spor:backfill)" },
|
|
1916
|
+
print: { type: "boolean", desc: "dry run — print the prompt, launch nothing" },
|
|
1917
|
+
"dry-run": DRYRUN_OPT,
|
|
1918
|
+
},
|
|
1919
|
+
examples: ['spor dispatch "rotate the pipeline auth tokens" --dir ../api', "spor dispatch dec-x --model haiku", "spor dispatch --from-queue --print"],
|
|
1920
|
+
run: (cfg, p) => cmdDispatch(cfg, p),
|
|
1921
|
+
},
|
|
1922
|
+
repos: {
|
|
1923
|
+
group: "Dispatch (Claude Code background agents)", parse: "raw", args: "[list | add <slug> <path> | rm <slug>]",
|
|
1924
|
+
summary: "the local slug->repo-dir map used to pick the dispatch directory",
|
|
1925
|
+
help: "Show or edit the per-machine slug->repo-dir map dispatch uses to find a repo.\nThe map self-registers as you open sessions.\n\n spor repos list the map\n spor repos add <slug> <p> map a slug to a path\n spor repos rm <slug> forget a mapping",
|
|
1926
|
+
examples: ["spor repos", "spor repos add api ~/code/api"],
|
|
1927
|
+
run: (cfg, args) => cmdRepos(cfg, args),
|
|
1928
|
+
},
|
|
1929
|
+
|
|
1930
|
+
// --- Other ---
|
|
1931
|
+
cost: {
|
|
1932
|
+
group: "Other", parse: "raw", args: "[--since D]",
|
|
1933
|
+
summary: "LLM spend summary from journal/llm-calls (local)",
|
|
1934
|
+
help: "Summarize recorded LLM spend from journal/llm-calls. Byte-identical\npassthrough to lib/cost.js.",
|
|
1935
|
+
options: {
|
|
1936
|
+
since: { type: "string", value: "YYYY-MM-DD", desc: "include calls on/after this date" },
|
|
1937
|
+
until: { type: "string", value: "YYYY-MM-DD", desc: "include calls on/before this date" },
|
|
1938
|
+
project: { type: "string", value: "slug", desc: "scope to a project" },
|
|
1939
|
+
json: { type: "boolean", desc: "machine-readable JSON output" },
|
|
1940
|
+
},
|
|
1941
|
+
examples: ["spor cost", "spor cost --since 2026-06-01"],
|
|
1942
|
+
run: (cfg, args) => passthrough("cost.js", args),
|
|
1943
|
+
},
|
|
1944
|
+
version: {
|
|
1945
|
+
group: "Other", parse: "meta", args: "", summary: "print version",
|
|
1946
|
+
run: () => 0,
|
|
1947
|
+
},
|
|
1948
|
+
help: {
|
|
1949
|
+
group: "Other", parse: "meta", args: "[<command>]", summary: "this message, or a command's detailed help",
|
|
1950
|
+
run: () => 0,
|
|
1951
|
+
},
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
const GROUP_ORDER = [
|
|
1955
|
+
"Getting started",
|
|
1956
|
+
"Team admin (remote, admin token)",
|
|
1957
|
+
"Graph",
|
|
1958
|
+
"Repo scoping",
|
|
1959
|
+
"Dispatch (Claude Code background agents)",
|
|
1960
|
+
"Other",
|
|
1961
|
+
];
|
|
1962
|
+
|
|
1963
|
+
// alias -> canonical verb (every canonical maps to itself).
|
|
1964
|
+
const ALIAS_TO_CANON = (() => {
|
|
1965
|
+
const m = {};
|
|
1966
|
+
for (const [name, e] of Object.entries(COMMANDS)) {
|
|
1967
|
+
m[name] = name;
|
|
1968
|
+
for (const a of e.aliases || []) m[a] = name;
|
|
1969
|
+
}
|
|
1970
|
+
return m;
|
|
1971
|
+
})();
|
|
1972
|
+
function resolveVerb(v) {
|
|
1973
|
+
return Object.prototype.hasOwnProperty.call(ALIAS_TO_CANON, v) ? ALIAS_TO_CANON[v] : null;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// Build the parseArgs options descriptor from a table entry's options, dropping
|
|
1977
|
+
// the help-only keys (desc/value) so only {type, short, multiple} reach parseArgs.
|
|
1978
|
+
function paOptions(options) {
|
|
1979
|
+
const o = {};
|
|
1980
|
+
for (const [name, spec] of Object.entries(options || {})) {
|
|
1981
|
+
o[name] = { type: spec.type };
|
|
1982
|
+
if (spec.short) o[name].short = spec.short;
|
|
1983
|
+
if (spec.multiple) o[name].multiple = true;
|
|
1984
|
+
}
|
|
1985
|
+
return o;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Closest candidate by edit distance, for "did you mean --foo?" hints.
|
|
1989
|
+
function editDistance(a, b) {
|
|
1990
|
+
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
1991
|
+
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
|
|
1992
|
+
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
1993
|
+
for (let i = 1; i <= a.length; i++)
|
|
1994
|
+
for (let j = 1; j <= b.length; j++)
|
|
1995
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1996
|
+
return dp[a.length][b.length];
|
|
1997
|
+
}
|
|
1998
|
+
function suggest(word, candidates) {
|
|
1999
|
+
let best = null;
|
|
2000
|
+
let bestD = Infinity;
|
|
2001
|
+
for (const c of candidates) {
|
|
2002
|
+
const d = editDistance(word, c);
|
|
2003
|
+
if (d < bestD) {
|
|
2004
|
+
bestD = d;
|
|
2005
|
+
best = c;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return bestD <= Math.max(2, Math.ceil(word.length / 3)) ? best : null;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// Turn a parseArgs throw into a friendly, no-stack-trace error + a flag hint.
|
|
2012
|
+
function parseError(e, entry, verb) {
|
|
2013
|
+
const m = /'(-{1,2}[^']+)'/.exec(e.message || "");
|
|
2014
|
+
if (e.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" && m) {
|
|
2015
|
+
err(`spor ${verb}: unknown flag '${m[1]}'`);
|
|
2016
|
+
const s = suggest(m[1].replace(/^-+/, ""), Object.keys(entry.options || {}));
|
|
2017
|
+
if (s) err(` did you mean --${s}?`);
|
|
2018
|
+
err(` run 'spor ${verb} --help' for the flag list.`);
|
|
2019
|
+
return 1;
|
|
2020
|
+
}
|
|
2021
|
+
err(`spor ${verb}: ${e.message}`);
|
|
2022
|
+
err(` run 'spor ${verb} --help' for usage.`);
|
|
2023
|
+
return 1;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// The top-level listing, generated from the table so it can't drift.
|
|
2027
|
+
function renderTopHelp() {
|
|
2028
|
+
const verbs = Object.keys(COMMANDS);
|
|
2029
|
+
const sigOf = (v) => `${v}${COMMANDS[v].args ? " " + COMMANDS[v].args : ""}`;
|
|
2030
|
+
const width = Math.min(22, Math.max(...verbs.map((v) => sigOf(v).length)));
|
|
2031
|
+
const lines = [HELP_HEADER, ""];
|
|
2032
|
+
for (const group of GROUP_ORDER) {
|
|
2033
|
+
const inGroup = verbs.filter((v) => COMMANDS[v].group === group);
|
|
2034
|
+
if (!inGroup.length) continue;
|
|
2035
|
+
lines.push(group);
|
|
2036
|
+
for (const v of inGroup) lines.push(` ${sigOf(v).padEnd(width)} ${COMMANDS[v].summary}`);
|
|
2037
|
+
lines.push("");
|
|
2038
|
+
}
|
|
2039
|
+
lines.push(HELP_FOOTER);
|
|
2040
|
+
return lines.join("\n");
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// One command's detailed page (usage, aliases, description, flags, examples).
|
|
2044
|
+
function renderCmdHelp(verb) {
|
|
2045
|
+
const e = COMMANDS[verb];
|
|
2046
|
+
const opts = Object.entries(e.options || {});
|
|
2047
|
+
const sig = `spor ${verb}${e.args ? " " + e.args : ""}${opts.length ? " [options]" : ""}`;
|
|
2048
|
+
const lines = [sig, "", e.summary];
|
|
2049
|
+
if (e.aliases && e.aliases.length) lines.push(`Aliases: ${e.aliases.join(", ")}`);
|
|
2050
|
+
if (e.help) lines.push("", e.help);
|
|
2051
|
+
if (opts.length) {
|
|
2052
|
+
const rendered = opts.map(([name, o]) => [`--${name}${o.type === "string" ? ` <${o.value || "value"}>` : ""}`, o.desc || ""]);
|
|
2053
|
+
const w = Math.min(26, Math.max(...rendered.map((r) => r[0].length)));
|
|
2054
|
+
lines.push("", "Options:");
|
|
2055
|
+
for (const [flag, desc] of rendered) lines.push(` ${flag.padEnd(w)} ${desc}`);
|
|
2056
|
+
}
|
|
2057
|
+
if (e.examples && e.examples.length) {
|
|
2058
|
+
lines.push("", "Examples:");
|
|
2059
|
+
for (const ex of e.examples) lines.push(` ${ex}`);
|
|
2060
|
+
}
|
|
2061
|
+
return lines.join("\n");
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1739
2064
|
async function main() {
|
|
1740
2065
|
const argv = process.argv.slice(2);
|
|
1741
2066
|
const verb = argv.shift();
|
|
1742
2067
|
const args = argv;
|
|
1743
2068
|
const cfg = loadConfig({ cwd: process.cwd() });
|
|
1744
2069
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
case "enable":
|
|
1779
|
-
return cmdScope(true);
|
|
1780
|
-
case "disable":
|
|
1781
|
-
return cmdScope(false);
|
|
1782
|
-
case "link":
|
|
1783
|
-
return cmdLink(args);
|
|
1784
|
-
case "invite":
|
|
1785
|
-
return await cmdInvite(cfg, args);
|
|
1786
|
-
case "token":
|
|
1787
|
-
return await cmdToken(cfg, args);
|
|
1788
|
-
case "next":
|
|
1789
|
-
case "queue":
|
|
1790
|
-
return await cmdNext(cfg, args);
|
|
1791
|
-
case "get":
|
|
1792
|
-
return await cmdGet(cfg, args);
|
|
1793
|
-
case "lens":
|
|
1794
|
-
case "render-lens":
|
|
1795
|
-
return await cmdLens(cfg, args);
|
|
1796
|
-
case "compile":
|
|
1797
|
-
case "brief":
|
|
1798
|
-
return cmdCompile(cfg, verb, args);
|
|
1799
|
-
case "dispatch":
|
|
1800
|
-
case "bg":
|
|
1801
|
-
return await cmdDispatch(cfg, args);
|
|
1802
|
-
case "repos":
|
|
1803
|
-
return cmdRepos(cfg, args);
|
|
1804
|
-
case "validate":
|
|
1805
|
-
return passthrough("validate.js", args);
|
|
1806
|
-
case "cost":
|
|
1807
|
-
return passthrough("cost.js", args); // local: LLM spend summary
|
|
1808
|
-
default:
|
|
1809
|
-
err(`spor: unknown verb '${verb}'. Try 'spor help'.`);
|
|
1810
|
-
return 1;
|
|
2070
|
+
// Top-level help / version are intercepted before table dispatch. `spor help
|
|
2071
|
+
// <command>` prints that command's detailed page.
|
|
2072
|
+
if (verb === undefined || verb === "help" || verb === "-h" || verb === "--help") {
|
|
2073
|
+
const topic = verb === "help" && args[0] ? resolveVerb(args[0]) : null;
|
|
2074
|
+
out(topic && COMMANDS[topic].parse !== "meta" ? renderCmdHelp(topic) : renderTopHelp());
|
|
2075
|
+
return 0;
|
|
2076
|
+
}
|
|
2077
|
+
if (verb === "version" || verb === "--version" || verb === "-v") {
|
|
2078
|
+
out(version());
|
|
2079
|
+
return 0;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const canon = resolveVerb(verb);
|
|
2083
|
+
if (!canon || COMMANDS[canon].parse === "meta") {
|
|
2084
|
+
err(`spor: unknown verb '${verb}'. Try 'spor help'.`);
|
|
2085
|
+
return 1;
|
|
2086
|
+
}
|
|
2087
|
+
const entry = COMMANDS[canon];
|
|
2088
|
+
|
|
2089
|
+
// `spor <command> --help|-h` => the command's own page.
|
|
2090
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2091
|
+
out(renderCmdHelp(canon));
|
|
2092
|
+
return 0;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
if (entry.parse === "raw") return await entry.run(cfg, args, verb);
|
|
2096
|
+
|
|
2097
|
+
// strict: util.parseArgs is the parser; a parse failure is a friendly error.
|
|
2098
|
+
let parsed;
|
|
2099
|
+
try {
|
|
2100
|
+
parsed = parseArgs({ args, options: paOptions(entry.options), allowPositionals: true, strict: true });
|
|
2101
|
+
} catch (e) {
|
|
2102
|
+
return parseError(e, entry, canon);
|
|
1811
2103
|
}
|
|
2104
|
+
return await entry.run(cfg, parsed, verb);
|
|
1812
2105
|
}
|
|
1813
2106
|
|
|
1814
2107
|
// Expose the pure helpers for unit tests (the version-check logic has no I/O),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sporhq/spor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Spor — a shared memory substrate for teams and agents. Decisions, their reasons, and the traces they leave. Knowledge-graph context compiler: session-start briefings, per-prompt digests, capture at discovery, end-of-session distillation, decision queue.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Anthony Allen",
|