@opum-ai/lore 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -22
- package/bin/lore.cjs +35 -7
- package/package.json +17 -17
- package/src/adapters/backlog.ts +0 -1084
- package/src/adapters/git.ts +0 -221
- package/src/cli.ts +0 -667
- package/src/commands/agent.ts +0 -301
- package/src/commands/agents.ts +0 -302
- package/src/commands/args.ts +0 -209
- package/src/commands/changed.ts +0 -70
- package/src/commands/check.ts +0 -1031
- package/src/commands/codex-bridge.ts +0 -49
- package/src/commands/concurrency.ts +0 -48
- package/src/commands/context.ts +0 -292
- package/src/commands/discover.ts +0 -89
- package/src/commands/explorer.ts +0 -253
- package/src/commands/export.ts +0 -93
- package/src/commands/fswrite.ts +0 -928
- package/src/commands/graph.ts +0 -291
- package/src/commands/help.ts +0 -151
- package/src/commands/impact.ts +0 -59
- package/src/commands/init.ts +0 -583
- package/src/commands/instructions.ts +0 -91
- package/src/commands/link.ts +0 -929
- package/src/commands/new.ts +0 -476
- package/src/commands/orphans.ts +0 -457
- package/src/commands/path.ts +0 -67
- package/src/commands/provenance.ts +0 -68
- package/src/commands/query.ts +0 -312
- package/src/commands/reconcile-shared.ts +0 -280
- package/src/commands/rename.ts +0 -585
- package/src/commands/replace.ts +0 -320
- package/src/commands/scaffold.ts +0 -346
- package/src/commands/schema.ts +0 -293
- package/src/commands/snapshot.ts +0 -130
- package/src/commands/supersede.ts +0 -400
- package/src/commands/sync.ts +0 -371
- package/src/commands/tasks.ts +0 -271
- package/src/commands/traversal.ts +0 -151
- package/src/commands/validate.ts +0 -226
- package/src/config.ts +0 -598
- package/src/core/agent-bridge.ts +0 -287
- package/src/core/agent-context.ts +0 -498
- package/src/core/agent-profile.ts +0 -447
- package/src/core/bundle.ts +0 -893
- package/src/core/check.ts +0 -853
- package/src/core/codex-bridge.ts +0 -100
- package/src/core/concept.ts +0 -597
- package/src/core/consumer-scaffold.ts +0 -433
- package/src/core/context.ts +0 -271
- package/src/core/explorer-contract.ts +0 -441
- package/src/core/explorer-qualification.ts +0 -58
- package/src/core/explorer.ts +0 -518
- package/src/core/finding.ts +0 -31
- package/src/core/graph.ts +0 -201
- package/src/core/indexes.ts +0 -436
- package/src/core/instructions.ts +0 -209
- package/src/core/ladybug-driver.ts +0 -1795
- package/src/core/ladybug-lifecycle.ts +0 -1178
- package/src/core/ladybug-native.ts +0 -95
- package/src/core/ladybug-source.ts +0 -667
- package/src/core/links.ts +0 -681
- package/src/core/log.ts +0 -253
- package/src/core/managed-block.ts +0 -540
- package/src/core/manifest.ts +0 -718
- package/src/core/order.ts +0 -13
- package/src/core/profile.ts +0 -1007
- package/src/core/projection.ts +0 -195
- package/src/core/query.ts +0 -542
- package/src/core/reconcile.ts +0 -236
- package/src/core/replace.ts +0 -419
- package/src/core/retrieval.ts +0 -213
- package/src/core/rewrite.ts +0 -940
- package/src/core/scaffold.ts +0 -255
- package/src/core/schema.ts +0 -366
- package/src/core/snapshot-runtime.ts +0 -52
- package/src/core/snapshot-store.ts +0 -287
- package/src/core/snapshot.ts +0 -711
- package/src/core/template.ts +0 -429
- package/src/core/traversal.ts +0 -487
- package/src/core/validate.ts +0 -517
- package/src/core/workspace-contract.ts +0 -473
- package/src/core/workspace-projection.ts +0 -365
- package/src/core/workspace-retrieval.ts +0 -196
- package/src/core/workspace-source.ts +0 -174
- package/src/errors.ts +0 -697
- package/src/meta.ts +0 -7
- package/src/output.ts +0 -589
- package/src/scripts/upstream-backlog-watch.ts +0 -288
- package/src/state.ts +0 -390
package/src/cli.ts
DELETED
|
@@ -1,667 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* cli.ts — lore's primary surface (lore-design §2): a thin Commander entrypoint.
|
|
4
|
-
*
|
|
5
|
-
* It resolves global flags + the output mode once, dispatches a subcommand to its
|
|
6
|
-
* thin handler in `commands/`, and funnels every thrown {@link LoreError} through the
|
|
7
|
-
* one `reportError` seam so the exit-code/`--json`-envelope contract is identical
|
|
8
|
-
* across commands (cli-contract §4–§5). The handler does the work and returns its
|
|
9
|
-
* success code; the router owns parsing, mode resolution, and error reporting.
|
|
10
|
-
*
|
|
11
|
-
* Commander owns tokenization, option validation, positional collection, and
|
|
12
|
-
* declarative subcommand routing. Lore retains its own output/error seams: every
|
|
13
|
-
* parser failure is translated to a {@link LoreError}, and Commander is configured
|
|
14
|
-
* never to exit the process or write directly to a real/injected stream.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { Command } from "commander";
|
|
18
|
-
import type { BacklogAdapter } from "./adapters/backlog";
|
|
19
|
-
import { runAgent } from "./commands/agent";
|
|
20
|
-
import { runAgents } from "./commands/agents";
|
|
21
|
-
import { commanderOption, commanderUnknownCommand, commanderUsageError } from "./commands/args";
|
|
22
|
-
import { runChanged } from "./commands/changed";
|
|
23
|
-
import { type FetchLike, type ResolveHost, runCheck } from "./commands/check";
|
|
24
|
-
import { runContext } from "./commands/context";
|
|
25
|
-
import { runExplorer } from "./commands/explorer";
|
|
26
|
-
import { runExport } from "./commands/export";
|
|
27
|
-
import { runGraph } from "./commands/graph";
|
|
28
|
-
import { renderTopLevelHelp, runHelp } from "./commands/help";
|
|
29
|
-
import { runImpact } from "./commands/impact";
|
|
30
|
-
import { type AgentAvailability, type InitPrompter, runInit } from "./commands/init";
|
|
31
|
-
import { runInstructions } from "./commands/instructions";
|
|
32
|
-
import { runLink, runUnlink } from "./commands/link";
|
|
33
|
-
import { runNew } from "./commands/new";
|
|
34
|
-
import { runOrphans } from "./commands/orphans";
|
|
35
|
-
import { runPath } from "./commands/path";
|
|
36
|
-
import { runProvenance } from "./commands/provenance";
|
|
37
|
-
import { runQuery } from "./commands/query";
|
|
38
|
-
import { runRename } from "./commands/rename";
|
|
39
|
-
import { runReplace } from "./commands/replace";
|
|
40
|
-
import { runScaffold } from "./commands/scaffold";
|
|
41
|
-
import { runSchema } from "./commands/schema";
|
|
42
|
-
import { runSnapshot } from "./commands/snapshot";
|
|
43
|
-
import { runSupersede } from "./commands/supersede";
|
|
44
|
-
import { runSync } from "./commands/sync";
|
|
45
|
-
import { runTasks } from "./commands/tasks";
|
|
46
|
-
import { runValidate } from "./commands/validate";
|
|
47
|
-
import { buildManifest } from "./core/manifest";
|
|
48
|
-
import { loadRetrievalGraph, type RetrievalGraphLoader } from "./core/retrieval";
|
|
49
|
-
import { EXIT_OK, EXIT_UNCAUGHT, LoreError, reportError, type Writer } from "./errors";
|
|
50
|
-
import { VERSION } from "./meta";
|
|
51
|
-
import { emit, errorRenderOpts, type OutputContext, type Renderable, resolveOutput } from "./output";
|
|
52
|
-
|
|
53
|
-
// The top-level help text (the `--help`/no-command catalog) is rendered from the
|
|
54
|
-
// capability manifest via `renderTopLevelHelp` (commands/help.ts) — one source
|
|
55
|
-
// for both `lore --help` and the `lore help` command, so no separately-maintained
|
|
56
|
-
// `USAGE` literal can drift from the real command surface (LORE-38).
|
|
57
|
-
|
|
58
|
-
/** The subcommand and normalized command-local tokens Commander resolved for one invocation. */
|
|
59
|
-
interface ParsedArgs {
|
|
60
|
-
command: string;
|
|
61
|
-
commandArgs: string[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** The ordinary global options Commander parses without invoking its built-in help/version exits. */
|
|
65
|
-
interface GlobalOptions {
|
|
66
|
-
json: boolean;
|
|
67
|
-
plain: boolean;
|
|
68
|
-
version: boolean;
|
|
69
|
-
help: boolean;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** The injectable environment a {@link run} sees; every field defaults to the real process. */
|
|
73
|
-
export interface RunContext {
|
|
74
|
-
stdout?: Writer;
|
|
75
|
-
stderr?: Writer;
|
|
76
|
-
env?: Record<string, string | undefined>;
|
|
77
|
-
cwd?: string;
|
|
78
|
-
isTTY?: boolean;
|
|
79
|
-
/**
|
|
80
|
-
* Whether **stderr** (not stdout) is a TTY. Gates ANSI color on the stderr diagnostic path
|
|
81
|
-
* (`reportError`, cli-contract §6) independent of `isTTY` (stdout's own TTY state, which alone
|
|
82
|
-
* drives `mode`/stdout's pretty color), AND (LORE-260 review round 2, BLOCKING-1) is the OTHER
|
|
83
|
-
* half of `lore init`'s wizard gate alongside `stdinIsTTY`: every wizard question is written to
|
|
84
|
-
* stderr, so a redirected stderr must veto the wizard even with a still-TTY stdin — gating on
|
|
85
|
-
* `stdinIsTTY` alone left the wizard blocked on a prompt nobody could see (confirmed live under a
|
|
86
|
-
* pty: `lore init >/dev/null 2>&1` hung forever). Defaults the same way `isTTY` does: an injected
|
|
87
|
-
* `stderr` sink with no explicit hint here is treated as non-TTY; only the real-process path (no
|
|
88
|
-
* injected sink) reads the actual `process.stderr.isTTY`, coerced to a strict boolean via
|
|
89
|
-
* {@link coerceRealTTY} (LORE-250; a raw, uncoerced read is `undefined` — not
|
|
90
|
-
* `false` — on a non-TTY stream and re-leaks color downstream, see that
|
|
91
|
-
* function's doc).
|
|
92
|
-
*/
|
|
93
|
-
stderrIsTTY?: boolean;
|
|
94
|
-
/**
|
|
95
|
-
* Whether **stdin** is a TTY — one half of `lore init`'s wizard gate (LORE-260, cli-contract's
|
|
96
|
-
* non-interactive mandate; see {@link stderrIsTTY} for the other half). Independent of `isTTY`
|
|
97
|
-
* (stdout's own TTY state) and `stderrIsTTY`: a real terminal session normally has all three true,
|
|
98
|
-
* but a script piping input while capturing output can have any combination. Defaults the same way
|
|
99
|
-
* `isTTY`/`stderrIsTTY` do: an injected `stdout` sink with no explicit hint here is treated as
|
|
100
|
-
* non-TTY (so a test harness never accidentally enables a blocking prompt); only the real-process
|
|
101
|
-
* path (no injected sink) reads the actual `process.stdin.isTTY`, coerced to a strict boolean via
|
|
102
|
-
* {@link coerceRealTTY}.
|
|
103
|
-
*/
|
|
104
|
-
stdinIsTTY?: boolean;
|
|
105
|
-
/** The fetch `check --external` uses for liveness; defaults to the global `fetch`. Injected so a caller (or a test) controls or stubs the network. */
|
|
106
|
-
fetch?: FetchLike;
|
|
107
|
-
/** DNS resolution `check --external`'s SSRF guard uses (LORE-71); defaults to real `node:dns`. Injected so a caller (or a test) controls or stubs DNS. */
|
|
108
|
-
resolveHost?: ResolveHost;
|
|
109
|
-
/** The Backlog adapter `link`/`unlink` use; defaults to the real `backlog` binary on PATH. Injected so a caller (or a test) touches no subprocess. */
|
|
110
|
-
adapter?: BacklogAdapter;
|
|
111
|
-
/**
|
|
112
|
-
* Internal graph/query/context backend seam. The real Commander dispatch uses
|
|
113
|
-
* verified indexed retrieval with reference fallback; tests may inject either
|
|
114
|
-
* conformance implementation without adding a public parser flag.
|
|
115
|
-
*/
|
|
116
|
-
retrieval?: RetrievalGraphLoader;
|
|
117
|
-
/** `lore init`'s interactive-wizard I/O seam (LORE-260); defaults to a real `readline` session over stdin/stderr. Injected so a caller (or a test) drives the wizard without a real terminal. */
|
|
118
|
-
prompter?: InitPrompter;
|
|
119
|
-
/** Injectable executable discovery for `lore init`; keeps router tests independent of the host PATH. */
|
|
120
|
-
agentAvailability?: () => AgentAvailability;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Coerce a real (uninjected) stream's `.isTTY` reading to a strict boolean.
|
|
125
|
-
*
|
|
126
|
-
* Node/Bun leave `process.stdout.isTTY` / `process.stderr.isTTY` **unset** —
|
|
127
|
-
* `undefined`, not `false` — on a non-TTY stream (a pipe, or a `>`/`2>`
|
|
128
|
-
* redirect); @types/node's `boolean | undefined` typing on the property does
|
|
129
|
-
* not make this visible at the type level. `resolveOutput`'s `stderrIsTTY`
|
|
130
|
-
* input (output.ts) and `errorRenderOpts`'s `stderrIsTTY` parameter both
|
|
131
|
-
* default an *absent* value to `true` — a deliberate no-op for callers that
|
|
132
|
-
* never pass the field at all, so every pre-LORE-250 call site keeps its exact
|
|
133
|
-
* prior behavior. That default cannot distinguish "the caller didn't pass
|
|
134
|
-
* this" from "the real stream's `.isTTY` happened to read as `undefined`
|
|
135
|
-
* because it isn't a terminal" — so handing either of those functions an
|
|
136
|
-
* uncoerced `.isTTY` read is indistinguishable from omitting the field
|
|
137
|
-
* entirely, and silently falls back to "assume TTY", reopening the exact
|
|
138
|
-
* color leak this coercion exists to close.
|
|
139
|
-
*
|
|
140
|
-
* Confirmed live at HEAD under a real pty before this fix (LORE-250, round
|
|
141
|
-
* 3 review): `script -q /dev/null zsh -c 'bun src/cli.ts <cmd> 2>err.log'`
|
|
142
|
-
* painted ANSI into `err.log` even though stderr was plainly redirected,
|
|
143
|
-
* because the uncoerced real-process read reached `resolveOutput`/
|
|
144
|
-
* `errorRenderOpts` as `undefined` and both `?? true` defaults swallowed it.
|
|
145
|
-
*
|
|
146
|
-
* `=== true` has no such ambiguity: every non-`true` reading (`undefined` or
|
|
147
|
-
* `false`) coerces to `false`, so the value `run()` computes from a real
|
|
148
|
-
* stream is always a genuine boolean, never `undefined` — the `?? true`
|
|
149
|
-
* defaults downstream are only ever reached by a caller that omitted the
|
|
150
|
-
* field outright.
|
|
151
|
-
*/
|
|
152
|
-
export function coerceRealTTY(isTTY: boolean | undefined): boolean {
|
|
153
|
-
return isTTY === true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Parse `argv`, dispatch the subcommand, and return the process exit code. Pure with
|
|
158
|
-
* respect to its injected {@link RunContext} (streams, env, cwd, TTY) so the whole
|
|
159
|
-
* router is testable without touching the real process.
|
|
160
|
-
*
|
|
161
|
-
* The output mode is resolved first, then **every** path runs under one try/catch:
|
|
162
|
-
* unknown global flags are rejected before any short-circuit (so a typo'd flag never
|
|
163
|
-
* slips through `--version`/`--help`/no-command to a silent exit 0), and `--version`/
|
|
164
|
-
* `--help` render through the same {@link emit} seam as a command — a
|
|
165
|
-
* `{schemaVersion, kind, data}` envelope under `--json`, plain text otherwise — so a
|
|
166
|
-
* machine consumer that always pipes `--json` can decode their output too. `--help`/`-h`
|
|
167
|
-
* given *with* a command (e.g. `lore query --help`) renders that command's own detailed
|
|
168
|
-
* help via {@link runHelp} rather than the top-level catalog — only a bare `--help`/no
|
|
169
|
-
* command falls through to {@link renderTopLevelHelp} (LORE-107).
|
|
170
|
-
*/
|
|
171
|
-
export function run(argv: readonly string[], context: RunContext = {}): number | Promise<number> {
|
|
172
|
-
const stdout = context.stdout ?? process.stdout;
|
|
173
|
-
const stderr = context.stderr ?? process.stderr;
|
|
174
|
-
let parsed: ParsedArgs | undefined;
|
|
175
|
-
const program = createProgram((invocation) => {
|
|
176
|
-
parsed = invocation;
|
|
177
|
-
});
|
|
178
|
-
let parseError: unknown;
|
|
179
|
-
try {
|
|
180
|
-
program.parse(argv.slice(2), { from: "user" });
|
|
181
|
-
} catch (error) {
|
|
182
|
-
parseError = error;
|
|
183
|
-
}
|
|
184
|
-
const globals = program.opts<GlobalOptions>();
|
|
185
|
-
// Stderr's own TTY state, independent of stdout's — same injected-sink-defaults-
|
|
186
|
-
// non-TTY / real-process-defaults-real-TTY shape as `isTTY` below, but read from
|
|
187
|
-
// `stderr` so a redirected `2>` is never mistaken for a terminal just because
|
|
188
|
-
// stdout is one (LORE-250, cli-contract §6). Computed *before* `resolveOutput`
|
|
189
|
-
// so it can be folded into `output.color` at the source (see `resolveOutput`'s
|
|
190
|
-
// `stderrIsTTY` input) — the single point every downstream consumer already
|
|
191
|
-
// reads, including the `WarningCollector.flush({ color: options.output.color,
|
|
192
|
-
// ... })` call sites in `commands/*.ts`, which never receive `stderrIsTTY`
|
|
193
|
-
// directly and would otherwise leak stdout's color onto a redirected stderr.
|
|
194
|
-
//
|
|
195
|
-
// The real-process branch reads `process.stderr.isTTY` through
|
|
196
|
-
// `coerceRealTTY`, NOT bare — Node/Bun leave that property `undefined` (not
|
|
197
|
-
// `false`) on a non-TTY stream, and an uncoerced `undefined` here is
|
|
198
|
-
// indistinguishable from "the field was never passed", which silently
|
|
199
|
-
// re-triggers `resolveOutput`'s/`errorRenderOpts`'s `?? true` back-compat
|
|
200
|
-
// defaults and reopens the leak this line exists to close (LORE-250, round
|
|
201
|
-
// 3 — see {@link coerceRealTTY}'s doc for the confirmed real-pty repro).
|
|
202
|
-
const stderrIsTTY = context.stderrIsTTY ?? (context.stderr ? false : coerceRealTTY(process.stderr.isTTY));
|
|
203
|
-
// Same injected-sink-defaults-non-TTY / real-process-defaults-real-TTY shape as `stderrIsTTY`
|
|
204
|
-
// above, keyed off `stdout` (not a `stdin` field — there is no injected stdin stream in
|
|
205
|
-
// `RunContext`, only this resolved boolean) since a test harness that injects capturing streams
|
|
206
|
-
// is never a real terminal session on any of the three streams.
|
|
207
|
-
const stdinIsTTY = context.stdinIsTTY ?? (context.stdout ? false : coerceRealTTY(process.stdin.isTTY));
|
|
208
|
-
const output = resolveOutput({
|
|
209
|
-
json: globals.json,
|
|
210
|
-
plain: globals.plain,
|
|
211
|
-
// A caller that injects its own stdout sink but no TTY hint is not at a terminal,
|
|
212
|
-
// so its mode resolves to plain (no stray ANSI in a captured buffer); only the
|
|
213
|
-
// real-process path (no injected sink) reads the actual `process.stdout.isTTY`.
|
|
214
|
-
isTTY: context.isTTY ?? (context.stdout ? false : process.stdout.isTTY),
|
|
215
|
-
env: context.env ?? process.env,
|
|
216
|
-
stderrIsTTY,
|
|
217
|
-
});
|
|
218
|
-
try {
|
|
219
|
-
if (parseError !== undefined) {
|
|
220
|
-
const unknownCommand = commanderUnknownCommand(parseError);
|
|
221
|
-
// Lore's global meta flags intentionally short-circuit an otherwise unknown
|
|
222
|
-
// positional command, matching the pre-Commander contract. Known commands still
|
|
223
|
-
// parse all their flags first, so `lore init --bogus --version` remains an error.
|
|
224
|
-
if (unknownCommand !== undefined && (globals.version || globals.help)) {
|
|
225
|
-
parsed = { command: unknownCommand, commandArgs: [] };
|
|
226
|
-
} else {
|
|
227
|
-
throw commanderUsageError(parseError, parsed?.command);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (globals.version || parsed === undefined) {
|
|
231
|
-
if (globals.version) {
|
|
232
|
-
return emitMeta("version", { version: VERSION }, VERSION, output, stdout);
|
|
233
|
-
}
|
|
234
|
-
const helpText = renderTopLevelHelp();
|
|
235
|
-
return emitMeta("help", { usage: helpText }, helpText, output, stdout);
|
|
236
|
-
}
|
|
237
|
-
if (globals.help) {
|
|
238
|
-
// A command was given alongside `--help`/`-h` (in any position): render that
|
|
239
|
-
// command's own detailed help — the same as `lore help <command>` — instead of
|
|
240
|
-
// the generic top-level catalog, so `lore <cmd> --help` describes the command the
|
|
241
|
-
// user actually typed (LORE-107). Still reject a stray unrecognized flag first (same
|
|
242
|
-
// invariant as the `--version`/no-command paths above): a typo'd flag must never be
|
|
243
|
-
// swallowed just because `--help` also appears on the line.
|
|
244
|
-
return runHelp({ output, args: [parsed.command], stdout });
|
|
245
|
-
}
|
|
246
|
-
// `stderrIsTTY` is threaded into `dispatch`'s context the same way `stdinIsTTY` already is
|
|
247
|
-
// (LORE-260 review round 2, BLOCKING-1): it was resolved above but never actually reached
|
|
248
|
-
// `runInit`'s `InitOptions`, so the wizard's stdin-only gate could fire with a redirected
|
|
249
|
-
// stderr — a caller sees no prompt yet the process blocks waiting for an answer.
|
|
250
|
-
const result = dispatch(parsed, { ...context, stdout, stderr, stdinIsTTY, stderrIsTTY }, output);
|
|
251
|
-
// Async command paths (external checks, Backlog mutations, and indexed
|
|
252
|
-
// graph/query/context retrieval) return a Promise. A rejection from one
|
|
253
|
-
// must funnel through the **same** error seam as a synchronous throw
|
|
254
|
-
// (formatted diagnostic + the right exit code), not escape to the
|
|
255
|
-
// entrypoint's bare backstop. The sync `catch` below cannot see an async
|
|
256
|
-
// rejection, so attach the seam to the promise here.
|
|
257
|
-
if (result instanceof Promise) {
|
|
258
|
-
return result.catch((err: unknown) => reportError(err, { ...errorRenderOpts(output, stderrIsTTY), stderr }));
|
|
259
|
-
}
|
|
260
|
-
return result;
|
|
261
|
-
} catch (err) {
|
|
262
|
-
return reportError(err, { ...errorRenderOpts(output, stderrIsTTY), stderr });
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** One command-local option occurrence, retained in input order for repeatable/duplicate parity. */
|
|
267
|
-
interface OptionOccurrence {
|
|
268
|
-
readonly name: string;
|
|
269
|
-
readonly value?: string;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Build a fresh Commander graph for one invocation.
|
|
274
|
-
*
|
|
275
|
-
* The capability manifest is the declaration source for command names and flags,
|
|
276
|
-
* and also drives Lore's generated help. Each command accepts a variadic positional
|
|
277
|
-
* collection here so global `--help`/`--version` can short-circuit incomplete command
|
|
278
|
-
* lines exactly as before; command-specific arity/value rules remain in the thin
|
|
279
|
-
* handlers. Built-in help/version are disabled, output is swallowed, and
|
|
280
|
-
* `exitOverride` turns every parser exit into a caught {@link CommanderError}.
|
|
281
|
-
*/
|
|
282
|
-
function createProgram(onInvocation: (parsed: ParsedArgs) => void): Command {
|
|
283
|
-
const manifest = buildManifest();
|
|
284
|
-
const program = new Command()
|
|
285
|
-
.name("lore")
|
|
286
|
-
.helpOption(false)
|
|
287
|
-
.helpCommand(false)
|
|
288
|
-
.showSuggestionAfterError(false)
|
|
289
|
-
.exitOverride()
|
|
290
|
-
.configureOutput({ writeOut: () => {}, writeErr: () => {} })
|
|
291
|
-
// A variadic root positional lets Lore retain its exact unknown-command
|
|
292
|
-
// classification (including a bare "-" and a token after a root "--").
|
|
293
|
-
// Known declarative subcommands still win Commander's normal dispatch.
|
|
294
|
-
.argument("[args...]")
|
|
295
|
-
.action((args: string[]) => {
|
|
296
|
-
const command = args[0];
|
|
297
|
-
if (command !== undefined) {
|
|
298
|
-
onInvocation({ command, commandArgs: args.slice(1) });
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
for (const flag of manifest.globalFlags) {
|
|
303
|
-
program.addOption(commanderOption(flag));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
for (const definition of manifest.commands) {
|
|
307
|
-
const occurrences: OptionOccurrence[] = [];
|
|
308
|
-
const positionalSyntax = commanderPositionalSyntax(definition.args);
|
|
309
|
-
const command = program
|
|
310
|
-
.command(definition.name)
|
|
311
|
-
.description(definition.summary)
|
|
312
|
-
.helpOption(false)
|
|
313
|
-
.exitOverride((error) => {
|
|
314
|
-
(error as typeof error & { loreCommand?: string }).loreCommand = definition.name;
|
|
315
|
-
throw error;
|
|
316
|
-
})
|
|
317
|
-
// Keep arity validation in the thin handlers so established Lore wording,
|
|
318
|
-
// hints, and error metadata stay compatible. Commander still receives the
|
|
319
|
-
// manifest-derived positional shape and performs token/end-of-options parsing.
|
|
320
|
-
.allowExcessArguments(true)
|
|
321
|
-
.arguments(positionalSyntax);
|
|
322
|
-
for (const flag of definition.flags) {
|
|
323
|
-
command.addOption(commanderOption(flag));
|
|
324
|
-
command.on(`option:${flag.name}`, (value?: string) => {
|
|
325
|
-
occurrences.push(value === undefined ? { name: flag.name } : { name: flag.name, value });
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
command.action(function () {
|
|
329
|
-
onInvocation({
|
|
330
|
-
command: definition.name,
|
|
331
|
-
commandArgs: normalizeCommandArgs(this.args, occurrences),
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
return program;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Convert the manifest's human-facing positional signature to a permissive
|
|
340
|
-
* Commander declaration. Required operands are intentionally made optional:
|
|
341
|
-
* command handlers retain their established arity diagnostics, while Commander
|
|
342
|
-
* owns positional tokenization and the literal `--` boundary.
|
|
343
|
-
*/
|
|
344
|
-
function commanderPositionalSyntax(signature: string): string {
|
|
345
|
-
const operands = [...signature.matchAll(/([A-Za-z][A-Za-z0-9-]*)(…)?/g)].map((match) => ({
|
|
346
|
-
name: match[1] as string,
|
|
347
|
-
variadic: match[2] !== undefined,
|
|
348
|
-
}));
|
|
349
|
-
if (operands.length === 0) {
|
|
350
|
-
// Commands with no documented operands still accept a compatibility catch-all
|
|
351
|
-
// so their handlers can preserve the exact "takes no arguments" diagnostic.
|
|
352
|
-
return "[args...]";
|
|
353
|
-
}
|
|
354
|
-
return operands.map(({ name, variadic }) => `[${name}${variadic ? "..." : ""}]`).join(" ");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Reconstitute a stable command-local vector for the existing thin handlers.
|
|
359
|
-
* Options are emitted in their original occurrence order using inline values, then
|
|
360
|
-
* positionals. A literal `--` is inserted when a positional starts with `-`, so no
|
|
361
|
-
* downstream command can accidentally reclassify a Commander positional as a flag.
|
|
362
|
-
*/
|
|
363
|
-
function normalizeCommandArgs(positionals: readonly string[], occurrences: readonly OptionOccurrence[]): string[] {
|
|
364
|
-
const options = occurrences.map((occurrence) =>
|
|
365
|
-
occurrence.value === undefined ? `--${occurrence.name}` : `--${occurrence.name}=${occurrence.value}`,
|
|
366
|
-
);
|
|
367
|
-
if (positionals.some((arg) => arg.startsWith("-") && arg !== "-")) {
|
|
368
|
-
return [...options, "--", ...positionals];
|
|
369
|
-
}
|
|
370
|
-
return [...positionals, ...options];
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Render a meta result (version / help) through the same {@link emit} seam a command
|
|
375
|
-
* uses: a `{schemaVersion, kind, data}` envelope under `--json`, the plain `text`
|
|
376
|
-
* otherwise. This is what keeps `lore --version --json` machine-parseable instead of a
|
|
377
|
-
* bare line a `--json` consumer cannot decode.
|
|
378
|
-
*/
|
|
379
|
-
function emitMeta(
|
|
380
|
-
kind: string,
|
|
381
|
-
data: Record<string, string>,
|
|
382
|
-
text: string,
|
|
383
|
-
output: OutputContext,
|
|
384
|
-
stdout: Writer,
|
|
385
|
-
): number {
|
|
386
|
-
const renderable: Renderable<Record<string, string>> = { kind, data, pretty: () => text, plain: () => text };
|
|
387
|
-
emit(renderable, output, stdout);
|
|
388
|
-
return EXIT_OK;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Route a parsed invocation to its command handler, throwing a `usage` error on bad input. Returns
|
|
393
|
-
* a `number` for the synchronous commands and a `Promise<number>` for the async
|
|
394
|
-
* ones — external checks, Backlog-backed mutations, and indexed
|
|
395
|
-
* graph/query/context retrieval.
|
|
396
|
-
*/
|
|
397
|
-
type CommandHandler = (args: readonly string[], context: RunContext, output: OutputContext) => number | Promise<number>;
|
|
398
|
-
|
|
399
|
-
/** Handler registry; Commander/manifest own routing declarations, this table only injects Lore seams. */
|
|
400
|
-
const COMMAND_HANDLERS: Readonly<Record<string, CommandHandler>> = {
|
|
401
|
-
init: (args, context, output) => {
|
|
402
|
-
const root = context.cwd || process.cwd();
|
|
403
|
-
return runInit({
|
|
404
|
-
root,
|
|
405
|
-
output,
|
|
406
|
-
args,
|
|
407
|
-
stdout: context.stdout,
|
|
408
|
-
stderr: context.stderr,
|
|
409
|
-
stdinIsTTY: context.stdinIsTTY,
|
|
410
|
-
stderrIsTTY: context.stderrIsTTY,
|
|
411
|
-
jsonRequested: output.mode === "json",
|
|
412
|
-
adapter: context.adapter,
|
|
413
|
-
prompter: context.prompter,
|
|
414
|
-
agentAvailability: context.agentAvailability,
|
|
415
|
-
});
|
|
416
|
-
},
|
|
417
|
-
new: (args, context, output) =>
|
|
418
|
-
runNew({
|
|
419
|
-
root: context.cwd || process.cwd(),
|
|
420
|
-
output,
|
|
421
|
-
args,
|
|
422
|
-
stdout: context.stdout,
|
|
423
|
-
stderr: context.stderr,
|
|
424
|
-
}),
|
|
425
|
-
validate: (args, context, output) =>
|
|
426
|
-
runValidate({
|
|
427
|
-
root: context.cwd || process.cwd(),
|
|
428
|
-
output,
|
|
429
|
-
args,
|
|
430
|
-
stdout: context.stdout,
|
|
431
|
-
stderr: context.stderr,
|
|
432
|
-
}),
|
|
433
|
-
check: (args, context, output) =>
|
|
434
|
-
runCheck({
|
|
435
|
-
root: context.cwd || process.cwd(),
|
|
436
|
-
output,
|
|
437
|
-
args,
|
|
438
|
-
stdout: context.stdout,
|
|
439
|
-
stderr: context.stderr,
|
|
440
|
-
fetch: context.fetch,
|
|
441
|
-
resolveHost: context.resolveHost,
|
|
442
|
-
adapter: context.adapter,
|
|
443
|
-
}),
|
|
444
|
-
replace: (args, context, output) =>
|
|
445
|
-
runReplace({
|
|
446
|
-
root: context.cwd || process.cwd(),
|
|
447
|
-
output,
|
|
448
|
-
args,
|
|
449
|
-
stdout: context.stdout,
|
|
450
|
-
stderr: context.stderr,
|
|
451
|
-
}),
|
|
452
|
-
rename: (args, context, output) =>
|
|
453
|
-
runRename({
|
|
454
|
-
root: context.cwd || process.cwd(),
|
|
455
|
-
output,
|
|
456
|
-
args,
|
|
457
|
-
stdout: context.stdout,
|
|
458
|
-
stderr: context.stderr,
|
|
459
|
-
adapter: context.adapter,
|
|
460
|
-
}),
|
|
461
|
-
supersede: (args, context, output) =>
|
|
462
|
-
runSupersede({
|
|
463
|
-
root: context.cwd || process.cwd(),
|
|
464
|
-
output,
|
|
465
|
-
args,
|
|
466
|
-
stdout: context.stdout,
|
|
467
|
-
stderr: context.stderr,
|
|
468
|
-
}),
|
|
469
|
-
link: (args, context, output) =>
|
|
470
|
-
runLink({
|
|
471
|
-
root: context.cwd || process.cwd(),
|
|
472
|
-
output,
|
|
473
|
-
args,
|
|
474
|
-
stdout: context.stdout,
|
|
475
|
-
stderr: context.stderr,
|
|
476
|
-
adapter: context.adapter,
|
|
477
|
-
}),
|
|
478
|
-
unlink: (args, context, output) =>
|
|
479
|
-
runUnlink({
|
|
480
|
-
root: context.cwd || process.cwd(),
|
|
481
|
-
output,
|
|
482
|
-
args,
|
|
483
|
-
stdout: context.stdout,
|
|
484
|
-
stderr: context.stderr,
|
|
485
|
-
adapter: context.adapter,
|
|
486
|
-
}),
|
|
487
|
-
sync: (args, context, output) =>
|
|
488
|
-
runSync({
|
|
489
|
-
root: context.cwd || process.cwd(),
|
|
490
|
-
output,
|
|
491
|
-
args,
|
|
492
|
-
stdout: context.stdout,
|
|
493
|
-
stderr: context.stderr,
|
|
494
|
-
adapter: context.adapter,
|
|
495
|
-
}),
|
|
496
|
-
tasks: (args, context, output) =>
|
|
497
|
-
runTasks({
|
|
498
|
-
root: context.cwd || process.cwd(),
|
|
499
|
-
output,
|
|
500
|
-
args,
|
|
501
|
-
stdout: context.stdout,
|
|
502
|
-
stderr: context.stderr,
|
|
503
|
-
adapter: context.adapter,
|
|
504
|
-
}),
|
|
505
|
-
orphans: (args, context, output) =>
|
|
506
|
-
runOrphans({
|
|
507
|
-
root: context.cwd || process.cwd(),
|
|
508
|
-
output,
|
|
509
|
-
args,
|
|
510
|
-
stdout: context.stdout,
|
|
511
|
-
stderr: context.stderr,
|
|
512
|
-
adapter: context.adapter,
|
|
513
|
-
}),
|
|
514
|
-
schema: (args, context, output) =>
|
|
515
|
-
runSchema({ root: context.cwd || process.cwd(), output, args, stdout: context.stdout }),
|
|
516
|
-
scaffold: (args, context, output) =>
|
|
517
|
-
runScaffold({ root: context.cwd || process.cwd(), output, args, stdout: context.stdout }),
|
|
518
|
-
graph: (args, context, output) =>
|
|
519
|
-
runGraph({
|
|
520
|
-
root: context.cwd || process.cwd(),
|
|
521
|
-
output,
|
|
522
|
-
args,
|
|
523
|
-
stdout: context.stdout,
|
|
524
|
-
stderr: context.stderr,
|
|
525
|
-
adapter: context.adapter,
|
|
526
|
-
retrieval: context.retrieval ?? loadRetrievalGraph,
|
|
527
|
-
}),
|
|
528
|
-
path: (args, context, output) =>
|
|
529
|
-
runPath({
|
|
530
|
-
root: context.cwd || process.cwd(),
|
|
531
|
-
output,
|
|
532
|
-
args,
|
|
533
|
-
stdout: context.stdout,
|
|
534
|
-
stderr: context.stderr,
|
|
535
|
-
adapter: context.adapter,
|
|
536
|
-
retrieval: context.retrieval ?? loadRetrievalGraph,
|
|
537
|
-
}),
|
|
538
|
-
impact: (args, context, output) =>
|
|
539
|
-
runImpact({
|
|
540
|
-
root: context.cwd || process.cwd(),
|
|
541
|
-
output,
|
|
542
|
-
args,
|
|
543
|
-
stdout: context.stdout,
|
|
544
|
-
stderr: context.stderr,
|
|
545
|
-
adapter: context.adapter,
|
|
546
|
-
retrieval: context.retrieval ?? loadRetrievalGraph,
|
|
547
|
-
}),
|
|
548
|
-
snapshot: (args, context, output) =>
|
|
549
|
-
runSnapshot({
|
|
550
|
-
root: context.cwd || process.cwd(),
|
|
551
|
-
output,
|
|
552
|
-
args,
|
|
553
|
-
stdout: context.stdout,
|
|
554
|
-
stderr: context.stderr,
|
|
555
|
-
adapter: context.adapter,
|
|
556
|
-
}),
|
|
557
|
-
changed: (args, context, output) =>
|
|
558
|
-
runChanged({
|
|
559
|
-
root: context.cwd || process.cwd(),
|
|
560
|
-
output,
|
|
561
|
-
args,
|
|
562
|
-
stdout: context.stdout,
|
|
563
|
-
stderr: context.stderr,
|
|
564
|
-
adapter: context.adapter,
|
|
565
|
-
}),
|
|
566
|
-
provenance: (args, context, output) =>
|
|
567
|
-
runProvenance({
|
|
568
|
-
root: context.cwd || process.cwd(),
|
|
569
|
-
output,
|
|
570
|
-
args,
|
|
571
|
-
stdout: context.stdout,
|
|
572
|
-
stderr: context.stderr,
|
|
573
|
-
adapter: context.adapter,
|
|
574
|
-
}),
|
|
575
|
-
explorer: (args, context, output) =>
|
|
576
|
-
runExplorer({
|
|
577
|
-
root: context.cwd || process.cwd(),
|
|
578
|
-
output,
|
|
579
|
-
args,
|
|
580
|
-
stdout: context.stdout,
|
|
581
|
-
stderr: context.stderr,
|
|
582
|
-
adapter: context.adapter,
|
|
583
|
-
}),
|
|
584
|
-
export: (args, context, output) =>
|
|
585
|
-
runExport({
|
|
586
|
-
root: context.cwd || process.cwd(),
|
|
587
|
-
output,
|
|
588
|
-
args,
|
|
589
|
-
stdout: context.stdout,
|
|
590
|
-
stderr: context.stderr,
|
|
591
|
-
adapter: context.adapter,
|
|
592
|
-
}),
|
|
593
|
-
query: (args, context, output) =>
|
|
594
|
-
runQuery({
|
|
595
|
-
root: context.cwd || process.cwd(),
|
|
596
|
-
output,
|
|
597
|
-
args,
|
|
598
|
-
stdout: context.stdout,
|
|
599
|
-
stderr: context.stderr,
|
|
600
|
-
adapter: context.adapter,
|
|
601
|
-
retrieval: context.retrieval ?? loadRetrievalGraph,
|
|
602
|
-
}),
|
|
603
|
-
context: (args, context, output) =>
|
|
604
|
-
runContext({
|
|
605
|
-
root: context.cwd || process.cwd(),
|
|
606
|
-
output,
|
|
607
|
-
args,
|
|
608
|
-
stdout: context.stdout,
|
|
609
|
-
stderr: context.stderr,
|
|
610
|
-
adapter: context.adapter,
|
|
611
|
-
retrieval: context.retrieval ?? loadRetrievalGraph,
|
|
612
|
-
}),
|
|
613
|
-
agent: (args, context, output) =>
|
|
614
|
-
runAgent({
|
|
615
|
-
root: context.cwd || process.cwd(),
|
|
616
|
-
output,
|
|
617
|
-
args,
|
|
618
|
-
stdout: context.stdout,
|
|
619
|
-
stderr: context.stderr,
|
|
620
|
-
adapter: context.adapter,
|
|
621
|
-
retrieval: context.retrieval ?? loadRetrievalGraph,
|
|
622
|
-
}),
|
|
623
|
-
instructions: (args, context, output) => runInstructions({ output, args, stdout: context.stdout }),
|
|
624
|
-
agents: (args, context, output) =>
|
|
625
|
-
runAgents({ root: context.cwd || process.cwd(), output, args, stdout: context.stdout }),
|
|
626
|
-
help: (args, context, output) => runHelp({ output, args, stdout: context.stdout }),
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
/** Command-handler registry names in declarative dispatch order (test/bridge lockstep seam). */
|
|
630
|
-
export function commandHandlerNames(): readonly string[] {
|
|
631
|
-
return Object.keys(COMMAND_HANDLERS);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function dispatch(parsed: ParsedArgs, context: RunContext, output: OutputContext): number | Promise<number> {
|
|
635
|
-
const root = context.cwd || process.cwd();
|
|
636
|
-
const handler = COMMAND_HANDLERS[parsed.command];
|
|
637
|
-
if (handler === undefined) {
|
|
638
|
-
throw new LoreError("usage", `unknown command "${parsed.command}"`, "run `lore --help` to list commands", {
|
|
639
|
-
command: parsed.command,
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
return handler(parsed.commandArgs, { ...context, cwd: root }, output);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Only drive the real process when executed directly (not when imported by tests). `run` returns a
|
|
646
|
-
// number for synchronous commands and a Promise for the async ones; it funnels
|
|
647
|
-
// its own async rejections through `reportError`, so `Promise.resolve(...).then`
|
|
648
|
-
// normally receives a numeric exit code. The `.catch` is a last-ditch backstop
|
|
649
|
-
// (e.g. `reportError` itself throwing) — `EXIT_UNCAUGHT` (1), the uncaught-fault
|
|
650
|
-
// code, not the validation gate's `6`.
|
|
651
|
-
//
|
|
652
|
-
// Setting `process.exitCode` rather than calling `process.exit()` is deliberate (LORE-70):
|
|
653
|
-
// `emit`/`reportError`'s writes to `process.stdout`/`process.stderr` are async for a piped
|
|
654
|
-
// destination, and `process.exit()` tears the process down without waiting for them to
|
|
655
|
-
// drain — a large `--json` payload silently truncates at the pipe's internal buffer size
|
|
656
|
-
// with a misleading exit code 0. Leaving the exit to the runtime lets pending writes (and
|
|
657
|
-
// any other pending I/O) finish naturally before the process ends with the recorded code.
|
|
658
|
-
if (import.meta.main) {
|
|
659
|
-
Promise.resolve(run(process.argv)).then(
|
|
660
|
-
(code) => {
|
|
661
|
-
process.exitCode = code;
|
|
662
|
-
},
|
|
663
|
-
() => {
|
|
664
|
-
process.exitCode = EXIT_UNCAUGHT;
|
|
665
|
-
},
|
|
666
|
-
);
|
|
667
|
-
}
|