@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
@@ -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
- }
@@ -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
- }