@contentful/experience-design-system-cli 2.2.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 (165) hide show
  1. package/README.md +532 -0
  2. package/bin/cli.js +58 -0
  3. package/dist/package.json +56 -0
  4. package/dist/src/analyze/command.d.ts +3 -0
  5. package/dist/src/analyze/command.js +175 -0
  6. package/dist/src/analyze/extract/astro.d.ts +5 -0
  7. package/dist/src/analyze/extract/astro.js +280 -0
  8. package/dist/src/analyze/extract/pipeline.d.ts +6 -0
  9. package/dist/src/analyze/extract/pipeline.js +298 -0
  10. package/dist/src/analyze/extract/react.d.ts +2 -0
  11. package/dist/src/analyze/extract/react.js +1949 -0
  12. package/dist/src/analyze/extract/slot-detection.d.ts +35 -0
  13. package/dist/src/analyze/extract/slot-detection.js +101 -0
  14. package/dist/src/analyze/extract/stencil.d.ts +2 -0
  15. package/dist/src/analyze/extract/stencil.js +293 -0
  16. package/dist/src/analyze/extract/tsx-shared.d.ts +8 -0
  17. package/dist/src/analyze/extract/tsx-shared.js +263 -0
  18. package/dist/src/analyze/extract/vue-tsx.d.ts +2 -0
  19. package/dist/src/analyze/extract/vue-tsx.js +498 -0
  20. package/dist/src/analyze/extract/vue.d.ts +5 -0
  21. package/dist/src/analyze/extract/vue.js +647 -0
  22. package/dist/src/analyze/extract/web-components.d.ts +2 -0
  23. package/dist/src/analyze/extract/web-components.js +866 -0
  24. package/dist/src/analyze/pre-classify.d.ts +17 -0
  25. package/dist/src/analyze/pre-classify.js +144 -0
  26. package/dist/src/analyze/select/command.d.ts +2 -0
  27. package/dist/src/analyze/select/command.js +256 -0
  28. package/dist/src/analyze/select/index.d.ts +6 -0
  29. package/dist/src/analyze/select/index.js +5 -0
  30. package/dist/src/analyze/select/parser.d.ts +6 -0
  31. package/dist/src/analyze/select/parser.js +53 -0
  32. package/dist/src/analyze/select/persistence.d.ts +9 -0
  33. package/dist/src/analyze/select/persistence.js +42 -0
  34. package/dist/src/analyze/select/stdout.d.ts +7 -0
  35. package/dist/src/analyze/select/stdout.js +3 -0
  36. package/dist/src/analyze/select/tui/App.d.ts +8 -0
  37. package/dist/src/analyze/select/tui/App.js +491 -0
  38. package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +20 -0
  39. package/dist/src/analyze/select/tui/components/ComponentDetail.js +43 -0
  40. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +11 -0
  41. package/dist/src/analyze/select/tui/components/FieldEditor.js +531 -0
  42. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +10 -0
  43. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +15 -0
  44. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +7 -0
  45. package/dist/src/analyze/select/tui/components/HelpOverlay.js +11 -0
  46. package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +11 -0
  47. package/dist/src/analyze/select/tui/components/JsonEditor.js +154 -0
  48. package/dist/src/analyze/select/tui/components/JsonPanel.d.ts +11 -0
  49. package/dist/src/analyze/select/tui/components/JsonPanel.js +62 -0
  50. package/dist/src/analyze/select/tui/components/PreviewSummaryBar.d.ts +8 -0
  51. package/dist/src/analyze/select/tui/components/PreviewSummaryBar.js +29 -0
  52. package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +8 -0
  53. package/dist/src/analyze/select/tui/components/QuitDialog.js +14 -0
  54. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +15 -0
  55. package/dist/src/analyze/select/tui/components/Sidebar.js +48 -0
  56. package/dist/src/analyze/select/tui/components/SourcePanel.d.ts +11 -0
  57. package/dist/src/analyze/select/tui/components/SourcePanel.js +52 -0
  58. package/dist/src/analyze/select/tui/components/StatusBar.d.ts +11 -0
  59. package/dist/src/analyze/select/tui/components/StatusBar.js +6 -0
  60. package/dist/src/analyze/select/tui/components/TopBar.d.ts +10 -0
  61. package/dist/src/analyze/select/tui/components/TopBar.js +5 -0
  62. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +24 -0
  63. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +68 -0
  64. package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +24 -0
  65. package/dist/src/analyze/select/tui/hooks/useKeymap.js +67 -0
  66. package/dist/src/analyze/select/tui/hooks/useSession.d.ts +19 -0
  67. package/dist/src/analyze/select/tui/hooks/useSession.js +52 -0
  68. package/dist/src/analyze/select/tui/hooks/useUndo.d.ts +8 -0
  69. package/dist/src/analyze/select/tui/hooks/useUndo.js +26 -0
  70. package/dist/src/analyze/select/types.d.ts +46 -0
  71. package/dist/src/analyze/select/types.js +20 -0
  72. package/dist/src/analyze/select-agent/command.d.ts +2 -0
  73. package/dist/src/analyze/select-agent/command.js +208 -0
  74. package/dist/src/analyze/tui/AnalyzeView.d.ts +24 -0
  75. package/dist/src/analyze/tui/AnalyzeView.js +38 -0
  76. package/dist/src/apply/api-client.d.ts +35 -0
  77. package/dist/src/apply/api-client.js +143 -0
  78. package/dist/src/apply/command.d.ts +6 -0
  79. package/dist/src/apply/command.js +787 -0
  80. package/dist/src/apply/manifest.d.ts +1 -0
  81. package/dist/src/apply/manifest.js +1 -0
  82. package/dist/src/apply/tui/SelectView.d.ts +18 -0
  83. package/dist/src/apply/tui/SelectView.js +34 -0
  84. package/dist/src/apply/tui/ServerApplyView.d.ts +32 -0
  85. package/dist/src/apply/tui/ServerApplyView.js +42 -0
  86. package/dist/src/apply/tui/ServerPreviewView.d.ts +9 -0
  87. package/dist/src/apply/tui/ServerPreviewView.js +21 -0
  88. package/dist/src/credentials-store.d.ts +8 -0
  89. package/dist/src/credentials-store.js +30 -0
  90. package/dist/src/generate/agent-runner.d.ts +86 -0
  91. package/dist/src/generate/agent-runner.js +314 -0
  92. package/dist/src/generate/command.d.ts +2 -0
  93. package/dist/src/generate/command.js +545 -0
  94. package/dist/src/generate/edit/command.d.ts +2 -0
  95. package/dist/src/generate/edit/command.js +126 -0
  96. package/dist/src/generate/prompt-builder.d.ts +18 -0
  97. package/dist/src/generate/prompt-builder.js +202 -0
  98. package/dist/src/generate/tui/GenerateView.d.ts +12 -0
  99. package/dist/src/generate/tui/GenerateView.js +10 -0
  100. package/dist/src/import/command.d.ts +2 -0
  101. package/dist/src/import/command.js +96 -0
  102. package/dist/src/import/orchestrator.d.ts +37 -0
  103. package/dist/src/import/orchestrator.js +374 -0
  104. package/dist/src/import/path-utils.d.ts +15 -0
  105. package/dist/src/import/path-utils.js +30 -0
  106. package/dist/src/import/tui/WizardApp.d.ts +10 -0
  107. package/dist/src/import/tui/WizardApp.js +906 -0
  108. package/dist/src/import/tui/steps/CredentialsStep.d.ts +15 -0
  109. package/dist/src/import/tui/steps/CredentialsStep.js +79 -0
  110. package/dist/src/import/tui/steps/DoneStep.d.ts +20 -0
  111. package/dist/src/import/tui/steps/DoneStep.js +17 -0
  112. package/dist/src/import/tui/steps/ErrorStep.d.ts +8 -0
  113. package/dist/src/import/tui/steps/ErrorStep.js +11 -0
  114. package/dist/src/import/tui/steps/GateStep.d.ts +14 -0
  115. package/dist/src/import/tui/steps/GateStep.js +20 -0
  116. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +8 -0
  117. package/dist/src/import/tui/steps/GenerateReviewStep.js +208 -0
  118. package/dist/src/import/tui/steps/PathValidationStep.d.ts +10 -0
  119. package/dist/src/import/tui/steps/PathValidationStep.js +151 -0
  120. package/dist/src/import/tui/steps/PreviewStep.d.ts +21 -0
  121. package/dist/src/import/tui/steps/PreviewStep.js +36 -0
  122. package/dist/src/import/tui/steps/RunningStep.d.ts +10 -0
  123. package/dist/src/import/tui/steps/RunningStep.js +20 -0
  124. package/dist/src/import/tui/steps/TokenInputStep.d.ts +8 -0
  125. package/dist/src/import/tui/steps/TokenInputStep.js +70 -0
  126. package/dist/src/import/tui/steps/WelcomeStep.d.ts +7 -0
  127. package/dist/src/import/tui/steps/WelcomeStep.js +33 -0
  128. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +15 -0
  129. package/dist/src/import/tui/steps/WizardPreviewStep.js +121 -0
  130. package/dist/src/import/tui/steps/preview-diff.d.ts +10 -0
  131. package/dist/src/import/tui/steps/preview-diff.js +132 -0
  132. package/dist/src/index.d.ts +1 -0
  133. package/dist/src/index.js +2 -0
  134. package/dist/src/output/format.d.ts +23 -0
  135. package/dist/src/output/format.js +110 -0
  136. package/dist/src/print/command.d.ts +2 -0
  137. package/dist/src/print/command.js +199 -0
  138. package/dist/src/print/validate/tui/ValidateView.d.ts +15 -0
  139. package/dist/src/print/validate/tui/ValidateView.js +37 -0
  140. package/dist/src/print/validate/validators/cdf-validator.d.ts +2 -0
  141. package/dist/src/print/validate/validators/cdf-validator.js +104 -0
  142. package/dist/src/print/validate/validators/dtcg-validator.d.ts +2 -0
  143. package/dist/src/print/validate/validators/dtcg-validator.js +110 -0
  144. package/dist/src/print/validate/validators/format-errors.d.ts +12 -0
  145. package/dist/src/print/validate/validators/format-errors.js +18 -0
  146. package/dist/src/program.d.ts +2 -0
  147. package/dist/src/program.js +25 -0
  148. package/dist/src/session/command.d.ts +2 -0
  149. package/dist/src/session/command.js +261 -0
  150. package/dist/src/session/db.d.ts +111 -0
  151. package/dist/src/session/db.js +1114 -0
  152. package/dist/src/session/migration.d.ts +4 -0
  153. package/dist/src/session/migration.js +117 -0
  154. package/dist/src/session/session-id.d.ts +1 -0
  155. package/dist/src/session/session-id.js +212 -0
  156. package/dist/src/session/stats.d.ts +27 -0
  157. package/dist/src/session/stats.js +89 -0
  158. package/dist/src/setup/command.d.ts +2 -0
  159. package/dist/src/setup/command.js +765 -0
  160. package/dist/src/types.d.ts +48 -0
  161. package/dist/src/types.js +1 -0
  162. package/package.json +55 -0
  163. package/skills/generate-components.md +361 -0
  164. package/skills/generate-tokens.md +194 -0
  165. package/skills/select-components.md +180 -0
@@ -0,0 +1,545 @@
1
+ import { createElement } from 'react';
2
+ import { render } from 'ink';
3
+ import { access, readFile, readdir, stat } from 'node:fs/promises';
4
+ import { join, resolve } from 'node:path';
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { parseToolCallLines, parseTokenToolCallLines, resolveBinary, runAgent, } from './agent-runner.js';
8
+ import { OutputFormatter, c } from '../output/format.js';
9
+ import { buildPrompt, resolveSkillPath } from './prompt-builder.js';
10
+ import { GenerateView } from './tui/GenerateView.js';
11
+ import { registerGenerateEditCommand } from './edit/command.js';
12
+ import { openPipelineDb, loadRawComponents, applyToolCalls, applyTokenToolCalls, computeComponentInputHash, computeTokenInputHash, lookupCache, lookupCacheByEntity, storeCache, copyComponentFromCache, copyTokensFromCache, } from '../session/db.js';
13
+ import { getRefineArtifactsRoot, getRefineSessionPaths } from '../analyze/select/persistence.js';
14
+ const execFileAsync = promisify(execFile);
15
+ const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
16
+ const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
17
+ const DEFAULT_COMPONENT_CONCURRENCY = 10;
18
+ const RETRY_BACKOFF_MS = Number(process.env.EDS_RETRY_BACKOFF_MS ?? 5_000);
19
+ function die(message) {
20
+ process.stderr.write(`${message}\n`);
21
+ process.exit(1);
22
+ }
23
+ async function pathExists(p) {
24
+ return access(p)
25
+ .then(() => true)
26
+ .catch(() => false);
27
+ }
28
+ async function assertFileExists(flag, p) {
29
+ if (!(await pathExists(p)))
30
+ die(`Error: file not found: ${p} (from ${flag})`);
31
+ }
32
+ async function readFileInline(path) {
33
+ if (!path)
34
+ return undefined;
35
+ const resolved = resolve(path);
36
+ let s;
37
+ try {
38
+ s = await stat(resolved);
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ if (!s.isDirectory())
44
+ return readFile(resolved, 'utf8');
45
+ // Directory: collect and concatenate all JSON files
46
+ const files = [];
47
+ async function walk(dir) {
48
+ let entries;
49
+ try {
50
+ entries = await readdir(dir);
51
+ }
52
+ catch {
53
+ return;
54
+ }
55
+ for (const entry of entries.sort()) {
56
+ const full = join(dir, entry);
57
+ let es;
58
+ try {
59
+ es = await stat(full);
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ if (es.isDirectory()) {
65
+ await walk(full);
66
+ }
67
+ else if (entry.endsWith('.json')) {
68
+ files.push(full);
69
+ }
70
+ }
71
+ }
72
+ await walk(resolved);
73
+ if (files.length === 0)
74
+ return undefined;
75
+ const parts = await Promise.all(files.map((f) => readFile(f, 'utf8').catch(() => '')));
76
+ return parts.filter(Boolean).join('\n\n');
77
+ }
78
+ async function assertBinaryInPath(binary) {
79
+ try {
80
+ await execFileAsync('which', [binary]);
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ function printFallbackInstructions(options) {
88
+ const binary = resolveBinary(options.agent);
89
+ const skillPath = resolveSkillPath(options.skill);
90
+ const lines = [
91
+ `Error: agent '${options.agent}' not found in $PATH (looked for binary: ${binary}).`,
92
+ `Install it or use one of: claude, codex, opencode, cursor`,
93
+ ``,
94
+ `To run the generation step manually:`,
95
+ ``,
96
+ ` 1. Open your coding agent`,
97
+ ` 2. Run this skill (all input data will be embedded inline):`,
98
+ ` ${skillPath}`,
99
+ ``,
100
+ ` Use --dry-run to print the full prompt including all inline data.`,
101
+ ];
102
+ lines.push(``, ` When done, the agent output must be stored in the session database.`);
103
+ lines.push(` Re-run the generate command with the agent available, or use --dry-run to inspect the prompt.`);
104
+ process.stderr.write(lines.join('\n') + '\n');
105
+ }
106
+ async function runOneComponent(agent, model, db, sessionId, component, tokensInline, tokenMapInline, index, total, verbose, noCache) {
107
+ const pos = c.dim(`[${index + 1}/${total}]`);
108
+ if (!noCache) {
109
+ const inputHash = computeComponentInputHash(component);
110
+ const cached = lookupCache(db, inputHash, 'component', component.component_id);
111
+ if (cached) {
112
+ copyComponentFromCache(db, cached.sourceSessionId, sessionId, component.component_id);
113
+ process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.green('cached')}\n`);
114
+ return {
115
+ componentName: component.name,
116
+ classified: 0,
117
+ excluded: 0,
118
+ slots: 0,
119
+ warnings: [],
120
+ failed: false,
121
+ cached: true,
122
+ };
123
+ }
124
+ // Check for pinned (human-edited) entry with a different hash
125
+ const pinned = lookupCacheByEntity(db, 'component', component.component_id);
126
+ if (pinned?.humanEdited) {
127
+ copyComponentFromCache(db, pinned.sourceSessionId, sessionId, component.component_id);
128
+ process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.cyan('pinned (human-edited)')}\n`);
129
+ return {
130
+ componentName: component.name,
131
+ classified: 0,
132
+ excluded: 0,
133
+ slots: 0,
134
+ warnings: [`${component.name}: source changed but human edits preserved`],
135
+ failed: false,
136
+ cached: true,
137
+ };
138
+ }
139
+ }
140
+ const rawComponentsInline = JSON.stringify([
141
+ {
142
+ name: component.name,
143
+ source: component.source,
144
+ framework: component.framework,
145
+ props: component.props,
146
+ slots: component.slots,
147
+ },
148
+ ], null, 2);
149
+ const prompt = await buildPrompt({
150
+ skill: 'components',
151
+ mode: 'autonomous',
152
+ rawComponentsInline,
153
+ tokensInline,
154
+ tokenMapInline,
155
+ outDir: process.cwd(),
156
+ componentName: component.name,
157
+ });
158
+ const maxAttempts = 2;
159
+ let lastError = '';
160
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
161
+ if (attempt > 1)
162
+ await new Promise((res) => setTimeout(res, RETRY_BACKOFF_MS));
163
+ let outputBuf = '';
164
+ const formatter = new OutputFormatter(verbose, (s) => {
165
+ outputBuf += s;
166
+ });
167
+ const result = await runAgent({
168
+ agent,
169
+ model,
170
+ prompt,
171
+ interactive: false,
172
+ timeoutMs: DEFAULT_TIMEOUT_MS,
173
+ onOutput: (chunk) => formatter.push(chunk),
174
+ });
175
+ formatter.flush();
176
+ // Write header + all tool-call output as one block so concurrent workers don't interleave.
177
+ const retryNote = attempt > 1 ? ` ${c.yellow(`retrying (${attempt}/${maxAttempts})`)}` : '';
178
+ process.stderr.write(` ${pos} ${c.bold(component.name)}${retryNote}\n${outputBuf}`);
179
+ if (result.timedOut) {
180
+ // Don't retry timeouts — same timeout will hit again
181
+ return {
182
+ componentName: component.name,
183
+ classified: 0,
184
+ excluded: 0,
185
+ slots: 0,
186
+ warnings: [],
187
+ failed: true,
188
+ error: `timed out after ${DEFAULT_TIMEOUT_MS / 60000} minutes`,
189
+ };
190
+ }
191
+ if (result.exitCode !== 0) {
192
+ lastError = `agent exited with code ${result.exitCode}`;
193
+ continue;
194
+ }
195
+ const { calls, warnings } = parseToolCallLines(result.stdout);
196
+ if (calls.length === 0) {
197
+ lastError = 'agent produced no tool calls';
198
+ continue;
199
+ }
200
+ const applied = applyToolCalls(db, sessionId, component.component_id, component.name, calls, warnings);
201
+ if (!noCache) {
202
+ const inputHash = computeComponentInputHash(component);
203
+ storeCache(db, inputHash, 'component', component.component_id, sessionId, false);
204
+ }
205
+ return {
206
+ componentName: component.name,
207
+ classified: applied.classified,
208
+ excluded: applied.excluded,
209
+ slots: applied.slots,
210
+ warnings: applied.warnings,
211
+ failed: false,
212
+ };
213
+ }
214
+ return {
215
+ componentName: component.name,
216
+ classified: 0,
217
+ excluded: 0,
218
+ slots: 0,
219
+ warnings: [],
220
+ failed: true,
221
+ error: lastError,
222
+ };
223
+ }
224
+ async function runAllComponents(agent, model, db, sessionId, components, tokensInline, tokenMapInline, verbose, noCache) {
225
+ const concurrency = Number(process.env.EDS_GENERATE_CONCURRENCY ?? DEFAULT_COMPONENT_CONCURRENCY);
226
+ process.stderr.write(`Categorizing ${c.bold(String(components.length))} component${components.length === 1 ? '' : 's'}` +
227
+ c.dim(` (concurrency: ${concurrency})`) +
228
+ '\n');
229
+ const results = new Array(components.length);
230
+ let next = 0;
231
+ async function worker() {
232
+ while (next < components.length) {
233
+ const i = next++;
234
+ results[i] = await runOneComponent(agent, model, db, sessionId, components[i], tokensInline, tokenMapInline, i, components.length, verbose, noCache);
235
+ }
236
+ }
237
+ await Promise.all(Array.from({ length: Math.min(concurrency, components.length) }, worker));
238
+ return results;
239
+ }
240
+ function resolveSessionId(sessionFlag) {
241
+ if (sessionFlag)
242
+ return sessionFlag;
243
+ const db = openPipelineDb();
244
+ try {
245
+ const row = db
246
+ .prepare(`SELECT s.id FROM sessions s
247
+ JOIN steps st ON st.session_id = s.id
248
+ WHERE st.command = 'analyze extract'
249
+ AND st.status = 'complete'
250
+ ORDER BY st.started_at DESC
251
+ LIMIT 1`)
252
+ .get();
253
+ if (!row) {
254
+ process.stderr.write('Error: no completed analyze extract session found. Run analyze extract first, or pass --session <id>.\n');
255
+ process.exit(1);
256
+ }
257
+ return row.id;
258
+ }
259
+ finally {
260
+ db.close();
261
+ }
262
+ }
263
+ async function loadAcceptedNames(sessionId) {
264
+ try {
265
+ const artifactsRoot = getRefineArtifactsRoot();
266
+ const paths = await getRefineSessionPaths(sessionId, artifactsRoot);
267
+ const raw = await readFile(paths.statePath, 'utf8');
268
+ const snapshot = JSON.parse(raw);
269
+ const accepted = snapshot.components.filter((c) => c.status === 'accepted').map((c) => c.name);
270
+ if (accepted.length === 0)
271
+ return null;
272
+ return new Set(accepted);
273
+ }
274
+ catch {
275
+ return null;
276
+ }
277
+ }
278
+ async function runGenerateSkill(skill, opts, verbose = false) {
279
+ if (!VALID_AGENTS.has(opts.agent)) {
280
+ die(`Error: unknown agent '${opts.agent}'. Accepted values: claude, codex, opencode, cursor`);
281
+ }
282
+ const agent = opts.agent;
283
+ if (skill === 'tokens' && !opts.rawTokens) {
284
+ die('Error: --raw-tokens is required when using generate tokens');
285
+ }
286
+ if (opts.rawTokens)
287
+ await assertFileExists('--raw-tokens', opts.rawTokens);
288
+ if (opts.tokens)
289
+ await assertFileExists('--tokens', opts.tokens);
290
+ if (opts.tokenMap)
291
+ await assertFileExists('--token-map', opts.tokenMap);
292
+ // Read all token files inline so the agent never needs to read files itself
293
+ const [rawTokensInline, tokensInline, tokenMapInline] = await Promise.all([
294
+ readFileInline(opts.rawTokens),
295
+ readFileInline(opts.tokens),
296
+ readFileInline(opts.tokenMap),
297
+ ]);
298
+ // Load raw components from DB for the components skill
299
+ let sessionId;
300
+ let allComponents;
301
+ if (skill === 'components') {
302
+ sessionId = resolveSessionId(opts.session);
303
+ const acceptedNames = await loadAcceptedNames(sessionId);
304
+ const db = openPipelineDb();
305
+ try {
306
+ allComponents = loadRawComponents(db, sessionId, acceptedNames ?? undefined);
307
+ }
308
+ finally {
309
+ db.close();
310
+ }
311
+ if (allComponents.length === 0) {
312
+ die(`Error: session '${sessionId}' has no raw components. Run analyze extract first.`);
313
+ }
314
+ if (acceptedNames) {
315
+ process.stderr.write(`Scope: ${allComponents.length} accepted component(s) from analyze select\n`);
316
+ }
317
+ // Warn about duplicate component names — later occurrences will overwrite earlier ones
318
+ const nameCounts = new Map();
319
+ for (const c of allComponents) {
320
+ const sources = nameCounts.get(c.name) ?? [];
321
+ sources.push(c.source);
322
+ nameCounts.set(c.name, sources);
323
+ }
324
+ const dupes = [...nameCounts.entries()].filter(([, srcs]) => srcs.length > 1);
325
+ if (dupes.length > 0) {
326
+ process.stderr.write(`Warning: ${dupes.length} duplicate component name(s) detected — only the last occurrence will be generated:\n`);
327
+ for (const [name, sources] of dupes) {
328
+ process.stderr.write(` ${name}:\n`);
329
+ for (const src of sources)
330
+ process.stderr.write(` ${src}\n`);
331
+ }
332
+ }
333
+ }
334
+ if (opts.dryRun) {
335
+ const sampleComponent = allComponents?.[0];
336
+ const sampleInline = sampleComponent
337
+ ? JSON.stringify([
338
+ {
339
+ name: sampleComponent.name,
340
+ source: sampleComponent.source,
341
+ framework: sampleComponent.framework,
342
+ props: sampleComponent.props,
343
+ slots: sampleComponent.slots,
344
+ },
345
+ ], null, 2)
346
+ : undefined;
347
+ const prompt = await buildPrompt({
348
+ skill,
349
+ mode: 'autonomous',
350
+ rawComponentsInline: sampleInline ?? rawTokensInline,
351
+ rawTokensInline: skill === 'tokens' ? rawTokensInline : undefined,
352
+ rawTokensFilename: opts.rawTokens ? resolve(opts.rawTokens).split('/').pop() : undefined,
353
+ tokensInline,
354
+ tokenMapInline,
355
+ outDir: process.cwd(),
356
+ });
357
+ process.stdout.write(prompt + '\n');
358
+ process.exit(0);
359
+ }
360
+ const binary = resolveBinary(agent);
361
+ if (!(await assertBinaryInPath(binary))) {
362
+ printFallbackInstructions({
363
+ agent,
364
+ skill,
365
+ sessionId: sessionId ?? '',
366
+ });
367
+ process.exit(1);
368
+ }
369
+ if (skill === 'components' && allComponents && sessionId) {
370
+ const db = openPipelineDb();
371
+ let componentResults;
372
+ try {
373
+ componentResults = await runAllComponents(agent, opts.model, db, sessionId, allComponents, tokensInline, tokenMapInline, verbose, opts.cache === false || process.env.EDS_NO_CACHE === '1');
374
+ }
375
+ finally {
376
+ db.close();
377
+ }
378
+ const failed = componentResults.filter((r) => r.failed);
379
+ const cachedResults = componentResults.filter((r) => r.cached);
380
+ const generated = componentResults.filter((r) => !r.failed && !r.cached);
381
+ const allWarnings = componentResults.flatMap((r) => r.warnings.map((w) => ` ${r.componentName}: ${w}`));
382
+ if (allWarnings.length > 0) {
383
+ process.stderr.write(c.yellow('Warnings:') + '\n' + allWarnings.join('\n') + '\n');
384
+ }
385
+ if (failed.length > 0) {
386
+ process.stderr.write(c.red(`Failed (${failed.length}/${componentResults.length}):`) + '\n');
387
+ for (const f of failed) {
388
+ process.stderr.write(` ${c.red('✗')} ${f.componentName} ${c.dim(f.error ?? 'unknown error')}\n`);
389
+ }
390
+ }
391
+ const totalClassified = generated.reduce((s, r) => s + r.classified, 0);
392
+ const totalExcluded = generated.reduce((s, r) => s + r.excluded, 0);
393
+ const allOk = failed.length === 0;
394
+ const cachedNote = cachedResults.length > 0 ? c.dim(` (${cachedResults.length} cached)`) : '';
395
+ process.stderr.write((allOk ? c.green('✓') : c.yellow('⚠')) +
396
+ ` ${generated.length + cachedResults.length}/${componentResults.length} components` +
397
+ cachedNote +
398
+ c.dim(` ${totalClassified} classified, ${totalExcluded} excluded`) +
399
+ '\n');
400
+ if (generated.length === 0 && cachedResults.length === 0) {
401
+ die('Error: all components failed to generate — check agent output above');
402
+ }
403
+ }
404
+ else if (skill === 'tokens') {
405
+ const noCache = opts.cache === false || process.env.EDS_NO_CACHE === '1';
406
+ const tokenInputContent = rawTokensInline ?? '';
407
+ const tokenInputHash = computeTokenInputHash(tokenInputContent);
408
+ const db = openPipelineDb();
409
+ try {
410
+ let resolvedSessionId = opts.session;
411
+ if (!resolvedSessionId) {
412
+ const s = db
413
+ .prepare(`SELECT s.id FROM sessions s
414
+ JOIN steps st ON st.session_id = s.id
415
+ WHERE st.command = 'analyze extract' AND st.status = 'complete'
416
+ ORDER BY st.started_at DESC LIMIT 1`)
417
+ .get();
418
+ if (s) {
419
+ resolvedSessionId = s.id;
420
+ }
421
+ else {
422
+ const { generateSessionId } = await import('../session/session-id.js');
423
+ const newId = generateSessionId();
424
+ const now = new Date().toISOString();
425
+ db.prepare('INSERT INTO sessions (id, name, created_at, updated_at) VALUES (?, NULL, ?, ?)').run(newId, now, now);
426
+ resolvedSessionId = newId;
427
+ }
428
+ }
429
+ // Check cache before invoking agent
430
+ if (!noCache) {
431
+ const tokenCached = lookupCache(db, tokenInputHash, 'token_set', '__tokens__');
432
+ if (tokenCached) {
433
+ copyTokensFromCache(db, tokenCached.sourceSessionId, resolvedSessionId);
434
+ sessionId = resolvedSessionId;
435
+ process.stderr.write(`Done: tokens reused from cache ${c.dim(`(source: ${tokenCached.sourceSessionId.slice(0, 12)})`)}\n`);
436
+ db.close();
437
+ // Skip agent invocation — jump to view
438
+ const viewResult = { skill, agent, sessionId: sessionId ?? '' };
439
+ if (process.stdout.isTTY) {
440
+ const { waitUntilExit } = render(createElement(GenerateView, { result: viewResult, onExit: () => process.exit(0) }));
441
+ await waitUntilExit();
442
+ }
443
+ else {
444
+ process.stdout.write(`generate complete\nskill: ${skill}\nagent: ${agent}\nsession: ${sessionId ?? ''}\n`);
445
+ process.exit(0);
446
+ }
447
+ return;
448
+ }
449
+ }
450
+ // Cache miss — invoke agent
451
+ const prompt = await buildPrompt({
452
+ skill,
453
+ mode: 'autonomous',
454
+ rawTokensInline,
455
+ rawTokensFilename: opts.rawTokens ? resolve(opts.rawTokens).split('/').pop() : undefined,
456
+ tokensInline,
457
+ tokenMapInline,
458
+ outDir: process.cwd(),
459
+ });
460
+ const result = await runAgent({
461
+ agent,
462
+ model: opts.model,
463
+ prompt,
464
+ interactive: false,
465
+ timeoutMs: DEFAULT_TIMEOUT_MS * 5,
466
+ });
467
+ if (result.timedOut) {
468
+ die(`Error: agent did not complete within ${(DEFAULT_TIMEOUT_MS * 5) / 60000} minutes`);
469
+ }
470
+ if (result.exitCode !== 0) {
471
+ if (result.stderr)
472
+ process.stderr.write(result.stderr);
473
+ die(`Error: agent exited with code ${result.exitCode}`);
474
+ }
475
+ const { calls: tokenCalls, warnings: tokenWarnings } = parseTokenToolCallLines(result.stdout);
476
+ const tokenCount = tokenCalls.filter((tc) => tc.tool === 'set_token').length;
477
+ if (tokenCount === 0) {
478
+ process.stderr.write(`Error: agent produced no set_token calls.\n` +
479
+ `Run with --dry-run to inspect the prompt.\n\n` +
480
+ `Agent output:\n${result.stdout}\n`);
481
+ process.exit(1);
482
+ }
483
+ if (tokenWarnings.length > 0) {
484
+ process.stderr.write(`Warnings:\n${tokenWarnings.map((w) => ` ${w}`).join('\n')}\n`);
485
+ }
486
+ applyTokenToolCalls(db, resolvedSessionId, tokenCalls, []);
487
+ if (!noCache) {
488
+ storeCache(db, tokenInputHash, 'token_set', '__tokens__', resolvedSessionId, false);
489
+ }
490
+ sessionId = resolvedSessionId;
491
+ const groupCount = tokenCalls.filter((tc) => tc.tool === 'set_group').length;
492
+ process.stderr.write(`Done: ${tokenCount} tokens, ${groupCount} groups stored\n`);
493
+ }
494
+ finally {
495
+ db.close();
496
+ }
497
+ }
498
+ const viewResult = {
499
+ skill,
500
+ agent,
501
+ sessionId: sessionId ?? '',
502
+ };
503
+ if (process.stdout.isTTY) {
504
+ const { waitUntilExit } = render(createElement(GenerateView, {
505
+ result: viewResult,
506
+ onExit: () => process.exit(0),
507
+ }));
508
+ await waitUntilExit();
509
+ }
510
+ else {
511
+ process.stdout.write(`generate complete\nskill: ${skill}\nagent: ${agent}\nsession: ${sessionId ?? ''}\n`);
512
+ process.exit(0);
513
+ }
514
+ }
515
+ function addAgentFlags(cmd) {
516
+ return cmd
517
+ .requiredOption('--agent <name>', 'Agent to use: claude, codex, opencode, cursor')
518
+ .option('--model <name>', 'Model to use (defaults to a small/fast model per agent)')
519
+ .option('--verbose', 'Show full agent output including reasoning text')
520
+ .option('--dry-run', 'Print the prompt without invoking the agent')
521
+ .option('--no-cache', 'Bypass generation cache and force AI re-generation');
522
+ }
523
+ export function registerGenerateCommand(program) {
524
+ const generate = program.command('generate').description('Generate CDF/DTCG artifacts or correct generation output');
525
+ // generate components subcommand
526
+ const componentsCmd = generate
527
+ .command('components')
528
+ .description('Invoke a coding agent to produce components.json from raw analysis output')
529
+ .option('--session <id>', 'Session ID from analyze extract (defaults to most recent)')
530
+ .option('--tokens <path>', 'Path to tokens.json for token-linked prop resolution')
531
+ .option('--token-map <path>', 'Path to token-name-map.json sidecar');
532
+ addAgentFlags(componentsCmd).action(async (opts) => {
533
+ await runGenerateSkill('components', opts, opts.verbose ?? false);
534
+ });
535
+ registerGenerateEditCommand(componentsCmd, 'components');
536
+ // generate tokens subcommand
537
+ const tokensCmd = generate
538
+ .command('tokens')
539
+ .description('Invoke a coding agent to produce tokens.json from raw token data')
540
+ .option('--raw-tokens <path>', 'Path to raw token input file');
541
+ addAgentFlags(tokensCmd).action(async (opts) => {
542
+ await runGenerateSkill('tokens', opts, opts.verbose ?? false);
543
+ });
544
+ registerGenerateEditCommand(tokensCmd, 'tokens');
545
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerGenerateEditCommand(parent: Command, skill: string): void;
@@ -0,0 +1,126 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ const SAFE_PATH_RE = /^[a-zA-Z0-9_.$[\]=]+$/;
4
+ const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
5
+ function applyDotPath(obj, path, value) {
6
+ if (!SAFE_PATH_RE.test(path)) {
7
+ process.stderr.write(`Warning: --patch path contains invalid characters: '${path}', skipping\n`);
8
+ return;
9
+ }
10
+ const parts = path.split('.');
11
+ if (parts.some((p) => PROTO_KEYS.has(p))) {
12
+ process.stderr.write(`Warning: --patch path contains forbidden key: '${path}', skipping\n`);
13
+ return;
14
+ }
15
+ let current = obj;
16
+ for (let i = 0; i < parts.length - 1; i++) {
17
+ const part = parts[i];
18
+ const arrayMatch = /^(.+)\[name=(.+)\]$/.exec(part);
19
+ if (arrayMatch) {
20
+ const [, fieldName, matchValue] = arrayMatch;
21
+ const arr = current[fieldName];
22
+ if (Array.isArray(arr)) {
23
+ const item = arr.find((el) => el['name'] === matchValue);
24
+ if (item) {
25
+ current = item;
26
+ }
27
+ else {
28
+ process.stderr.write(`Warning: --patch array item [name=${matchValue}] not found in '${fieldName}', skipping\n`);
29
+ return;
30
+ }
31
+ }
32
+ }
33
+ else {
34
+ if (typeof current[part] !== 'object' || current[part] === null) {
35
+ process.stderr.write(`Warning: --patch path '${path}' — '${part}' is not an object, skipping\n`);
36
+ return;
37
+ }
38
+ current = current[part];
39
+ }
40
+ }
41
+ const lastPart = parts[parts.length - 1];
42
+ current[lastPart] = value;
43
+ }
44
+ function applyPatch(components, ops) {
45
+ return components.map((c) => {
46
+ const op = ops.find((o) => o.component === c.name);
47
+ if (!op)
48
+ return c;
49
+ let updated = { ...c };
50
+ if (op.status) {
51
+ updated = { ...updated, status: op.status };
52
+ }
53
+ if (op.set) {
54
+ const clone = structuredClone(updated);
55
+ for (const [path, value] of Object.entries(op.set)) {
56
+ applyDotPath(clone, path, value);
57
+ }
58
+ updated = clone;
59
+ }
60
+ return updated;
61
+ });
62
+ }
63
+ async function loadComponentsFromSession(_sessionId, _skill) {
64
+ // TODO: read from pipeline.db session when the unified session layer ships
65
+ // For now return empty list; non-interactive flags operate on whatever is in the DB
66
+ process.stderr.write('Error: generate edit requires a session database (not yet implemented — coming in the next release)\n');
67
+ process.exit(1);
68
+ }
69
+ async function runNonInteractive(opts, skill) {
70
+ let components = await loadComponentsFromSession(opts.session, skill);
71
+ if (opts.acceptAll || (opts.reject ?? []).length > 0) {
72
+ const rejectPatterns = (opts.reject ?? []).map((p) => p.toLowerCase());
73
+ components = components.map((c) => {
74
+ const rejected = rejectPatterns.some((p) => c.name.toLowerCase().includes(p));
75
+ return { ...c, status: rejected ? 'rejected' : 'accepted' };
76
+ });
77
+ }
78
+ if (opts.patch) {
79
+ let patchOps;
80
+ try {
81
+ const raw = await readFile(resolve(opts.patch), 'utf8');
82
+ patchOps = JSON.parse(raw);
83
+ }
84
+ catch {
85
+ process.stderr.write(`Error: cannot read or parse --patch file: ${opts.patch}\n`);
86
+ process.exit(1);
87
+ return;
88
+ }
89
+ const knownNames = new Set(components.map((c) => c.name));
90
+ for (const op of patchOps) {
91
+ if (!knownNames.has(op.component)) {
92
+ process.stderr.write(`Warning: --patch targets unknown component '${op.component}', skipping\n`);
93
+ }
94
+ }
95
+ components = applyPatch(components, patchOps);
96
+ }
97
+ const accepted = components.filter((c) => c.status === 'accepted');
98
+ const rejected = components.filter((c) => c.status === 'rejected');
99
+ process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length}\n`);
100
+ }
101
+ export function registerGenerateEditCommand(parent, skill) {
102
+ parent
103
+ .command('edit')
104
+ .description(`Review and correct generate ${skill} output before pushing`)
105
+ .option('--session <id>', 'Session ID to operate on (defaults to most recent active session)')
106
+ .option('--accept-all', 'Accept all definitions without launching the TUI')
107
+ .option('--reject <pattern>', 'Reject definitions whose name contains pattern (repeatable)', collect, [])
108
+ .option('--patch <path>', 'Path to a JSON patch file for structured definition overrides')
109
+ .action(async ({ session, acceptAll, reject, patch }) => {
110
+ const nonInteractive = acceptAll || (reject ?? []).length > 0 || !!patch;
111
+ if (nonInteractive) {
112
+ await runNonInteractive({ session, acceptAll, reject, patch }, skill);
113
+ return;
114
+ }
115
+ if (!process.stdout.isTTY) {
116
+ process.stderr.write(`Error: generate ${skill} edit requires an interactive terminal\n`);
117
+ process.exit(1);
118
+ }
119
+ // TUI not yet implemented
120
+ process.stderr.write(`Error: interactive generate ${skill} edit TUI is not yet available. Use --accept-all, --reject, or --patch for non-interactive mode.\n`);
121
+ process.exit(1);
122
+ });
123
+ }
124
+ function collect(val, prev) {
125
+ return [...prev, val];
126
+ }
@@ -0,0 +1,18 @@
1
+ /** `components` — classify component props; `tokens` — classify design tokens; `select` — decide whether a component belongs in Contentful Experience Orchestration */
2
+ export type Skill = 'components' | 'tokens' | 'select';
3
+ export type Mode = 'autonomous' | 'interactive';
4
+ export interface PromptOptions {
5
+ skill: Skill;
6
+ mode: Mode;
7
+ rawComponentsInline?: string;
8
+ rawTokensInline?: string;
9
+ /** Original filename for raw tokens — used to set the correct code fence language. */
10
+ rawTokensFilename?: string;
11
+ tokensInline?: string;
12
+ tokenMapInline?: string;
13
+ outDir: string;
14
+ /** For components skill only: the single component's name (used in error messages). */
15
+ componentName?: string;
16
+ }
17
+ export declare function buildPrompt(options: PromptOptions): Promise<string>;
18
+ export declare function resolveSkillPath(skill: Skill): string;