@contentful/experience-design-system-cli 2.12.0 → 2.12.1-dev-build-bae15c7.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.
Files changed (144) hide show
  1. package/README.md +214 -357
  2. package/dist/package.json +1 -1
  3. package/dist/src/analyze/extract/astro.js +9 -0
  4. package/dist/src/analyze/extract/react.js +23 -0
  5. package/dist/src/analyze/extract/stencil.js +3 -0
  6. package/dist/src/analyze/extract/vue.js +6 -0
  7. package/dist/src/analyze/extract/web-components.js +10 -0
  8. package/dist/src/analyze/pre-classify.d.ts +1 -1
  9. package/dist/src/analyze/pre-classify.js +3 -3
  10. package/dist/src/analyze/select/persistence.d.ts +13 -0
  11. package/dist/src/analyze/select/persistence.js +38 -0
  12. package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
  13. package/dist/src/analyze/select/preview-annotations.js +62 -0
  14. package/dist/src/analyze/select/tui/App.js +5 -2
  15. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
  16. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
  17. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
  18. package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
  19. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
  20. package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
  21. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
  22. package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
  23. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
  24. package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
  25. package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
  26. package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
  27. package/dist/src/analyze/select-agent/command.d.ts +2 -0
  28. package/dist/src/analyze/select-agent/command.js +260 -45
  29. package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
  30. package/dist/src/analyze/select-agent/show-rationale.js +92 -0
  31. package/dist/src/apply/command.js +7 -3
  32. package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
  33. package/dist/src/apply/tui/ServerApplyView.js +3 -2
  34. package/dist/src/credentials-store.d.ts +5 -0
  35. package/dist/src/credentials-store.js +7 -1
  36. package/dist/src/generate/agent-runner.d.ts +14 -0
  37. package/dist/src/generate/agent-runner.js +16 -0
  38. package/dist/src/generate/command.d.ts +6 -0
  39. package/dist/src/generate/command.js +45 -10
  40. package/dist/src/generate/progress.d.ts +9 -0
  41. package/dist/src/generate/progress.js +11 -0
  42. package/dist/src/generate/prompt-builder.d.ts +8 -0
  43. package/dist/src/generate/prompt-builder.js +22 -11
  44. package/dist/src/import/agent-model-resolve.d.ts +23 -0
  45. package/dist/src/import/agent-model-resolve.js +35 -0
  46. package/dist/src/import/auto-filter-resolve.d.ts +12 -0
  47. package/dist/src/import/auto-filter-resolve.js +16 -0
  48. package/dist/src/import/command.js +248 -8
  49. package/dist/src/import/orchestrator.d.ts +2 -0
  50. package/dist/src/import/orchestrator.js +11 -3
  51. package/dist/src/import/print-prompt.d.ts +35 -0
  52. package/dist/src/import/print-prompt.js +31 -0
  53. package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
  54. package/dist/src/import/tui/CustomPromptBanner.js +7 -0
  55. package/dist/src/import/tui/WizardApp.d.ts +112 -8
  56. package/dist/src/import/tui/WizardApp.js +820 -333
  57. package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
  58. package/dist/src/import/tui/auto-filter-error.js +29 -0
  59. package/dist/src/import/tui/final-review-host.d.ts +15 -0
  60. package/dist/src/import/tui/final-review-host.js +20 -0
  61. package/dist/src/import/tui/merge-ai-decisions.d.ts +5 -0
  62. package/dist/src/import/tui/merge-ai-decisions.js +22 -0
  63. package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
  64. package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
  65. package/dist/src/import/tui/push-progress.d.ts +23 -0
  66. package/dist/src/import/tui/push-progress.js +14 -0
  67. package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
  68. package/dist/src/import/tui/run-print-files-helpers.js +8 -0
  69. package/dist/src/import/tui/run-teaser.d.ts +5 -0
  70. package/dist/src/import/tui/run-teaser.js +9 -0
  71. package/dist/src/import/tui/runLivePreview.d.ts +35 -0
  72. package/dist/src/import/tui/runLivePreview.js +73 -0
  73. package/dist/src/import/tui/runScopeGate.d.ts +22 -0
  74. package/dist/src/import/tui/runScopeGate.js +28 -0
  75. package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
  76. package/dist/src/import/tui/scope-gate-host.js +20 -0
  77. package/dist/src/import/tui/spawn-generate.d.ts +33 -0
  78. package/dist/src/import/tui/spawn-generate.js +53 -0
  79. package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
  80. package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
  81. package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
  82. package/dist/src/import/tui/steps/DoneStep.js +10 -4
  83. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
  84. package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
  85. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
  86. package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
  87. package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
  88. package/dist/src/import/tui/steps/PushingStep.js +40 -0
  89. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
  90. package/dist/src/import/tui/steps/ScopeGateStep.js +250 -0
  91. package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
  92. package/dist/src/import/tui/useLivePreview.d.ts +35 -0
  93. package/dist/src/import/tui/useLivePreview.js +120 -0
  94. package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
  95. package/dist/src/import/tui/wizard-generate-progress.js +25 -0
  96. package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
  97. package/dist/src/import/tui/wizard-save-flow.js +20 -0
  98. package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
  99. package/dist/src/import/tui/wizard-state-transitions.js +79 -0
  100. package/dist/src/lib/contentful-urls.d.ts +34 -0
  101. package/dist/src/lib/contentful-urls.js +41 -0
  102. package/dist/src/program.js +2 -0
  103. package/dist/src/runs/export-helpers.d.ts +30 -0
  104. package/dist/src/runs/export-helpers.js +59 -0
  105. package/dist/src/runs/fingerprint.d.ts +47 -0
  106. package/dist/src/runs/fingerprint.js +75 -0
  107. package/dist/src/runs/ls-command.d.ts +12 -0
  108. package/dist/src/runs/ls-command.js +137 -0
  109. package/dist/src/runs/modify-launcher.d.ts +24 -0
  110. package/dist/src/runs/modify-launcher.js +36 -0
  111. package/dist/src/runs/path-prompt.d.ts +8 -0
  112. package/dist/src/runs/path-prompt.js +72 -0
  113. package/dist/src/runs/push-creds-prompt.d.ts +25 -0
  114. package/dist/src/runs/push-creds-prompt.js +39 -0
  115. package/dist/src/runs/push-helpers.d.ts +29 -0
  116. package/dist/src/runs/push-helpers.js +69 -0
  117. package/dist/src/runs/replay-helpers.d.ts +67 -0
  118. package/dist/src/runs/replay-helpers.js +148 -0
  119. package/dist/src/runs/resolve-run-target.d.ts +12 -0
  120. package/dist/src/runs/resolve-run-target.js +45 -0
  121. package/dist/src/runs/run-picker-mount.d.ts +46 -0
  122. package/dist/src/runs/run-picker-mount.js +72 -0
  123. package/dist/src/runs/run-picker.d.ts +17 -0
  124. package/dist/src/runs/run-picker.js +145 -0
  125. package/dist/src/runs/save-conflict.d.ts +8 -0
  126. package/dist/src/runs/save-conflict.js +56 -0
  127. package/dist/src/runs/save-path-resolver.d.ts +55 -0
  128. package/dist/src/runs/save-path-resolver.js +71 -0
  129. package/dist/src/runs/staleness.d.ts +38 -0
  130. package/dist/src/runs/staleness.js +140 -0
  131. package/dist/src/runs/store.d.ts +78 -0
  132. package/dist/src/runs/store.js +136 -0
  133. package/dist/src/session/cache-keys.d.ts +15 -0
  134. package/dist/src/session/cache-keys.js +38 -0
  135. package/dist/src/session/db.d.ts +89 -3
  136. package/dist/src/session/db.js +447 -104
  137. package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
  138. package/dist/src/setup/auto-filter-prompt.js +24 -0
  139. package/dist/src/setup/command.d.ts +7 -0
  140. package/dist/src/setup/command.js +58 -2
  141. package/dist/src/types.d.ts +6 -0
  142. package/package.json +2 -2
  143. package/skills/generate-components.md +54 -6
  144. package/skills/select-components.md +13 -4
@@ -0,0 +1,140 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { sha256Hex } from './fingerprint.js';
4
+ const UNKNOWN = {
5
+ stale: false,
6
+ staleComponents: [],
7
+ staleTokens: false,
8
+ savedComponentsEdited: false,
9
+ savedTokensEdited: false,
10
+ missingSourceFiles: [],
11
+ };
12
+ /**
13
+ * Re-stat the source files and re-hash the saved artifacts captured by a
14
+ * run record's fingerprints, and report what (if anything) drifted.
15
+ *
16
+ * Returns `stale: false` when the run is missing a source fingerprint
17
+ * entirely (v2 records pre-dating runs.json v3 — treat as UNKNOWN, not
18
+ * stale, so existing runs keep replaying).
19
+ */
20
+ export async function checkRunStaleness(run) {
21
+ if (!run.sourceFingerprint)
22
+ return { ...UNKNOWN };
23
+ const result = {
24
+ stale: false,
25
+ staleComponents: [],
26
+ staleTokens: false,
27
+ savedComponentsEdited: false,
28
+ savedTokensEdited: false,
29
+ missingSourceFiles: [],
30
+ };
31
+ // ── Per-file source check ────────────────────────────────────────────
32
+ for (const [absPath, entry] of Object.entries(run.sourceFingerprint.files)) {
33
+ try {
34
+ const st = await stat(absPath);
35
+ if (st.mtime.toISOString() !== entry.mtime) {
36
+ result.staleComponents.push(entry.componentName ?? absPath);
37
+ result.stale = true;
38
+ }
39
+ }
40
+ catch {
41
+ result.missingSourceFiles.push(absPath);
42
+ result.stale = true;
43
+ }
44
+ }
45
+ // ── Raw tokens (single file: re-stat AND re-hash) ────────────────────
46
+ if (run.sourceFingerprint.rawTokensPath) {
47
+ const absPath = run.sourceFingerprint.rawTokensPath;
48
+ try {
49
+ const [st, buf] = await Promise.all([stat(absPath), readFile(absPath)]);
50
+ const mtime = st.mtime.toISOString();
51
+ const hash = sha256Hex(buf);
52
+ const mtimeChanged = run.sourceFingerprint.rawTokensMtime !== null && mtime !== run.sourceFingerprint.rawTokensMtime;
53
+ const hashChanged = run.sourceFingerprint.rawTokensContentHash !== null && hash !== run.sourceFingerprint.rawTokensContentHash;
54
+ if (mtimeChanged || hashChanged) {
55
+ result.staleTokens = true;
56
+ result.stale = true;
57
+ }
58
+ }
59
+ catch {
60
+ result.staleTokens = true;
61
+ result.stale = true;
62
+ }
63
+ }
64
+ // ── Saved-file check (read & hash components.json / tokens.json) ─────
65
+ if (run.savedFingerprint) {
66
+ if (run.savedFingerprint.componentsJsonHash !== null) {
67
+ const compPath = join(run.savePath, 'components.json');
68
+ try {
69
+ const buf = await readFile(compPath);
70
+ if (sha256Hex(buf) !== run.savedFingerprint.componentsJsonHash) {
71
+ result.savedComponentsEdited = true;
72
+ result.stale = true;
73
+ }
74
+ }
75
+ catch {
76
+ result.savedComponentsEdited = true;
77
+ result.stale = true;
78
+ }
79
+ }
80
+ if (run.savedFingerprint.tokensJsonHash !== null) {
81
+ const tokensPath = run.tokensPath ?? join(run.savePath, 'tokens.json');
82
+ try {
83
+ const buf = await readFile(tokensPath);
84
+ if (sha256Hex(buf) !== run.savedFingerprint.tokensJsonHash) {
85
+ result.savedTokensEdited = true;
86
+ result.stale = true;
87
+ }
88
+ }
89
+ catch {
90
+ result.savedTokensEdited = true;
91
+ result.stale = true;
92
+ }
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+ /**
98
+ * Render a short human-readable summary of a Staleness result, e.g.
99
+ * `src: 3, tokens, saved`. Returns the empty string when not stale.
100
+ */
101
+ export function shortStalenessSummary(s) {
102
+ if (!s.stale)
103
+ return '';
104
+ const parts = [];
105
+ const srcCount = s.staleComponents.length + s.missingSourceFiles.length;
106
+ if (srcCount > 0)
107
+ parts.push(`src: ${srcCount}`);
108
+ if (s.staleTokens)
109
+ parts.push('tokens');
110
+ if (s.savedComponentsEdited || s.savedTokensEdited)
111
+ parts.push('saved');
112
+ return parts.join(', ');
113
+ }
114
+ /**
115
+ * Render a multi-line detail block for staleness output (used by `runs <id>`
116
+ * detail view and replay refusals). Caps long lists at 5 entries.
117
+ */
118
+ export function formatStalenessDetail(s) {
119
+ if (!s.stale)
120
+ return ['Status: FRESH'];
121
+ const lines = ['Status: STALE'];
122
+ const cap = 5;
123
+ if (s.staleComponents.length > 0) {
124
+ const head = s.staleComponents.slice(0, cap).join(', ');
125
+ const more = s.staleComponents.length > cap ? ` and ${s.staleComponents.length - cap} more` : '';
126
+ lines.push(` Source changed: ${head}${more}`);
127
+ }
128
+ if (s.missingSourceFiles.length > 0) {
129
+ const head = s.missingSourceFiles.slice(0, cap).join(', ');
130
+ const more = s.missingSourceFiles.length > cap ? ` and ${s.missingSourceFiles.length - cap} more` : '';
131
+ lines.push(` Source missing: ${head}${more}`);
132
+ }
133
+ if (s.staleTokens)
134
+ lines.push(' Raw tokens drifted');
135
+ if (s.savedComponentsEdited)
136
+ lines.push(' Saved components.json edited');
137
+ if (s.savedTokensEdited)
138
+ lines.push(' Saved tokens.json edited');
139
+ return lines;
140
+ }
@@ -0,0 +1,78 @@
1
+ export declare const RUNS_FILE_VERSION: 3;
2
+ export declare const RUNS_FILE_CAP = 200;
3
+ /** Versions this CLI can read. v1 / v2 files are auto-migrated in memory;
4
+ * new writes always use the latest version. */
5
+ export declare const READABLE_VERSIONS: Set<number>;
6
+ /** Per-file source fingerprint captured at save time. `files` is keyed by
7
+ * absolute (path.resolve'd) source path; each entry records the file's
8
+ * mtime (ISO 8601) at the moment the run record was written and, when
9
+ * known, the component name extracted from it (first-seen wins on
10
+ * collisions). */
11
+ export type SourceFingerprint = {
12
+ files: Record<string, {
13
+ mtime: string;
14
+ componentName?: string;
15
+ }>;
16
+ rawTokensPath: string | null;
17
+ rawTokensMtime: string | null;
18
+ rawTokensContentHash: string | null;
19
+ };
20
+ /** SHA-256 hashes of the JSON artifacts the wizard wrote to disk. Used on
21
+ * replay to detect manual edits to components.json / tokens.json. */
22
+ export type SavedFingerprint = {
23
+ componentsJsonHash: string | null;
24
+ tokensJsonHash: string | null;
25
+ };
26
+ export type RunRecord = {
27
+ id: string;
28
+ createdAt: string;
29
+ projectPath: string;
30
+ savePath: string;
31
+ componentCount: number;
32
+ tokenCount: number;
33
+ /** Filesystem path of the saved tokens.json. Null when no tokens were
34
+ * generated for the run. Added in runs.json v2. */
35
+ tokensPath: string | null;
36
+ /** Pipeline.db session id for the generated tokens. Null when no tokens
37
+ * step ran. Added in runs.json v2 so replay/push and modify can re-emit
38
+ * or re-push tokens. */
39
+ tokenSessionId: string | null;
40
+ agent: string;
41
+ pushedTo: {
42
+ spaceId: string;
43
+ environmentId: string;
44
+ host: string;
45
+ } | null;
46
+ extractSessionId: string;
47
+ generateSessionId: string | null;
48
+ /** Per-file source fingerprint. Null on v1/v2 records read from disk; v3
49
+ * writers always populate. Added in runs.json v3. Optional in the type
50
+ * so callers that don't compute it (e.g. older test fixtures) still
51
+ * satisfy `RunRecord`; `appendRun` always normalizes a missing value to
52
+ * null on the way to disk. */
53
+ sourceFingerprint?: SourceFingerprint | null;
54
+ /** Saved-artifact fingerprint. Null on v1/v2 records read from disk; v3
55
+ * writers always populate. Added in runs.json v3. Optional for the same
56
+ * reason as `sourceFingerprint`. */
57
+ savedFingerprint?: SavedFingerprint | null;
58
+ notes?: string;
59
+ };
60
+ export type RunsFile = {
61
+ version: typeof RUNS_FILE_VERSION;
62
+ runs: RunRecord[];
63
+ };
64
+ export declare function runsFilePath(): string;
65
+ export declare function generateUlid(now?: number): string;
66
+ export type AppendInput = Omit<RunRecord, 'id' | 'createdAt'> & Partial<Pick<RunRecord, 'id' | 'createdAt'>>;
67
+ export declare function appendRun(input: AppendInput): Promise<RunRecord>;
68
+ export type ListOptions = {
69
+ limit?: number;
70
+ projectPath?: string;
71
+ before?: string;
72
+ after?: string;
73
+ };
74
+ export declare function listRuns(opts?: ListOptions): Promise<RunRecord[]>;
75
+ export declare function getRun(id: string): Promise<RunRecord>;
76
+ export declare function updateRun(id: string, patch: Partial<Omit<RunRecord, 'id'>>): Promise<RunRecord>;
77
+ export declare function findRunBySavePath(savePath: string): Promise<RunRecord | null>;
78
+ export declare function findAllRunsBySavePath(savePath: string): Promise<RunRecord[]>;
@@ -0,0 +1,136 @@
1
+ import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { randomBytes } from 'node:crypto';
5
+ export const RUNS_FILE_VERSION = 3;
6
+ export const RUNS_FILE_CAP = 200;
7
+ /** Versions this CLI can read. v1 / v2 files are auto-migrated in memory;
8
+ * new writes always use the latest version. */
9
+ export const READABLE_VERSIONS = new Set([1, 2, 3]);
10
+ const RUNS_DIR = join(homedir(), '.config', 'experiences');
11
+ const RUNS_PATH = join(RUNS_DIR, 'runs.json');
12
+ export function runsFilePath() {
13
+ return RUNS_PATH;
14
+ }
15
+ // Crockford-base32 ULID (26 chars: 10 timestamp + 16 random).
16
+ const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
17
+ function encodeBase32(bytes, length) {
18
+ let out = '';
19
+ let bits = 0;
20
+ let value = 0;
21
+ for (const b of bytes) {
22
+ value = (value << 8) | b;
23
+ bits += 8;
24
+ while (bits >= 5) {
25
+ bits -= 5;
26
+ out += CROCKFORD[(value >> bits) & 0x1f];
27
+ }
28
+ }
29
+ if (bits > 0)
30
+ out += CROCKFORD[(value << (5 - bits)) & 0x1f];
31
+ return out.slice(0, length);
32
+ }
33
+ export function generateUlid(now = Date.now()) {
34
+ // 48-bit timestamp -> 10 chars
35
+ const tsBytes = new Uint8Array(6);
36
+ let n = now;
37
+ for (let i = 5; i >= 0; i--) {
38
+ tsBytes[i] = n & 0xff;
39
+ n = Math.floor(n / 256);
40
+ }
41
+ const ts = encodeBase32(tsBytes, 10);
42
+ const rand = encodeBase32(randomBytes(10), 16);
43
+ return (ts + rand).toUpperCase();
44
+ }
45
+ function migrateRecord(rec) {
46
+ return {
47
+ ...rec,
48
+ tokensPath: rec.tokensPath ?? null,
49
+ tokenSessionId: rec.tokenSessionId ?? null,
50
+ sourceFingerprint: rec.sourceFingerprint ?? null,
51
+ savedFingerprint: rec.savedFingerprint ?? null,
52
+ };
53
+ }
54
+ async function readFileMaybe() {
55
+ try {
56
+ const raw = await readFile(RUNS_PATH, 'utf8');
57
+ const parsed = JSON.parse(raw);
58
+ if (!READABLE_VERSIONS.has(parsed.version)) {
59
+ throw new Error(`runs.json version mismatch: file is v${parsed.version}, this CLI expects v${RUNS_FILE_VERSION} (also reads: ${[...READABLE_VERSIONS].join(', ')}). Back up and remove the file to start fresh.`);
60
+ }
61
+ // Always migrate to current schema in memory; new writes will persist v2.
62
+ const runs = parsed.runs.map(migrateRecord);
63
+ return { version: RUNS_FILE_VERSION, runs };
64
+ }
65
+ catch (err) {
66
+ const code = err.code;
67
+ if (code === 'ENOENT')
68
+ return null;
69
+ throw err;
70
+ }
71
+ }
72
+ async function writeAtomic(file) {
73
+ await mkdir(RUNS_DIR, { recursive: true });
74
+ const body = JSON.stringify(file, null, 2) + '\n';
75
+ const tmp = `${RUNS_PATH}.${process.pid}.${Date.now()}.tmp`;
76
+ await writeFile(tmp, body, { mode: 0o600 });
77
+ await rename(tmp, RUNS_PATH);
78
+ }
79
+ export async function appendRun(input) {
80
+ const existing = await readFileMaybe();
81
+ const record = {
82
+ ...input,
83
+ id: input.id ?? generateUlid(),
84
+ createdAt: input.createdAt ?? new Date().toISOString(),
85
+ sourceFingerprint: input.sourceFingerprint ?? null,
86
+ savedFingerprint: input.savedFingerprint ?? null,
87
+ };
88
+ const runs = existing ? [record, ...existing.runs] : [record];
89
+ if (runs.length > RUNS_FILE_CAP) {
90
+ const dropped = runs.length - RUNS_FILE_CAP;
91
+ runs.length = RUNS_FILE_CAP;
92
+ console.warn(`runs.json reached cap of ${RUNS_FILE_CAP}; dropped ${dropped} oldest entr${dropped === 1 ? 'y' : 'ies'}.`);
93
+ }
94
+ await writeAtomic({ version: RUNS_FILE_VERSION, runs });
95
+ return record;
96
+ }
97
+ export async function listRuns(opts = {}) {
98
+ const file = await readFileMaybe();
99
+ if (!file)
100
+ return [];
101
+ let runs = file.runs;
102
+ if (opts.projectPath)
103
+ runs = runs.filter((r) => r.projectPath === opts.projectPath);
104
+ if (opts.before)
105
+ runs = runs.filter((r) => r.createdAt < opts.before);
106
+ if (opts.after)
107
+ runs = runs.filter((r) => r.createdAt > opts.after);
108
+ if (typeof opts.limit === 'number')
109
+ runs = runs.slice(0, opts.limit);
110
+ return runs;
111
+ }
112
+ export async function getRun(id) {
113
+ const file = await readFileMaybe();
114
+ const found = file?.runs.find((r) => r.id === id);
115
+ if (!found)
116
+ throw new Error(`Run ${id} not found in ${RUNS_PATH}`);
117
+ return found;
118
+ }
119
+ export async function updateRun(id, patch) {
120
+ const file = (await readFileMaybe()) ?? { version: RUNS_FILE_VERSION, runs: [] };
121
+ const idx = file.runs.findIndex((r) => r.id === id);
122
+ if (idx < 0)
123
+ throw new Error(`Run ${id} not found in ${RUNS_PATH}`);
124
+ const updated = { ...file.runs[idx], ...patch, id };
125
+ file.runs[idx] = updated;
126
+ await writeAtomic(file);
127
+ return updated;
128
+ }
129
+ export async function findRunBySavePath(savePath) {
130
+ const file = await readFileMaybe();
131
+ return file?.runs.find((r) => r.savePath === savePath) ?? null;
132
+ }
133
+ export async function findAllRunsBySavePath(savePath) {
134
+ const file = await readFileMaybe();
135
+ return file?.runs.filter((r) => r.savePath === savePath) ?? [];
136
+ }
@@ -0,0 +1,15 @@
1
+ import { type Skill } from '../generate/prompt-builder.js';
2
+ /** sha256 hex of a string. Stable across runs. */
3
+ export declare function hashContent(content: string): string;
4
+ /** sha256 hex of a file's UTF-8 content. */
5
+ export declare function hashFile(filePath: string): Promise<string>;
6
+ /**
7
+ * Hash the prompt content for a skill — either the bundled skill file shipped
8
+ * with the CLI, or a custom override path. Used to key the select_cache and
9
+ * generation_cache so cache entries invalidate when the prompt itself changes
10
+ * (including bundled-skill updates across CLI versions).
11
+ *
12
+ * @param skill — which skill (matches resolveSkillPath in prompt-builder)
13
+ * @param skillPathOverride — optional absolute/relative path to a custom prompt
14
+ */
15
+ export declare function hashPromptForSkill(skill: Skill, skillPathOverride?: string): Promise<string>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Hash helpers for the fine-grained LLM cache.
3
+ *
4
+ * Three layers of cache (extract, select, generate) all need stable content
5
+ * hashes for cache-key derivation. Keep these primitives in one place so the
6
+ * algorithm and encoding never drift between callers.
7
+ *
8
+ * All hashes are sha256 hex (64 chars). No npm deps — uses node:crypto.
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ import { readFile } from 'node:fs/promises';
12
+ import { resolve } from 'node:path';
13
+ import { resolveSkillPath } from '../generate/prompt-builder.js';
14
+ /** sha256 hex of a string. Stable across runs. */
15
+ export function hashContent(content) {
16
+ return createHash('sha256').update(content).digest('hex');
17
+ }
18
+ /** sha256 hex of a file's UTF-8 content. */
19
+ export async function hashFile(filePath) {
20
+ const content = await readFile(filePath, 'utf8');
21
+ return hashContent(content);
22
+ }
23
+ /**
24
+ * Hash the prompt content for a skill — either the bundled skill file shipped
25
+ * with the CLI, or a custom override path. Used to key the select_cache and
26
+ * generation_cache so cache entries invalidate when the prompt itself changes
27
+ * (including bundled-skill updates across CLI versions).
28
+ *
29
+ * @param skill — which skill (matches resolveSkillPath in prompt-builder)
30
+ * @param skillPathOverride — optional absolute/relative path to a custom prompt
31
+ */
32
+ export async function hashPromptForSkill(skill, skillPathOverride) {
33
+ if (skillPathOverride) {
34
+ return hashFile(resolve(skillPathOverride));
35
+ }
36
+ const bundled = resolveSkillPath(skill);
37
+ return hashFile(bundled);
38
+ }
@@ -31,6 +31,48 @@ export interface ApplyToolCallsResult {
31
31
  slots: number;
32
32
  warnings: string[];
33
33
  }
34
+ /**
35
+ * Feature 1: load per-component metadata for surfacing in the review UI —
36
+ * source path, full source text, and per-prop rationale + source-location.
37
+ * Returns null when no row matches. Decoupled from loadCDFComponents so
38
+ * the CDF projection stays unaffected.
39
+ */
40
+ export interface ComponentReviewMetadata {
41
+ sourcePath: string | null;
42
+ componentSource: string | null;
43
+ props: Record<string, {
44
+ rationale: string | null;
45
+ sourceStartLine: number | null;
46
+ sourceEndLine: number | null;
47
+ }>;
48
+ }
49
+ export declare function loadComponentReviewMetadata(db: DatabaseSync, sessionId: string, componentName: string): ComponentReviewMetadata | null;
50
+ export interface ComponentRationale {
51
+ name: string;
52
+ description: string | null;
53
+ descriptionRationale: string | null;
54
+ propsRationale: string | null;
55
+ slotsRationale: string | null;
56
+ props: Array<{
57
+ name: string;
58
+ category: string | null;
59
+ description: string | null;
60
+ rationale: string | null;
61
+ }>;
62
+ slots: Array<{
63
+ name: string;
64
+ description: string | null;
65
+ rationale: string | null;
66
+ }>;
67
+ }
68
+ /**
69
+ * Component-rationale loader: data contract for the `I` ComponentRationalePanel.
70
+ * Returns the component's description + the three component-level rationale
71
+ * strings (why-description / why-props / why-slots), plus per-prop and per-slot
72
+ * rationale rows for the operator-facing panel. Returns null when no component
73
+ * matches.
74
+ */
75
+ export declare function loadComponentRationale(db: DatabaseSync, sessionId: string, componentName: string): ComponentRationale | null;
34
76
  export declare function applyToolCalls(db: DatabaseSync, sessionId: string, componentId: string, componentName: string, calls: ToolCall[], incomingWarnings: string[]): ApplyToolCallsResult;
35
77
  export interface MatchHints {
36
78
  command: CommandName;
@@ -42,7 +84,7 @@ export interface SessionResolution {
42
84
  isNew: boolean;
43
85
  isResumed: boolean;
44
86
  }
45
- export declare function getOrCreateSession(db: DatabaseSync, sessionFlag: string | undefined, sessionName: string | undefined, hints: MatchHints): SessionResolution;
87
+ export declare function getOrCreateSession(db: DatabaseSync, sessionFlag: string | undefined, sessionName: string | undefined, _hints: MatchHints): SessionResolution;
46
88
  export declare function createStep(db: DatabaseSync, sessionId: string, command: CommandName, inputs: Record<string, string>): number;
47
89
  export declare function updateStep(db: DatabaseSync, stepId: number, status: 'complete' | 'failed', outputs: Record<string, string>, error?: string): void;
48
90
  export declare function storeRawComponents(db: DatabaseSync, sessionId: string, components: RawComponentDefinition[], options?: {
@@ -74,6 +116,17 @@ export declare function loadCDFComponents(db: DatabaseSync, sessionId: string):
74
116
  key: string;
75
117
  entry: CDFComponentEntry;
76
118
  }>;
119
+ export type ScopeComponentRow = {
120
+ name: string;
121
+ componentId: string;
122
+ aiDecision: 'accepted' | 'rejected' | null;
123
+ aiReason: string | null;
124
+ };
125
+ export declare function loadScopeComponents(db: DatabaseSync, sessionId: string): ScopeComponentRow[];
126
+ export declare function applyScopeDecisions(db: DatabaseSync, sessionId: string, decisions: {
127
+ accepted: string[];
128
+ rejected: string[];
129
+ }): void;
77
130
  export declare function storeDTCGTokens(db: DatabaseSync, sessionId: string, groups: DTCGTokenGroup[], tokens: DTCGTokenEntry[]): void;
78
131
  export interface ApplyTokenToolCallsResult {
79
132
  tokens: number;
@@ -111,16 +164,49 @@ export interface CacheEntry {
111
164
  entityId: string;
112
165
  sourceSessionId: string;
113
166
  humanEdited: boolean;
167
+ /** sha256 of the prompt content. '' for pre-upgrade rows that predate the fine-grained cache. */
168
+ promptHash: string;
114
169
  createdAt: string;
115
170
  updatedAt: string;
116
171
  }
117
172
  export declare function computeComponentInputHash(component: RawComponentWithId): string;
118
173
  export declare function computeTokenInputHash(rawTokenContent: string): string;
119
- export declare function lookupCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string): CacheEntry | null;
174
+ export declare function lookupCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string, promptHash?: string): CacheEntry | null;
120
175
  export declare function lookupCacheByEntity(db: DatabaseSync, entityType: 'component' | 'token_set', entityId: string): CacheEntry | null;
121
- export declare function storeCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string, sourceSessionId: string, humanEdited: boolean): void;
176
+ export declare function storeCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string, sourceSessionId: string, humanEdited: boolean, promptHash?: string): void;
122
177
  export declare function storeScannedFiles(db: DatabaseSync, sessionId: string, filePaths: string[]): void;
123
178
  export declare function loadScannedFiles(db: DatabaseSync, sessionId: string): string[];
124
179
  export declare function markCacheHumanEdited(db: DatabaseSync, entityType: 'component' | 'token_set', entityId: string): void;
125
180
  export declare function copyComponentFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string, componentId: string): void;
126
181
  export declare function copyTokensFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string): void;
182
+ /**
183
+ * Returns the CLI cache version used to namespace cache rows. To avoid busting
184
+ * the cache on every npm patch bump, this hashes the content of the bundled
185
+ * skill prompts. When the prompts change, cache invalidates; when only library
186
+ * code or test fixtures change, the cache stays warm.
187
+ *
188
+ * Memoized per-process. Falls back to package version on any I/O error.
189
+ */
190
+ export declare function getCliCacheVersion(): Promise<string>;
191
+ export interface ExtractCacheEntry {
192
+ filePath: string;
193
+ fileHash: string;
194
+ cliVersion: string;
195
+ createdAt: string;
196
+ updatedAt: string;
197
+ components: RawComponentDefinition[];
198
+ }
199
+ export declare function storeExtractCache(db: DatabaseSync, filePath: string, fileHash: string, cliVersion: string, components: RawComponentDefinition[]): void;
200
+ export declare function lookupExtractCache(db: DatabaseSync, fileHash: string, cliVersion: string): ExtractCacheEntry | null;
201
+ export type SelectDecision = 'accepted' | 'rejected';
202
+ export interface SelectCacheEntry {
203
+ componentHash: string;
204
+ promptHash: string;
205
+ cliVersion: string;
206
+ decision: SelectDecision;
207
+ reason: string | null;
208
+ createdAt: string;
209
+ updatedAt: string;
210
+ }
211
+ export declare function storeSelectCache(db: DatabaseSync, componentHash: string, promptHash: string, cliVersion: string, decision: SelectDecision, reason: string | null): void;
212
+ export declare function lookupSelectCache(db: DatabaseSync, componentHash: string, promptHash: string, cliVersion: string): SelectCacheEntry | null;