@opum-ai/lore 0.1.0 → 0.2.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/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/commands/schema.ts
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* commands/schema.ts — `lore schema export [--out <dir>] [--type <T>]`.
|
|
3
|
-
*
|
|
4
|
-
* The thin, side-effecting layer that materializes the active profile's editor JSON Schemas
|
|
5
|
-
* (cli-surface §schema; LORE-20). It loads the profile, asks core for the exact bytes per type via
|
|
6
|
-
* the shared {@link emitSchemaFiles} — the **same** emitter `lore init` uses, so an exported schema
|
|
7
|
-
* is byte-identical to the scaffolded one — and writes each file under `--out` (default
|
|
8
|
-
* `.lore/schemas/`), **overwriting** so a re-export refreshes the bytes after a profile change.
|
|
9
|
-
*
|
|
10
|
-
* A **full** export (no `--type`) to the **default** `.lore/schemas/` directory also **prunes** any
|
|
11
|
-
* orphaned `<slug>.schema.json` left there by a type that the profile no longer declares, so
|
|
12
|
-
* `.lore/schemas/` always mirrors the active profile rather than drifting (a stale schema would
|
|
13
|
-
* otherwise keep driving editor validation from a removed type's rules). Pruning never runs against a
|
|
14
|
-
* non-default `--out`: that directory isn't lore-owned, so a pre-existing `*.schema.json` sitting
|
|
15
|
-
* there — including one placed by an unrelated tool — must never be silently deleted. A single-`--type`
|
|
16
|
-
* export touches only that one file and prunes nothing.
|
|
17
|
-
*
|
|
18
|
-
* The emitted Draft-7 schemas are what makes the `# yaml-language-server: $schema=…` modeline `lore
|
|
19
|
-
* new`/`lore init` stamp resolve, driving YAML autocomplete in VS Code/Obsidian (AC#1); because the
|
|
20
|
-
* profile is loaded from the project's `.lore/profile.toml`, a custom type's schema is exported too
|
|
21
|
-
* (AC#2). With no profile present this is the built-in story-convention profile (zero-config). Those
|
|
22
|
-
* stamped modelines point at the default `.lore/schemas/`, so a non-default `--out` is for ad-hoc/CI
|
|
23
|
-
* use — it will not drive the autocomplete of an already-scaffolded bundle.
|
|
24
|
-
*
|
|
25
|
-
* Validation lives here: the sole subcommand is `export` (anything else is a `usage` error, exit 2);
|
|
26
|
-
* a repeated or value-less `--out`/`--type`, an unknown `--type`, or an `--out` that is absolute or
|
|
27
|
-
* escapes the repo is a `usage` error; a non-writable output directory surfaces as a `denied` error
|
|
28
|
-
* (exit 4) from the shared write seam. All file I/O is here ({@link ensureDir}/{@link writeFileNoFollow}/prune);
|
|
29
|
-
* the byte computation stays pure in `core/schema.ts`.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import { readdirSync, rmSync } from "node:fs";
|
|
33
|
-
import { isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
34
|
-
import { type CompiledType, loadProfile } from "../core/profile";
|
|
35
|
-
import { canonicalType, emitSchemaFiles, SCHEMAS_DIR, type SchemaFile } from "../core/schema";
|
|
36
|
-
import { EXIT_OK, LoreError, type Writer } from "../errors";
|
|
37
|
-
import { emit, type OutputContext, type Renderable } from "../output";
|
|
38
|
-
import { parseCommandArgs, singleOptionValue } from "./args";
|
|
39
|
-
import { ensureDir, findSymlinkSegment, ioError, writeFileNoFollow } from "./fswrite";
|
|
40
|
-
|
|
41
|
-
/** Options for {@link runSchema}; `root` and the stream are injectable for tests. */
|
|
42
|
-
export interface SchemaOptions {
|
|
43
|
-
/** The repo root whose `.lore/profile.toml` and output directory resolve against. */
|
|
44
|
-
root: string;
|
|
45
|
-
/** The resolved output mode/color (from `output.ts`). */
|
|
46
|
-
output: OutputContext;
|
|
47
|
-
/** The command's normalized positional + flag tokens from Commander. */
|
|
48
|
-
args: readonly string[];
|
|
49
|
-
/** stdout sink; defaults to `process.stdout`. */
|
|
50
|
-
stdout?: Writer;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** The parsed form of `lore schema`'s arguments. */
|
|
54
|
-
interface SchemaArgs {
|
|
55
|
-
/** The output directory (`--out`); `undefined` means the default {@link SCHEMAS_DIR}. */
|
|
56
|
-
out?: string;
|
|
57
|
-
/** The single type to export (`--type`), raw as the user spelled it; `undefined` means all types. */
|
|
58
|
-
type?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** One written or pruned file, for the report. */
|
|
62
|
-
interface ReportFile {
|
|
63
|
-
/** The repo-relative POSIX path (`<out>/<slug>.schema.json`). */
|
|
64
|
-
readonly path: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The `schema.result` payload: where the schemas went, which were written, and which stale ones were pruned. */
|
|
68
|
-
export interface SchemaExportResult {
|
|
69
|
-
/** The output directory, as given (or the default), for display. */
|
|
70
|
-
readonly out: string;
|
|
71
|
-
/** Every schema file written, in profile-declaration order. */
|
|
72
|
-
readonly files: readonly ReportFile[];
|
|
73
|
-
/** Orphaned `<slug>.schema.json` files removed because no profile type owns them (full export only). */
|
|
74
|
-
readonly removed: readonly ReportFile[];
|
|
75
|
-
/** How many schema files were written (== `files.length`). */
|
|
76
|
-
readonly count: number;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Run `lore schema export`: parse the arguments, load the active profile, compute each type's schema
|
|
81
|
-
* file via the shared pure emitter, write them under `--out` (overwriting), prune any orphaned schema
|
|
82
|
-
* files on a full export, emit the `schema.result`, and return `0`. A bad subcommand/flag, repeated or
|
|
83
|
-
* value-less flag, unknown `--type`, or repo-escaping `--out` throws a `usage` {@link LoreError} (exit
|
|
84
|
-
* `2`); a non-writable output directory a `denied` one (exit `4`).
|
|
85
|
-
*/
|
|
86
|
-
export function runSchema(options: SchemaOptions): number {
|
|
87
|
-
const parsed = parseSchemaArgs(options.args);
|
|
88
|
-
const profile = loadProfile({ root: options.root });
|
|
89
|
-
const outArg = parsed.out ?? SCHEMAS_DIR;
|
|
90
|
-
const absOutDir = confineOutDir(outArg, options.root);
|
|
91
|
-
|
|
92
|
-
let only: CompiledType | undefined;
|
|
93
|
-
if (parsed.type !== undefined) {
|
|
94
|
-
const canonical = canonicalType(parsed.type, profile);
|
|
95
|
-
only = profile.types.get(canonical);
|
|
96
|
-
if (only === undefined) {
|
|
97
|
-
throw new LoreError(
|
|
98
|
-
"usage",
|
|
99
|
-
`no type "${parsed.type}" in the active profile`,
|
|
100
|
-
`available types: ${[...profile.types.keys()].join(", ")}`,
|
|
101
|
-
{ type: parsed.type },
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const files = emitSchemaFiles(profile, { dir: outArg, only });
|
|
107
|
-
ensureDir(options.root, outArg);
|
|
108
|
-
for (const file of files) {
|
|
109
|
-
// `outArg` is confined to the repo, so `file.path` (`<outArg>/<slug>.schema.json`) is repo-relative
|
|
110
|
-
// and `join(root, file.path)` is its absolute target — no need to re-derive the directory. Uses
|
|
111
|
-
// `writeFileNoFollow`, not the plain `writeFileOverwriting`, because `emitSchemaFiles`'s output paths
|
|
112
|
-
// are computed from the profile, not read from an existing file lore just walked — unlike
|
|
113
|
-
// `writeFileOverwriting`'s other callers (`rename`/`supersede`), nothing upstream has ever confirmed
|
|
114
|
-
// `file.path`'s leaf isn't a symlink. A leaf symlink at e.g. `.lore/schemas/story.schema.json`
|
|
115
|
-
// pointing outside the repo would otherwise have a plain `writeFileSync` follow it and silently
|
|
116
|
-
// overwrite whatever it points to (LORE-123); `writeFileNoFollow`'s up-front `lstatSync` refuses
|
|
117
|
-
// that — and its temp-file-then-`renameSync` commit never dereferences a destination symlink
|
|
118
|
-
// either, even one raced in after that check (LORE-130) — with the same `conflict` `LoreError`
|
|
119
|
-
// the ancestor-symlink guard (LORE-93) already throws.
|
|
120
|
-
writeFileNoFollow(join(options.root, file.path), file.contents, file.path);
|
|
121
|
-
}
|
|
122
|
-
// A full export to the managed default directory owns its schema set, so a type dropped from the
|
|
123
|
-
// profile leaves a stale schema behind; prune it there. A single-`--type` export is surgical and
|
|
124
|
-
// never prunes its siblings, and a non-default `--out` is never lore-owned, so it is never pruned
|
|
125
|
-
// either — see `isManagedSchemasDir`.
|
|
126
|
-
const removed =
|
|
127
|
-
only === undefined && isManagedSchemasDir(absOutDir, options.root) ? pruneOrphans(absOutDir, outArg, files) : [];
|
|
128
|
-
|
|
129
|
-
const result: SchemaExportResult = {
|
|
130
|
-
out: outArg,
|
|
131
|
-
files: files.map((file) => ({ path: file.path })),
|
|
132
|
-
removed,
|
|
133
|
-
count: files.length,
|
|
134
|
-
};
|
|
135
|
-
emit(schemaRenderable(result), options.output, options.stdout);
|
|
136
|
-
return EXIT_OK;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* The subset of `node:path` {@link confineOutDir} resolves through, injectable so its win32-specific
|
|
141
|
-
* cross-drive check (see below, LORE-182) can be exercised with real Windows path semantics from a
|
|
142
|
-
* unit test running on any host platform — production call sites never pass this and get the host's
|
|
143
|
-
* own native functions (`HOST_PATH`) unconditionally.
|
|
144
|
-
*/
|
|
145
|
-
type PathOps = Pick<typeof import("node:path"), "resolve" | "relative" | "isAbsolute" | "sep">;
|
|
146
|
-
|
|
147
|
-
/** The host platform's real `node:path` primitives — {@link confineOutDir}'s production default. */
|
|
148
|
-
const HOST_PATH: PathOps = { resolve, relative, isAbsolute, sep };
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Resolve `--out` to an absolute directory, **confined to the repo** (mirroring `lore new`'s
|
|
152
|
-
* `resolveOutPath`): an absolute path or one escaping the root via `..` is a `usage` error, because
|
|
153
|
-
* the writes overwrite-truncate and a typo'd/hostile `--out ../../etc` would otherwise clobber files
|
|
154
|
-
* anywhere on disk. The repo root itself (`--out .`) is allowed.
|
|
155
|
-
*
|
|
156
|
-
* Checks `isAbsolute(out)` on the **raw argument** (LORE-124): `node:path`'s `relative()` only ever
|
|
157
|
-
* returns a relative-looking path (`""`, a `..`-prefixed climb, or a plain relative path) on POSIX, so
|
|
158
|
-
* `isAbsolute(rel)` alone can never be true there — checking the raw argument closes the gap LORE-124
|
|
159
|
-
* found, where an absolute `--out` resolving *inside* the repo slipped past silently. Callers
|
|
160
|
-
* downstream (`runSchema`) still pass the caller's raw `outArg`, not this function's resolved `abs`,
|
|
161
|
-
* into `emitSchemaFiles`/`ensureDir`/`join(root, file.path)`; if a same-inside absolute path were let
|
|
162
|
-
* through, `path.join(root, absOutDir)` would blindly concatenate rather than collapse, double-prefixing
|
|
163
|
-
* the root and creating a bogus directory instead of the real `.lore/schemas` (the unhandled ENOENT
|
|
164
|
-
* `pruneOrphans` then hit). Rejecting every absolute `--out` up front — inside the repo or not — closes
|
|
165
|
-
* that off at the source rather than trying to make every downstream `join` absolute-safe.
|
|
166
|
-
*
|
|
167
|
-
* `isAbsolute(rel)` is ALSO still checked, belt-and-suspenders (restored, LORE-182, after LORE-124
|
|
168
|
-
* called it dead code and dropped it): dead on POSIX per the paragraph above, but very much alive on
|
|
169
|
-
* win32 for a cross-drive **drive-relative** `--out` (Windows syntax like `"C:foo"` — relative to
|
|
170
|
-
* drive C's current directory, distinct from the absolute `"C:\\foo"` form `isAbsolute(out)` already
|
|
171
|
-
* catches). When the repo root is on a different drive (e.g. `"D:\\repo"`), neither `isAbsolute(out)`
|
|
172
|
-
* nor a `".."`-climb check catches `"C:foo"` — but `win32.relative("D:\\repo", "C:\\foo")` returns the
|
|
173
|
-
* target path unchanged (`"C:\\foo"`), which IS absolute, so `isAbsolute(rel)` catches what the other
|
|
174
|
-
* two checks miss. Without this clause the confused input still can't escape (writes stay lexically
|
|
175
|
-
* inside the repo and NTFS rejects the `:` in the eventual filename), so the blast radius is a
|
|
176
|
-
* confusing IO error instead of a clean usage one — this restores the clean error.
|
|
177
|
-
*/
|
|
178
|
-
export function confineOutDir(out: string, root: string, path: PathOps = HOST_PATH): string {
|
|
179
|
-
const abs = path.resolve(root, out);
|
|
180
|
-
const rel = path.relative(root, abs);
|
|
181
|
-
if (path.isAbsolute(out) || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
182
|
-
throw usage(`--out path "${out}" must be inside the repo`, "give a directory path relative to the repo root");
|
|
183
|
-
}
|
|
184
|
-
return abs;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Whether `absOutDir` is the managed default schema directory (`.lore/schemas/`, resolved against
|
|
189
|
-
* `root`) — the only directory lore itself owns the full contents of. Pruning is confined to this
|
|
190
|
-
* directory (see {@link pruneOrphans}'s caller): any other `--out`, including the repo root itself
|
|
191
|
-
* (`--out .`, which {@link confineOutDir} explicitly allows), may contain files lore didn't create and
|
|
192
|
-
* must never be silently deleted.
|
|
193
|
-
*
|
|
194
|
-
* The lexical match alone is not enough: `resolve()` never dereferences symlinks, so a `.lore/schemas`
|
|
195
|
-
* that is itself a symlink (or sits under a symlinked `.lore`) would still compare equal while actually
|
|
196
|
-
* pointing somewhere lore doesn't own — {@link pruneOrphans} would then `rmSync` through it into
|
|
197
|
-
* whatever real directory is on the other end. Reuses {@link findSymlinkSegment}'s existing
|
|
198
|
-
* per-segment `lstatSync` walk (LORE-76/LORE-77's precedent guard, `commands/fswrite.ts`) rather than
|
|
199
|
-
* a fresh check, so a symlinked default directory is treated as unmanaged — pruning is skipped, the
|
|
200
|
-
* same as any other `--out` lore doesn't own.
|
|
201
|
-
*/
|
|
202
|
-
function isManagedSchemasDir(absOutDir: string, root: string): boolean {
|
|
203
|
-
return absOutDir === resolve(root, SCHEMAS_DIR) && findSymlinkSegment(root, SCHEMAS_DIR) === null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Delete every `<name>.schema.json` in `absDir` that the just-written `files` set does not contain —
|
|
208
|
-
* the orphans a removed/renamed profile type left behind — and return them for the report. Only
|
|
209
|
-
* `*.schema.json` is touched, and only ever called for the managed default directory (see
|
|
210
|
-
* {@link isManagedSchemasDir}), so every file this walks is lore-owned; other files are never removed.
|
|
211
|
-
* The directory was just `ensureDir`'d, so a read failure here is a genuine IO fault, mapped via the
|
|
212
|
-
* shared {@link ioError}.
|
|
213
|
-
*/
|
|
214
|
-
function pruneOrphans(absDir: string, displayDir: string, files: readonly SchemaFile[]): ReportFile[] {
|
|
215
|
-
const keep = new Set(files.map((file) => posix.basename(file.path)));
|
|
216
|
-
let entries: string[];
|
|
217
|
-
try {
|
|
218
|
-
entries = readdirSync(absDir);
|
|
219
|
-
} catch (cause) {
|
|
220
|
-
throw ioError(cause, displayDir, "read directory");
|
|
221
|
-
}
|
|
222
|
-
const removed: ReportFile[] = [];
|
|
223
|
-
for (const entry of entries.sort()) {
|
|
224
|
-
if (entry.endsWith(".schema.json") && !keep.has(entry)) {
|
|
225
|
-
const rel = posix.join(displayDir, entry);
|
|
226
|
-
try {
|
|
227
|
-
rmSync(join(absDir, entry));
|
|
228
|
-
} catch (cause) {
|
|
229
|
-
throw ioError(cause, rel, "remove file");
|
|
230
|
-
}
|
|
231
|
-
removed.push({ path: rel });
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return removed;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ── Argument parsing ───────────────────────────────────────────────────────────
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Parse `schema`'s tokens into the `export` subcommand and the value flags `--out <dir>` / `--type
|
|
241
|
-
* <T>` (both also accept the `--flag=value` form). Commander has already resolved Lore's global
|
|
242
|
-
* flags, so a `--`-prefixed token here is a command flag: an unrecognized one is a `usage` error, as
|
|
243
|
-
* is a repeated or value-less flag, a missing/unknown subcommand, or a stray extra positional. A `--`
|
|
244
|
-
* ends option parsing.
|
|
245
|
-
*/
|
|
246
|
-
function parseSchemaArgs(args: readonly string[]): SchemaArgs {
|
|
247
|
-
const parsed = parseCommandArgs(args, "schema");
|
|
248
|
-
const positionals = parsed.positionals;
|
|
249
|
-
const out = singleOptionValue(parsed, "out");
|
|
250
|
-
const type = singleOptionValue(parsed, "type");
|
|
251
|
-
if (out === "") throw missingValue("--out");
|
|
252
|
-
if (type === "") throw missingValue("--type");
|
|
253
|
-
const sub = positionals[0];
|
|
254
|
-
if (sub === undefined) {
|
|
255
|
-
throw usage("`lore schema` needs a subcommand", "the only subcommand is `export`: run `lore schema export`");
|
|
256
|
-
}
|
|
257
|
-
if (sub !== "export") {
|
|
258
|
-
throw usage(`unknown schema subcommand "${sub}"`, "the only subcommand is `export`: run `lore schema export`");
|
|
259
|
-
}
|
|
260
|
-
if (positionals.length > 1) {
|
|
261
|
-
throw usage(`unexpected argument "${positionals[1]}"`, "run `lore schema export [--out <dir>] [--type <T>]`");
|
|
262
|
-
}
|
|
263
|
-
return { out, type };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** The `usage` error a value-less value flag raises, with a flag-appropriate example. */
|
|
267
|
-
function missingValue(flag: string): LoreError {
|
|
268
|
-
return usage(`${flag} needs a value`, `pass a value, e.g. \`${flag} ${flag === "--out" ? "<dir>" : "<Type>"}\``);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ── Output ─────────────────────────────────────────────────────────────────────
|
|
272
|
-
|
|
273
|
-
/** The per-result-type rendering bundle for `schema export` (output.ts dispatches on the mode). */
|
|
274
|
-
function schemaRenderable(data: SchemaExportResult): Renderable<SchemaExportResult> {
|
|
275
|
-
return { kind: "schema.result", data, pretty: render, plain: render };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** One `wrote <path>` line per file, any `removed <path>` lines, then a summary. (No color: no severities.) */
|
|
279
|
-
function render(data: SchemaExportResult): string {
|
|
280
|
-
const lines = data.files.map((file) => `wrote ${file.path}`);
|
|
281
|
-
for (const file of data.removed) {
|
|
282
|
-
lines.push(`removed ${file.path}`);
|
|
283
|
-
}
|
|
284
|
-
const noun = data.count === 1 ? "schema" : "schemas";
|
|
285
|
-
const prunedNote = data.removed.length > 0 ? `, ${data.removed.length} stale removed` : "";
|
|
286
|
-
lines.push(`${data.count} ${noun} exported to ${data.out}${prunedNote}`);
|
|
287
|
-
return lines.join("\n");
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** A `usage` {@link LoreError} (exit `2`) with an actionable hint. */
|
|
291
|
-
function usage(message: string, hint: string): LoreError {
|
|
292
|
-
return new LoreError("usage", message, hint);
|
|
293
|
-
}
|
package/src/commands/snapshot.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/** `lore snapshot`: explicit retained projection lifecycle. */
|
|
2
|
-
|
|
3
|
-
import type { BacklogAdapter } from "../adapters/backlog";
|
|
4
|
-
import type { RetainedSnapshot, RetainedSnapshotDescriptor } from "../core/snapshot";
|
|
5
|
-
import {
|
|
6
|
-
loadCurrentRetainedSnapshot,
|
|
7
|
-
resolveSnapshotScope,
|
|
8
|
-
type SnapshotRuntimeSelection,
|
|
9
|
-
} from "../core/snapshot-runtime";
|
|
10
|
-
import type { SnapshotScopeSelection } from "../core/snapshot-store";
|
|
11
|
-
import { deleteSnapshots, listSnapshots, retainSnapshot } from "../core/snapshot-store";
|
|
12
|
-
import { WarningCollector, type Writer } from "../errors";
|
|
13
|
-
import { emit, type OutputContext, type Renderable } from "../output";
|
|
14
|
-
import { assertFlagAtMostOnce, parseCommandArgs, singleOptionValue, usage } from "./args";
|
|
15
|
-
|
|
16
|
-
export interface SnapshotCommandOptions {
|
|
17
|
-
readonly root: string;
|
|
18
|
-
readonly output: OutputContext;
|
|
19
|
-
readonly args: readonly string[];
|
|
20
|
-
readonly stdout?: Writer;
|
|
21
|
-
readonly stderr?: Writer;
|
|
22
|
-
readonly adapter?: BacklogAdapter;
|
|
23
|
-
readonly loadCurrentSnapshot?: () => RetainedSnapshot | Promise<RetainedSnapshot>;
|
|
24
|
-
readonly resolveScope?: () => SnapshotScopeSelection | Promise<SnapshotScopeSelection>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type SnapshotCommandResult =
|
|
28
|
-
| {
|
|
29
|
-
readonly action: "retained" | "unchanged";
|
|
30
|
-
readonly snapshot: RetainedSnapshotDescriptor;
|
|
31
|
-
readonly retained: number;
|
|
32
|
-
readonly maximum: number;
|
|
33
|
-
}
|
|
34
|
-
| {
|
|
35
|
-
readonly action: "listed";
|
|
36
|
-
readonly snapshots: readonly RetainedSnapshotDescriptor[];
|
|
37
|
-
readonly retained: number;
|
|
38
|
-
readonly maximum: number;
|
|
39
|
-
}
|
|
40
|
-
| { readonly action: "deleted"; readonly deleted: readonly string[] };
|
|
41
|
-
|
|
42
|
-
export async function runSnapshot(options: SnapshotCommandOptions): Promise<number> {
|
|
43
|
-
const parsed = parseCommandArgs(options.args, "snapshot");
|
|
44
|
-
assertFlagAtMostOnce(parsed, "all");
|
|
45
|
-
const action = parsed.positionals[0];
|
|
46
|
-
if (action !== "retain" && action !== "list" && action !== "delete") {
|
|
47
|
-
throw usage("snapshot needs retain, list, or delete", "run `lore snapshot <retain|list|delete> [snapshot-key]`");
|
|
48
|
-
}
|
|
49
|
-
const selection = runtimeSelection(parsed);
|
|
50
|
-
const advisories = new WarningCollector();
|
|
51
|
-
let result: SnapshotCommandResult;
|
|
52
|
-
if (action === "retain") {
|
|
53
|
-
if (parsed.positionals.length !== 1 || parsed.flags.has("all") || selection.workspaceId !== undefined) {
|
|
54
|
-
throw usage(
|
|
55
|
-
"snapshot retain accepts no snapshot key, --all, or --workspace-id",
|
|
56
|
-
"retain the current repository or --workspace manifest snapshot",
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
const snapshot =
|
|
60
|
-
options.loadCurrentSnapshot === undefined
|
|
61
|
-
? await loadCurrentRetainedSnapshot({
|
|
62
|
-
root: options.root,
|
|
63
|
-
selection,
|
|
64
|
-
warnings: advisories,
|
|
65
|
-
adapter: options.adapter,
|
|
66
|
-
})
|
|
67
|
-
: await options.loadCurrentSnapshot();
|
|
68
|
-
result = retainSnapshot(options.root, snapshot);
|
|
69
|
-
} else if (action === "list") {
|
|
70
|
-
if (parsed.positionals.length !== 1 || parsed.flags.has("all"))
|
|
71
|
-
throw usage("snapshot list accepts no snapshot key or --all", "run `lore snapshot list`");
|
|
72
|
-
const scope =
|
|
73
|
-
options.resolveScope === undefined
|
|
74
|
-
? await resolveSnapshotScope({ root: options.root, selection, warnings: advisories, adapter: options.adapter })
|
|
75
|
-
: await options.resolveScope();
|
|
76
|
-
const snapshots = listSnapshots(options.root, scope);
|
|
77
|
-
result = { action: "listed", snapshots, retained: snapshots.length, maximum: 16 };
|
|
78
|
-
} else {
|
|
79
|
-
if (parsed.positionals.length > 2)
|
|
80
|
-
throw usage("snapshot delete accepts one snapshot key", "pass one exact snapshot key or --all");
|
|
81
|
-
const key = parsed.positionals[1];
|
|
82
|
-
if ((key === undefined) !== parsed.flags.has("all"))
|
|
83
|
-
throw usage("snapshot delete needs exactly one snapshot key or --all", "choose one explicit deletion scope");
|
|
84
|
-
const scope =
|
|
85
|
-
options.resolveScope === undefined
|
|
86
|
-
? await resolveSnapshotScope({ root: options.root, selection, warnings: advisories, adapter: options.adapter })
|
|
87
|
-
: await options.resolveScope();
|
|
88
|
-
const deleted = deleteSnapshots(options.root, scope, {
|
|
89
|
-
...(key !== undefined ? { snapshotKey: key } : {}),
|
|
90
|
-
...(parsed.flags.has("all") ? { all: true } : {}),
|
|
91
|
-
});
|
|
92
|
-
result = { action: "deleted", deleted: deleted.deleted };
|
|
93
|
-
}
|
|
94
|
-
advisories.flush({ color: options.output.color, stderr: options.stderr });
|
|
95
|
-
emit(snapshotRenderable(result), options.output, options.stdout);
|
|
96
|
-
return 0;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function runtimeSelection(parsed: ReturnType<typeof parseCommandArgs>): SnapshotRuntimeSelection {
|
|
100
|
-
const workspace = singleOptionValue(parsed, "workspace")?.trim();
|
|
101
|
-
const workspaceId = singleOptionValue(parsed, "workspace-id")?.trim();
|
|
102
|
-
if (workspace === "" || workspaceId === "")
|
|
103
|
-
throw usage("workspace selectors need a value", "pass a non-empty manifest path or workspace id");
|
|
104
|
-
if (workspace !== undefined && workspaceId !== undefined)
|
|
105
|
-
throw usage("--workspace and --workspace-id are mutually exclusive", "select one workspace scope");
|
|
106
|
-
return { ...(workspace !== undefined ? { workspace } : {}), ...(workspaceId !== undefined ? { workspaceId } : {}) };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function snapshotRenderable(data: SnapshotCommandResult): Renderable<SnapshotCommandResult> {
|
|
110
|
-
return { kind: "snapshot.result", data, pretty: renderSnapshot, plain: renderSnapshot };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function renderSnapshot(data: SnapshotCommandResult): string {
|
|
114
|
-
if (data.action === "listed") {
|
|
115
|
-
return data.snapshots.length === 0
|
|
116
|
-
? `0 retained snapshots (maximum ${data.maximum})`
|
|
117
|
-
: [
|
|
118
|
-
`${data.retained} retained snapshots (maximum ${data.maximum})`,
|
|
119
|
-
...data.snapshots.map(
|
|
120
|
-
(snapshot) =>
|
|
121
|
-
`${snapshot.snapshotKey} ${snapshot.scopeKind} ${snapshot.repositories.map((repository) => repository.gitCommit ?? "uncommitted").join(",")}`,
|
|
122
|
-
),
|
|
123
|
-
].join("\n");
|
|
124
|
-
}
|
|
125
|
-
if (data.action === "deleted")
|
|
126
|
-
return data.deleted.length === 0
|
|
127
|
-
? "deleted 0 retained snapshots"
|
|
128
|
-
: data.deleted.map((key) => `deleted ${key}`).join("\n");
|
|
129
|
-
return `${data.action} ${data.snapshot.snapshotKey} (${data.retained}/${data.maximum})`;
|
|
130
|
-
}
|