@opum-ai/lore 0.1.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/LICENSE +21 -0
- package/README.md +306 -0
- package/bin/lore.cjs +109 -0
- package/package.json +67 -0
- package/src/adapters/backlog.ts +1084 -0
- package/src/adapters/git.ts +221 -0
- package/src/cli.ts +667 -0
- package/src/commands/agent.ts +301 -0
- package/src/commands/agents.ts +302 -0
- package/src/commands/args.ts +209 -0
- package/src/commands/changed.ts +70 -0
- package/src/commands/check.ts +1031 -0
- package/src/commands/codex-bridge.ts +49 -0
- package/src/commands/concurrency.ts +48 -0
- package/src/commands/context.ts +292 -0
- package/src/commands/discover.ts +89 -0
- package/src/commands/explorer.ts +253 -0
- package/src/commands/export.ts +93 -0
- package/src/commands/fswrite.ts +928 -0
- package/src/commands/graph.ts +291 -0
- package/src/commands/help.ts +151 -0
- package/src/commands/impact.ts +59 -0
- package/src/commands/init.ts +583 -0
- package/src/commands/instructions.ts +91 -0
- package/src/commands/link.ts +929 -0
- package/src/commands/new.ts +476 -0
- package/src/commands/orphans.ts +457 -0
- package/src/commands/path.ts +67 -0
- package/src/commands/provenance.ts +68 -0
- package/src/commands/query.ts +312 -0
- package/src/commands/reconcile-shared.ts +280 -0
- package/src/commands/rename.ts +585 -0
- package/src/commands/replace.ts +320 -0
- package/src/commands/scaffold.ts +346 -0
- package/src/commands/schema.ts +293 -0
- package/src/commands/snapshot.ts +130 -0
- package/src/commands/supersede.ts +400 -0
- package/src/commands/sync.ts +371 -0
- package/src/commands/tasks.ts +271 -0
- package/src/commands/traversal.ts +151 -0
- package/src/commands/validate.ts +226 -0
- package/src/config.ts +598 -0
- package/src/core/agent-bridge.ts +287 -0
- package/src/core/agent-context.ts +498 -0
- package/src/core/agent-profile.ts +447 -0
- package/src/core/bundle.ts +893 -0
- package/src/core/check.ts +853 -0
- package/src/core/codex-bridge.ts +100 -0
- package/src/core/concept.ts +597 -0
- package/src/core/consumer-scaffold.ts +433 -0
- package/src/core/context.ts +271 -0
- package/src/core/explorer-contract.ts +441 -0
- package/src/core/explorer-qualification.ts +58 -0
- package/src/core/explorer.ts +518 -0
- package/src/core/finding.ts +31 -0
- package/src/core/graph.ts +201 -0
- package/src/core/indexes.ts +436 -0
- package/src/core/instructions.ts +209 -0
- package/src/core/ladybug-driver.ts +1795 -0
- package/src/core/ladybug-lifecycle.ts +1178 -0
- package/src/core/ladybug-native.ts +95 -0
- package/src/core/ladybug-source.ts +667 -0
- package/src/core/links.ts +681 -0
- package/src/core/log.ts +253 -0
- package/src/core/managed-block.ts +540 -0
- package/src/core/manifest.ts +718 -0
- package/src/core/order.ts +13 -0
- package/src/core/profile.ts +1007 -0
- package/src/core/projection.ts +195 -0
- package/src/core/query.ts +542 -0
- package/src/core/reconcile.ts +236 -0
- package/src/core/replace.ts +419 -0
- package/src/core/retrieval.ts +213 -0
- package/src/core/rewrite.ts +940 -0
- package/src/core/scaffold.ts +255 -0
- package/src/core/schema.ts +366 -0
- package/src/core/snapshot-runtime.ts +52 -0
- package/src/core/snapshot-store.ts +287 -0
- package/src/core/snapshot.ts +711 -0
- package/src/core/template.ts +429 -0
- package/src/core/traversal.ts +487 -0
- package/src/core/validate.ts +517 -0
- package/src/core/workspace-contract.ts +473 -0
- package/src/core/workspace-projection.ts +365 -0
- package/src/core/workspace-retrieval.ts +196 -0
- package/src/core/workspace-source.ts +174 -0
- package/src/errors.ts +697 -0
- package/src/meta.ts +7 -0
- package/src/output.ts +589 -0
- package/src/scripts/upstream-backlog-watch.ts +288 -0
- package/src/state.ts +390 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/** `lore explorer [--out <file>] [--force]` — deterministic offline graph artifact generation. */
|
|
2
|
+
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import type { BacklogAdapter } from "../adapters/backlog";
|
|
5
|
+
import {
|
|
6
|
+
buildExplorerChangeSnapshot,
|
|
7
|
+
buildExplorerSnapshot,
|
|
8
|
+
EXPLORER_ARTIFACT_VERSION,
|
|
9
|
+
explorerArtifactDigest,
|
|
10
|
+
renderExplorerArtifact,
|
|
11
|
+
renderExplorerChangeArtifact,
|
|
12
|
+
} from "../core/explorer";
|
|
13
|
+
import type { ExplorerSnapshot } from "../core/explorer-contract";
|
|
14
|
+
import { EXPECTED_LADYBUG_STORAGE_VERSION, EXPECTED_LADYBUG_VERSION } from "../core/ladybug-native";
|
|
15
|
+
import { loadLadybugProjectionSource } from "../core/ladybug-source";
|
|
16
|
+
import type { RetainedSnapshot } from "../core/snapshot";
|
|
17
|
+
import { resolveSnapshotScope } from "../core/snapshot-runtime";
|
|
18
|
+
import { loadSnapshot as loadStoredSnapshot } from "../core/snapshot-store";
|
|
19
|
+
import { EXIT_OK, LoreError, WarningCollector, type Writer } from "../errors";
|
|
20
|
+
import { emit, type OutputContext, type Renderable } from "../output";
|
|
21
|
+
import { assertFlagAtMostOnce, parseCommandArgs, singleOptionValue, usage, workspaceSelection } from "./args";
|
|
22
|
+
import { assertNoSymlinkInPath, classifyExistingFile, ensureDir, writeFileAtomic } from "./fswrite";
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_EXPLORER_ARTIFACT_PATH = ".lore/explorer/index.html";
|
|
25
|
+
|
|
26
|
+
export interface ExplorerCommandOptions {
|
|
27
|
+
readonly root: string;
|
|
28
|
+
readonly output: OutputContext;
|
|
29
|
+
readonly args: readonly string[];
|
|
30
|
+
readonly stdout?: Writer;
|
|
31
|
+
readonly stderr?: Writer;
|
|
32
|
+
readonly adapter?: BacklogAdapter;
|
|
33
|
+
readonly loadSnapshot?: ExplorerSnapshotLoader;
|
|
34
|
+
readonly loadRetainedSnapshot?: (selector: string) => RetainedSnapshot | Promise<RetainedSnapshot>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ExplorerSnapshotLoaderOptions {
|
|
38
|
+
readonly root: string;
|
|
39
|
+
readonly adapter?: BacklogAdapter;
|
|
40
|
+
readonly warnings: WarningCollector;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ExplorerSnapshotLoader = (options: ExplorerSnapshotLoaderOptions) => Promise<ExplorerSnapshot>;
|
|
44
|
+
|
|
45
|
+
export interface ExplorerArtifactResult {
|
|
46
|
+
readonly artifactVersion: typeof EXPLORER_ARTIFACT_VERSION;
|
|
47
|
+
readonly snapshotSchemaVersion: string;
|
|
48
|
+
readonly snapshotKey: string;
|
|
49
|
+
readonly path: string;
|
|
50
|
+
readonly action: "created" | "updated" | "unchanged";
|
|
51
|
+
readonly byteLength: number;
|
|
52
|
+
readonly digest: string;
|
|
53
|
+
readonly counts: ExplorerSnapshot["health"]["counts"];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ExplorerArgs {
|
|
57
|
+
readonly out: string;
|
|
58
|
+
readonly customOut: boolean;
|
|
59
|
+
readonly force: boolean;
|
|
60
|
+
readonly snapshot?: string;
|
|
61
|
+
readonly from?: string;
|
|
62
|
+
readonly to?: string;
|
|
63
|
+
readonly workspace?: { readonly manifestPath: string; readonly memberIds: readonly string[] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runExplorer(options: ExplorerCommandOptions): Promise<number> {
|
|
67
|
+
const parsed = parseExplorerArgs(options.args);
|
|
68
|
+
const target = confineArtifactPath(parsed.out, options.root);
|
|
69
|
+
const warnings = new WarningCollector();
|
|
70
|
+
let html: string;
|
|
71
|
+
let schemaVersion: string;
|
|
72
|
+
let snapshotKey: string;
|
|
73
|
+
let counts: ExplorerSnapshot["health"]["counts"];
|
|
74
|
+
if (parsed.snapshot !== undefined || parsed.from !== undefined) {
|
|
75
|
+
let scope: Awaited<ReturnType<typeof resolveSnapshotScope>> | undefined;
|
|
76
|
+
const loadRetained = async (selector: string): Promise<RetainedSnapshot> => {
|
|
77
|
+
if (options.loadRetainedSnapshot !== undefined) return options.loadRetainedSnapshot(selector);
|
|
78
|
+
scope ??= await resolveSnapshotScope({
|
|
79
|
+
root: options.root,
|
|
80
|
+
selection: parsed.workspace === undefined ? {} : { workspace: parsed.workspace.manifestPath },
|
|
81
|
+
warnings,
|
|
82
|
+
adapter: options.adapter,
|
|
83
|
+
});
|
|
84
|
+
return loadStoredSnapshot(options.root, scope, selector);
|
|
85
|
+
};
|
|
86
|
+
const from = await loadRetained(parsed.snapshot ?? (parsed.from as string));
|
|
87
|
+
const to = parsed.snapshot !== undefined ? from : await loadRetained(parsed.to as string);
|
|
88
|
+
const retained = buildExplorerChangeSnapshot(from, to, {
|
|
89
|
+
mode: parsed.snapshot !== undefined ? "snapshot" : "comparison",
|
|
90
|
+
repositories: parsed.workspace?.memberIds ?? [],
|
|
91
|
+
});
|
|
92
|
+
html = renderExplorerChangeArtifact(retained);
|
|
93
|
+
schemaVersion = retained.schemaVersion;
|
|
94
|
+
snapshotKey = retained.to.snapshotKey;
|
|
95
|
+
counts = retainedCounts(retained.to, parsed.workspace?.memberIds ?? []);
|
|
96
|
+
} else {
|
|
97
|
+
const snapshot = await (options.loadSnapshot ?? loadProductionSnapshot)({
|
|
98
|
+
root: options.root,
|
|
99
|
+
adapter: options.adapter,
|
|
100
|
+
warnings,
|
|
101
|
+
});
|
|
102
|
+
html = renderExplorerArtifact(snapshot);
|
|
103
|
+
schemaVersion = snapshot.schemaVersion;
|
|
104
|
+
snapshotKey = snapshot.source.snapshotKey;
|
|
105
|
+
counts = snapshot.health.counts;
|
|
106
|
+
}
|
|
107
|
+
warnings.flush({ color: options.output.color, stderr: options.stderr });
|
|
108
|
+
assertNoSymlinkInPath(options.root, target.relPath);
|
|
109
|
+
const existing = classifyExistingFile(target.absPath, html);
|
|
110
|
+
if (parsed.customOut && existing === "differs" && !parsed.force) {
|
|
111
|
+
throw new LoreError(
|
|
112
|
+
"conflict",
|
|
113
|
+
`cannot overwrite differing explorer artifact ${target.relPath}`,
|
|
114
|
+
"pass --force to replace it, choose another --out path, or remove the existing file",
|
|
115
|
+
{ path: target.relPath },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const action = existing === "missing" ? "created" : existing === "unchanged" ? "unchanged" : "updated";
|
|
119
|
+
if (action !== "unchanged") {
|
|
120
|
+
const parent = dirname(target.relPath);
|
|
121
|
+
if (parent !== ".") ensureDir(options.root, parent);
|
|
122
|
+
writeFileAtomic(target.absPath, html, target.relPath);
|
|
123
|
+
}
|
|
124
|
+
const data: ExplorerArtifactResult = {
|
|
125
|
+
artifactVersion: EXPLORER_ARTIFACT_VERSION,
|
|
126
|
+
snapshotSchemaVersion: schemaVersion,
|
|
127
|
+
snapshotKey,
|
|
128
|
+
path: target.relPath,
|
|
129
|
+
action,
|
|
130
|
+
byteLength: Buffer.byteLength(html),
|
|
131
|
+
digest: explorerArtifactDigest(html),
|
|
132
|
+
counts,
|
|
133
|
+
};
|
|
134
|
+
emit(explorerRenderable(data), options.output, options.stdout);
|
|
135
|
+
return EXIT_OK;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function loadProductionSnapshot(options: ExplorerSnapshotLoaderOptions): Promise<ExplorerSnapshot> {
|
|
139
|
+
const source = await loadLadybugProjectionSource({
|
|
140
|
+
root: options.root,
|
|
141
|
+
ladybugVersion: EXPECTED_LADYBUG_VERSION,
|
|
142
|
+
ladybugStorageVersion: EXPECTED_LADYBUG_STORAGE_VERSION,
|
|
143
|
+
adapter: options.adapter,
|
|
144
|
+
warnings: options.warnings,
|
|
145
|
+
});
|
|
146
|
+
return buildExplorerSnapshot(source);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseExplorerArgs(args: readonly string[]): ExplorerArgs {
|
|
150
|
+
const parsed = parseCommandArgs(args, "explorer");
|
|
151
|
+
assertFlagAtMostOnce(parsed, "out");
|
|
152
|
+
assertFlagAtMostOnce(parsed, "force");
|
|
153
|
+
const workspace = workspaceSelection(parsed);
|
|
154
|
+
if (parsed.positionals.length > 0) {
|
|
155
|
+
throw usage(`unexpected argument "${parsed.positionals[0]}"`, "run `lore explorer [--out <file>] [--force]`");
|
|
156
|
+
}
|
|
157
|
+
const out = singleOptionValue(parsed, "out");
|
|
158
|
+
const snapshot = nonEmptySelector(singleOptionValue(parsed, "snapshot"), "snapshot");
|
|
159
|
+
const from = nonEmptySelector(singleOptionValue(parsed, "from"), "from");
|
|
160
|
+
const to = nonEmptySelector(singleOptionValue(parsed, "to"), "to");
|
|
161
|
+
if (out === "") throw usage("--out needs a value", "pass a repository-relative .html file path");
|
|
162
|
+
if (snapshot !== undefined && (from !== undefined || to !== undefined)) {
|
|
163
|
+
throw usage(
|
|
164
|
+
"--snapshot is mutually exclusive with --from and --to",
|
|
165
|
+
"choose a retained snapshot view or a comparison",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if ((from === undefined) !== (to === undefined)) {
|
|
169
|
+
throw usage("--from and --to must be supplied together", "pass both retained snapshot selectors");
|
|
170
|
+
}
|
|
171
|
+
if (workspace !== undefined && snapshot === undefined && from === undefined) {
|
|
172
|
+
throw usage("--workspace requires retained snapshot selectors", "pass --snapshot or both --from and --to");
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
out: out ?? DEFAULT_EXPLORER_ARTIFACT_PATH,
|
|
176
|
+
customOut: out !== undefined,
|
|
177
|
+
force: parsed.flags.has("force"),
|
|
178
|
+
...(snapshot !== undefined ? { snapshot } : {}),
|
|
179
|
+
...(from !== undefined ? { from, to: to as string } : {}),
|
|
180
|
+
...(workspace !== undefined ? { workspace } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function nonEmptySelector(value: string | undefined, flag: string): string | undefined {
|
|
185
|
+
if (value === undefined) return undefined;
|
|
186
|
+
if (value.trim() === "")
|
|
187
|
+
throw usage(`--${flag} needs a value`, "pass an exact retained snapshot key or unambiguous commit");
|
|
188
|
+
return value.trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function retainedCounts(
|
|
192
|
+
snapshot: RetainedSnapshot,
|
|
193
|
+
repositories: readonly string[],
|
|
194
|
+
): ExplorerSnapshot["health"]["counts"] {
|
|
195
|
+
const selected = new Set(repositories);
|
|
196
|
+
const facts = snapshot.facts.filter(
|
|
197
|
+
(fact) => selected.size === 0 || (fact.provenance.memberId !== null && selected.has(fact.provenance.memberId)),
|
|
198
|
+
);
|
|
199
|
+
const edgeFingerprints = new Set<string>();
|
|
200
|
+
let danglingEdges = 0;
|
|
201
|
+
let duplicateEdges = 0;
|
|
202
|
+
for (const fact of facts) {
|
|
203
|
+
if (fact.kind !== "edge") continue;
|
|
204
|
+
if (fact.value.dangling === true) danglingEdges += 1;
|
|
205
|
+
const fingerprint = [fact.value.from, fact.value.kind, fact.value.target].join("\0");
|
|
206
|
+
if (edgeFingerprints.has(fingerprint)) duplicateEdges += 1;
|
|
207
|
+
edgeFingerprints.add(fingerprint);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
repositories:
|
|
211
|
+
selected.size === 0
|
|
212
|
+
? snapshot.repositories.length
|
|
213
|
+
: snapshot.repositories.filter(
|
|
214
|
+
(repository) => repository.memberId !== null && selected.has(repository.memberId),
|
|
215
|
+
).length,
|
|
216
|
+
concepts: facts.filter((fact) => fact.kind === "concept").length,
|
|
217
|
+
tasks: facts.filter((fact) => fact.kind === "task").length,
|
|
218
|
+
authoredEdges: facts.filter((fact) => fact.kind === "edge").length,
|
|
219
|
+
danglingEdges,
|
|
220
|
+
duplicateEdges,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function confineArtifactPath(path: string, root: string): { readonly absPath: string; readonly relPath: string } {
|
|
225
|
+
const absPath = resolve(root, path);
|
|
226
|
+
const rel = relative(root, absPath);
|
|
227
|
+
if (isAbsolute(path) || rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
228
|
+
throw usage(`--out path "${path}" must name a file inside the repo`, "give --out a repo-relative .html file path");
|
|
229
|
+
}
|
|
230
|
+
const relPath = rel.split(sep).join("/");
|
|
231
|
+
const first = relPath.split("/")[0]?.toLowerCase();
|
|
232
|
+
if (first === "docs" || first === "backlog" || first === ".git") {
|
|
233
|
+
throw usage(
|
|
234
|
+
`--out path "${path}" targets protected repository source`,
|
|
235
|
+
"write the derived artifact outside docs/, backlog/, and .git/, e.g. .lore/explorer/index.html",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (!relPath.toLowerCase().endsWith(".html")) {
|
|
239
|
+
throw usage(`--out path "${path}" must end in .html`, "pass a self-contained HTML artifact path");
|
|
240
|
+
}
|
|
241
|
+
return { absPath, relPath };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function explorerRenderable(data: ExplorerArtifactResult): Renderable<ExplorerArtifactResult> {
|
|
245
|
+
return { kind: "explorer.artifact", data, pretty: renderText, plain: renderText };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function renderText(data: ExplorerArtifactResult): string {
|
|
249
|
+
return [
|
|
250
|
+
`${data.action} ${data.path} (${data.byteLength} bytes, ${data.artifactVersion})`,
|
|
251
|
+
`snapshot ${data.snapshotKey}: ${data.counts.concepts} concepts, ${data.counts.tasks} tasks, ${data.counts.authoredEdges} edges`,
|
|
252
|
+
].join("\n");
|
|
253
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** `lore export --schema-version 1.0` — deterministic JSONL OKF projection. */
|
|
2
|
+
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { type BacklogAdapter, bunBacklogSpawn, createBacklogAdapter } from "../adapters/backlog";
|
|
5
|
+
import { resolveHeadSha } from "../adapters/git";
|
|
6
|
+
import { loadBundle } from "../core/bundle";
|
|
7
|
+
import { loadProfile } from "../core/profile";
|
|
8
|
+
import { buildProjection, PROJECTION_SCHEMA_VERSION } from "../core/projection";
|
|
9
|
+
import { DOCS_DIR } from "../core/scaffold";
|
|
10
|
+
import { EXIT_OK, LoreError, WarningCollector, type Writer } from "../errors";
|
|
11
|
+
import { VERSION } from "../meta";
|
|
12
|
+
import { emit, type OutputContext, type Renderable } from "../output";
|
|
13
|
+
import { parseCommandArgs, singleOptionValue } from "./args";
|
|
14
|
+
|
|
15
|
+
export interface ExportOptions {
|
|
16
|
+
readonly root: string;
|
|
17
|
+
readonly output: OutputContext;
|
|
18
|
+
readonly args: readonly string[];
|
|
19
|
+
readonly stdout?: Writer;
|
|
20
|
+
readonly stderr?: Writer;
|
|
21
|
+
readonly adapter?: BacklogAdapter;
|
|
22
|
+
readonly resolveGitCommit?: (root: string) => string | null;
|
|
23
|
+
readonly generatedAt?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runExport(options: ExportOptions): Promise<number> {
|
|
27
|
+
const schemaVersion = parseExportArgs(options.args, options.output);
|
|
28
|
+
// Everything below this line may touch bundle, Backlog, or Git state. Keeping
|
|
29
|
+
// version parsing above it makes unsupported breaking versions fail first.
|
|
30
|
+
const profile = loadProfile({ root: options.root });
|
|
31
|
+
const warnings = new WarningCollector();
|
|
32
|
+
const graph = loadBundle(join(options.root, DOCS_DIR), { warnings, profile });
|
|
33
|
+
warnings.flush({ color: options.output.color, stderr: options.stderr });
|
|
34
|
+
const adapter = options.adapter ?? createBacklogAdapter(bunBacklogSpawn(undefined, options.root));
|
|
35
|
+
const tasks = await adapter.listTasks();
|
|
36
|
+
const gitCommit = (options.resolveGitCommit ?? resolveHeadSha)(options.root);
|
|
37
|
+
const projection = buildProjection({
|
|
38
|
+
graph,
|
|
39
|
+
tasks,
|
|
40
|
+
docsRoot: DOCS_DIR,
|
|
41
|
+
okfVersion: profile.okfVersion,
|
|
42
|
+
exporterVersion: VERSION,
|
|
43
|
+
gitCommit,
|
|
44
|
+
generatedAt:
|
|
45
|
+
options.generatedAt !== undefined ? options.generatedAt : sourceDateEpoch(process.env.SOURCE_DATE_EPOCH),
|
|
46
|
+
});
|
|
47
|
+
if (options.output.mode === "json") {
|
|
48
|
+
const data = { projectionSchemaVersion: schemaVersion, records: projection.records };
|
|
49
|
+
const renderable: Renderable<typeof data> = {
|
|
50
|
+
kind: "projection.export",
|
|
51
|
+
data,
|
|
52
|
+
pretty: () => projection.jsonl,
|
|
53
|
+
plain: () => projection.jsonl,
|
|
54
|
+
};
|
|
55
|
+
emit(renderable, options.output, options.stdout);
|
|
56
|
+
} else {
|
|
57
|
+
(options.stdout ?? process.stdout).write(projection.jsonl);
|
|
58
|
+
}
|
|
59
|
+
return EXIT_OK;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseExportArgs(args: readonly string[], output: OutputContext): string {
|
|
63
|
+
void output;
|
|
64
|
+
const parsed = parseCommandArgs(args, "export");
|
|
65
|
+
if (parsed.positionals.length > 0) {
|
|
66
|
+
throw usage(`unexpected argument "${parsed.positionals[0]}"`, "run `lore export --help` to list options");
|
|
67
|
+
}
|
|
68
|
+
const rawVersion = singleOptionValue(parsed, "schema-version");
|
|
69
|
+
if (rawVersion === "") {
|
|
70
|
+
throw usage("--schema-version needs a value", `pass --schema-version ${PROJECTION_SCHEMA_VERSION}`);
|
|
71
|
+
}
|
|
72
|
+
const version = rawVersion ?? PROJECTION_SCHEMA_VERSION;
|
|
73
|
+
if (version !== PROJECTION_SCHEMA_VERSION) {
|
|
74
|
+
throw usage(
|
|
75
|
+
`unsupported projection schema version "${version}"`,
|
|
76
|
+
`this lore supports ${PROJECTION_SCHEMA_VERSION}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return version;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sourceDateEpoch(raw: string | undefined): string | null {
|
|
83
|
+
if (raw === undefined || raw.trim() === "") return null;
|
|
84
|
+
const seconds = Number(raw);
|
|
85
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
86
|
+
throw usage("SOURCE_DATE_EPOCH must be a non-negative number", "unset it or provide Unix epoch seconds");
|
|
87
|
+
}
|
|
88
|
+
return new Date(seconds * 1000).toISOString();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function usage(message: string, hint: string): LoreError {
|
|
92
|
+
return new LoreError("usage", message, hint);
|
|
93
|
+
}
|