@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.
Files changed (90) hide show
  1. package/README.md +33 -22
  2. package/bin/lore.cjs +35 -7
  3. package/package.json +17 -17
  4. package/src/adapters/backlog.ts +0 -1084
  5. package/src/adapters/git.ts +0 -221
  6. package/src/cli.ts +0 -667
  7. package/src/commands/agent.ts +0 -301
  8. package/src/commands/agents.ts +0 -302
  9. package/src/commands/args.ts +0 -209
  10. package/src/commands/changed.ts +0 -70
  11. package/src/commands/check.ts +0 -1031
  12. package/src/commands/codex-bridge.ts +0 -49
  13. package/src/commands/concurrency.ts +0 -48
  14. package/src/commands/context.ts +0 -292
  15. package/src/commands/discover.ts +0 -89
  16. package/src/commands/explorer.ts +0 -253
  17. package/src/commands/export.ts +0 -93
  18. package/src/commands/fswrite.ts +0 -928
  19. package/src/commands/graph.ts +0 -291
  20. package/src/commands/help.ts +0 -151
  21. package/src/commands/impact.ts +0 -59
  22. package/src/commands/init.ts +0 -583
  23. package/src/commands/instructions.ts +0 -91
  24. package/src/commands/link.ts +0 -929
  25. package/src/commands/new.ts +0 -476
  26. package/src/commands/orphans.ts +0 -457
  27. package/src/commands/path.ts +0 -67
  28. package/src/commands/provenance.ts +0 -68
  29. package/src/commands/query.ts +0 -312
  30. package/src/commands/reconcile-shared.ts +0 -280
  31. package/src/commands/rename.ts +0 -585
  32. package/src/commands/replace.ts +0 -320
  33. package/src/commands/scaffold.ts +0 -346
  34. package/src/commands/schema.ts +0 -293
  35. package/src/commands/snapshot.ts +0 -130
  36. package/src/commands/supersede.ts +0 -400
  37. package/src/commands/sync.ts +0 -371
  38. package/src/commands/tasks.ts +0 -271
  39. package/src/commands/traversal.ts +0 -151
  40. package/src/commands/validate.ts +0 -226
  41. package/src/config.ts +0 -598
  42. package/src/core/agent-bridge.ts +0 -287
  43. package/src/core/agent-context.ts +0 -498
  44. package/src/core/agent-profile.ts +0 -447
  45. package/src/core/bundle.ts +0 -893
  46. package/src/core/check.ts +0 -853
  47. package/src/core/codex-bridge.ts +0 -100
  48. package/src/core/concept.ts +0 -597
  49. package/src/core/consumer-scaffold.ts +0 -433
  50. package/src/core/context.ts +0 -271
  51. package/src/core/explorer-contract.ts +0 -441
  52. package/src/core/explorer-qualification.ts +0 -58
  53. package/src/core/explorer.ts +0 -518
  54. package/src/core/finding.ts +0 -31
  55. package/src/core/graph.ts +0 -201
  56. package/src/core/indexes.ts +0 -436
  57. package/src/core/instructions.ts +0 -209
  58. package/src/core/ladybug-driver.ts +0 -1795
  59. package/src/core/ladybug-lifecycle.ts +0 -1178
  60. package/src/core/ladybug-native.ts +0 -95
  61. package/src/core/ladybug-source.ts +0 -667
  62. package/src/core/links.ts +0 -681
  63. package/src/core/log.ts +0 -253
  64. package/src/core/managed-block.ts +0 -540
  65. package/src/core/manifest.ts +0 -718
  66. package/src/core/order.ts +0 -13
  67. package/src/core/profile.ts +0 -1007
  68. package/src/core/projection.ts +0 -195
  69. package/src/core/query.ts +0 -542
  70. package/src/core/reconcile.ts +0 -236
  71. package/src/core/replace.ts +0 -419
  72. package/src/core/retrieval.ts +0 -213
  73. package/src/core/rewrite.ts +0 -940
  74. package/src/core/scaffold.ts +0 -255
  75. package/src/core/schema.ts +0 -366
  76. package/src/core/snapshot-runtime.ts +0 -52
  77. package/src/core/snapshot-store.ts +0 -287
  78. package/src/core/snapshot.ts +0 -711
  79. package/src/core/template.ts +0 -429
  80. package/src/core/traversal.ts +0 -487
  81. package/src/core/validate.ts +0 -517
  82. package/src/core/workspace-contract.ts +0 -473
  83. package/src/core/workspace-projection.ts +0 -365
  84. package/src/core/workspace-retrieval.ts +0 -196
  85. package/src/core/workspace-source.ts +0 -174
  86. package/src/errors.ts +0 -697
  87. package/src/meta.ts +0 -7
  88. package/src/output.ts +0 -589
  89. package/src/scripts/upstream-backlog-watch.ts +0 -288
  90. 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
- }