@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,765 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { appendFile, readFile, access } from 'node:fs/promises';
3
+ import { join, dirname } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { createInterface } from 'node:readline';
7
+ import { promisify } from 'node:util';
8
+ import { readExoCredentials, writeExoCredentials, exoCredentialsPath } from '../credentials-store.js';
9
+ const execFileAsync = promisify(execFile);
10
+ const REQUIRED_NODE_MAJOR = 24;
11
+ // ── Output helpers ────────────────────────────────────────────────────────────
12
+ function ok(msg) {
13
+ process.stdout.write(` \x1b[32m✓\x1b[0m ${msg}\n`);
14
+ }
15
+ function fail(msg) {
16
+ process.stdout.write(` \x1b[31m✗\x1b[0m ${msg}\n`);
17
+ }
18
+ function warn(msg) {
19
+ process.stdout.write(` \x1b[33m⚠\x1b[0m ${msg}\n`);
20
+ }
21
+ function info(msg) {
22
+ process.stdout.write(` ${msg}\n`);
23
+ }
24
+ function section(title, tag) {
25
+ const tagStr = tag ? (tag === '[required]' ? ` \x1b[31m[required]\x1b[0m` : ` \x1b[2m[optional]\x1b[0m`) : '';
26
+ process.stdout.write(`\n\x1b[1m${title}\x1b[0m${tagStr}\n`);
27
+ }
28
+ function dim(msg) {
29
+ process.stdout.write(`\x1b[2m${msg}\x1b[0m\n`);
30
+ }
31
+ // ── Prompt helpers ────────────────────────────────────────────────────────────
32
+ function prompt(question) {
33
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
34
+ return new Promise((resolve) => {
35
+ rl.question(question, (answer) => {
36
+ rl.close();
37
+ resolve(answer.trim());
38
+ });
39
+ });
40
+ }
41
+ function promptSecret(question) {
42
+ // Use readline for all prompts — mixing raw-mode stdin listeners with
43
+ // readline createInterface causes readline to buffer+unshift unconsumed
44
+ // input back onto the stream, which the raw listener then re-reads,
45
+ // doubling the typed value. Using readline throughout avoids this entirely.
46
+ return new Promise((resolve) => {
47
+ const rl = createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout,
50
+ terminal: process.stdin.isTTY,
51
+ });
52
+ let value = '';
53
+ process.stdout.write(question);
54
+ if (process.stdin.isTTY) {
55
+ // Intercept the readline output write so we can replace echoed chars with *
56
+ const origWrite = rl.output.write.bind(rl.output);
57
+ rl.output.write = (s) => {
58
+ // Allow newline through; suppress everything else (the echoed characters)
59
+ if (s === '\r\n' || s === '\n' || s === '\r')
60
+ origWrite(s);
61
+ };
62
+ }
63
+ rl.on('line', (line) => {
64
+ value = line;
65
+ rl.close();
66
+ });
67
+ rl.once('close', () => {
68
+ process.stdout.write('\n');
69
+ resolve(value);
70
+ });
71
+ });
72
+ }
73
+ async function confirm(question, defaultYes = true) {
74
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
75
+ const answer = await prompt(` ${question} ${hint} `);
76
+ if (!answer)
77
+ return defaultYes;
78
+ return answer.toLowerCase().startsWith('y');
79
+ }
80
+ // ── Shell helpers ─────────────────────────────────────────────────────────────
81
+ async function binaryExists(name) {
82
+ try {
83
+ await execFileAsync('which', [name]);
84
+ return true;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ function runSpawn(cmd, args, opts = {}) {
91
+ return new Promise((resolve) => {
92
+ const child = spawn(cmd, args, {
93
+ cwd: opts.cwd,
94
+ env: opts.env ?? process.env,
95
+ stdio: ['ignore', 'pipe', 'pipe'],
96
+ });
97
+ let settled = false;
98
+ let stdout = '';
99
+ let stderr = '';
100
+ child.on('error', (err) => {
101
+ if (!settled) {
102
+ settled = true;
103
+ resolve({ exitCode: 1, stdout: '', stderr: err.message });
104
+ }
105
+ });
106
+ child.stdout.on('data', (d) => {
107
+ stdout += String(d);
108
+ });
109
+ child.stderr.on('data', (d) => {
110
+ stderr += String(d);
111
+ });
112
+ child.on('exit', (code) => {
113
+ if (!settled) {
114
+ settled = true;
115
+ resolve({ exitCode: code ?? 1, stdout, stderr });
116
+ }
117
+ });
118
+ });
119
+ }
120
+ // ── Shell profile detection ───────────────────────────────────────────────────
121
+ async function detectShellProfile() {
122
+ const shell = process.env['SHELL'] ?? '';
123
+ const home = homedir();
124
+ if (shell.includes('zsh')) {
125
+ return join(home, '.zshrc');
126
+ }
127
+ if (shell.includes('bash')) {
128
+ // Prefer .bash_profile on macOS (login shell), .bashrc on Linux
129
+ const bashProfile = join(home, '.bash_profile');
130
+ const exists = await access(bashProfile)
131
+ .then(() => true)
132
+ .catch(() => false);
133
+ return exists ? bashProfile : join(home, '.bashrc');
134
+ }
135
+ if (shell.includes('fish')) {
136
+ return join(home, '.config', 'fish', 'config.fish');
137
+ }
138
+ return join(home, '.profile');
139
+ }
140
+ async function profileContains(profilePath, str) {
141
+ try {
142
+ const content = await readFile(profilePath, 'utf8');
143
+ return content.includes(str);
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
149
+ async function appendToProfile(profilePath, lines) {
150
+ await appendFile(profilePath, `\n${lines}\n`, 'utf8');
151
+ }
152
+ // ── Step 1: Node.js ───────────────────────────────────────────────────────────
153
+ async function setupNode() {
154
+ section('Step 1: Node.js', '[required]');
155
+ const current = process.versions.node;
156
+ const major = parseInt(current.split('.')[0], 10);
157
+ if (major >= REQUIRED_NODE_MAJOR) {
158
+ ok(`Node.js v${current} — already good`);
159
+ return true;
160
+ }
161
+ fail(`Node.js v${current} — need v${REQUIRED_NODE_MAJOR}+`);
162
+ info('');
163
+ const hasNvm = (await binaryExists('nvm')) ||
164
+ (await access(join(homedir(), '.nvm', 'nvm.sh'))
165
+ .then(() => true)
166
+ .catch(() => false));
167
+ const hasFnm = await binaryExists('fnm');
168
+ if (hasNvm) {
169
+ info(`nvm detected. Will run: nvm install ${REQUIRED_NODE_MAJOR} && nvm use ${REQUIRED_NODE_MAJOR}`);
170
+ const go = await confirm(`Install and switch to Node ${REQUIRED_NODE_MAJOR} via nvm?`);
171
+ if (!go) {
172
+ warn(`Skipped. Re-run exo setup after switching to Node ${REQUIRED_NODE_MAJOR}.`);
173
+ return false;
174
+ }
175
+ // nvm is a shell function so we source it and run in a subshell
176
+ const nvmScript = join(homedir(), '.nvm', 'nvm.sh');
177
+ const result = await runSpawn('bash', [
178
+ '-c',
179
+ `source "${nvmScript}" && nvm install ${REQUIRED_NODE_MAJOR} && nvm alias default ${REQUIRED_NODE_MAJOR}`,
180
+ ]);
181
+ if (result.exitCode !== 0) {
182
+ fail('nvm install failed');
183
+ info(result.stderr.trim().split('\n').slice(0, 5).join('\n'));
184
+ info(`Run manually: nvm install ${REQUIRED_NODE_MAJOR} && nvm use ${REQUIRED_NODE_MAJOR}`);
185
+ return false;
186
+ }
187
+ ok(`Node ${REQUIRED_NODE_MAJOR} installed via nvm. Re-run exo setup in a fresh shell to pick it up.`);
188
+ return false; // Need fresh shell to get the new node on PATH
189
+ }
190
+ if (hasFnm) {
191
+ info(`fnm detected. Will run: fnm install ${REQUIRED_NODE_MAJOR} && fnm use ${REQUIRED_NODE_MAJOR}`);
192
+ const go = await confirm(`Install and switch to Node ${REQUIRED_NODE_MAJOR} via fnm?`);
193
+ if (!go) {
194
+ warn(`Skipped. Re-run exo setup after switching to Node ${REQUIRED_NODE_MAJOR}.`);
195
+ return false;
196
+ }
197
+ const result = await runSpawn('fnm', ['install', String(REQUIRED_NODE_MAJOR)]);
198
+ if (result.exitCode !== 0) {
199
+ fail('fnm install failed');
200
+ info(`Run manually: fnm install ${REQUIRED_NODE_MAJOR} && fnm use ${REQUIRED_NODE_MAJOR}`);
201
+ return false;
202
+ }
203
+ const useResult = await runSpawn('fnm', ['use', String(REQUIRED_NODE_MAJOR)]);
204
+ if (useResult.exitCode !== 0) {
205
+ warn(`fnm use ${REQUIRED_NODE_MAJOR} failed — node installed but not activated`);
206
+ info(`Run manually: fnm use ${REQUIRED_NODE_MAJOR} && fnm default ${REQUIRED_NODE_MAJOR}`);
207
+ }
208
+ else {
209
+ const defaultResult = await runSpawn('fnm', ['default', String(REQUIRED_NODE_MAJOR)]);
210
+ if (defaultResult.exitCode !== 0) {
211
+ warn(`fnm default ${REQUIRED_NODE_MAJOR} failed — version won't persist across new shells`);
212
+ info(`Run manually: fnm default ${REQUIRED_NODE_MAJOR}`);
213
+ }
214
+ }
215
+ ok(`Node ${REQUIRED_NODE_MAJOR} installed via fnm. Re-run exo setup in a fresh shell.`);
216
+ return false;
217
+ }
218
+ // No version manager found — offer to install nvm
219
+ info('No Node version manager detected (nvm or fnm).');
220
+ const installNvm = await confirm('Install nvm now? (recommended)');
221
+ if (installNvm) {
222
+ info('Running nvm install script...');
223
+ const result = await runSpawn('bash', [
224
+ '-c',
225
+ 'curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash',
226
+ ]);
227
+ if (result.exitCode !== 0) {
228
+ fail('nvm install failed');
229
+ info('Install manually: https://github.com/nvm-sh/nvm#installing-and-updating');
230
+ return false;
231
+ }
232
+ ok('nvm installed. Open a new shell, then re-run exo setup.');
233
+ return false;
234
+ }
235
+ info(`Install Node ${REQUIRED_NODE_MAJOR} manually from https://nodejs.org`);
236
+ return false;
237
+ }
238
+ // ── Step 2: pnpm ─────────────────────────────────────────────────────────────
239
+ async function setupPnpm() {
240
+ section('Step 2: pnpm', '[required]');
241
+ if (await binaryExists('pnpm')) {
242
+ const v = await runSpawn('pnpm', ['--version']);
243
+ ok(`pnpm v${v.stdout.trim()} — already installed`);
244
+ return true;
245
+ }
246
+ fail('pnpm not found');
247
+ info('');
248
+ const hasCorecpack = await binaryExists('corepack');
249
+ if (hasCorecpack) {
250
+ info('Will run: corepack enable && corepack prepare pnpm@latest --activate');
251
+ const go = await confirm('Install pnpm via corepack?');
252
+ if (go) {
253
+ const r1 = await runSpawn('corepack', ['enable']);
254
+ const r2 = r1.exitCode === 0 ? await runSpawn('corepack', ['prepare', 'pnpm@latest', '--activate']) : r1;
255
+ if (r2.exitCode !== 0) {
256
+ fail('corepack install failed');
257
+ info('Try: npm install -g pnpm');
258
+ return false;
259
+ }
260
+ ok('pnpm installed via corepack');
261
+ return true;
262
+ }
263
+ }
264
+ info('Will run: npm install -g pnpm');
265
+ const go = await confirm('Install pnpm via npm?');
266
+ if (!go) {
267
+ warn('Skipped. Install pnpm manually: npm install -g pnpm');
268
+ return false;
269
+ }
270
+ const result = await runSpawn('npm', ['install', '-g', 'pnpm']);
271
+ if (result.exitCode !== 0) {
272
+ fail('npm install -g pnpm failed');
273
+ info(result.stderr.trim().split('\n').slice(0, 5).join('\n'));
274
+ return false;
275
+ }
276
+ ok('pnpm installed');
277
+ return true;
278
+ }
279
+ // ── Step 3: install + build ───────────────────────────────────────────────────
280
+ async function setupBuild(repoRoot) {
281
+ section('Step 3: Install dependencies & build', '[required]');
282
+ info('Running pnpm install...');
283
+ const installResult = await runSpawn('pnpm', ['install', '--frozen-lockfile'], { cwd: repoRoot });
284
+ if (installResult.exitCode !== 0) {
285
+ fail('pnpm install failed');
286
+ const errLines = installResult.stderr.trim().split('\n').slice(0, 8);
287
+ for (const line of errLines)
288
+ info(line);
289
+ info('');
290
+ info('Try: pnpm install (without --frozen-lockfile) to update the lockfile');
291
+ return false;
292
+ }
293
+ ok('Dependencies installed');
294
+ info('Building CLI...');
295
+ const buildResult = await runSpawn('pnpm', ['--filter', '@contentful/experience-design-system-cli', 'run', 'build'], {
296
+ cwd: repoRoot,
297
+ });
298
+ if (buildResult.exitCode !== 0) {
299
+ fail('Build failed');
300
+ const errLines = buildResult.stderr.trim().split('\n').slice(0, 10);
301
+ for (const line of errLines)
302
+ info(line);
303
+ return false;
304
+ }
305
+ ok('CLI built successfully');
306
+ return true;
307
+ }
308
+ // ── Step 4: agent CLI ─────────────────────────────────────────────────────────
309
+ async function setupAgent() {
310
+ section('Step 4: Coding agent (claude, codex, opencode, or cursor)', '[required]');
311
+ info('exo import uses a coding agent to generate component definitions.');
312
+ info('');
313
+ const agents = [
314
+ { name: 'Claude Code', binary: 'claude', installHint: 'npm install -g @anthropic-ai/claude-code && claude login' },
315
+ { name: 'OpenAI Codex', binary: 'codex', installHint: 'npm install -g @openai/codex (requires OPENAI_API_KEY)' },
316
+ { name: 'OpenCode', binary: 'opencode', installHint: 'npm install -g opencode-ai && opencode auth' },
317
+ ];
318
+ for (const agent of agents) {
319
+ if (await binaryExists(agent.binary)) {
320
+ ok(`${agent.name} (${agent.binary}) found`);
321
+ return true;
322
+ }
323
+ }
324
+ warn('No coding agent found on PATH');
325
+ info('');
326
+ info('Choose one to install:');
327
+ info(' [1] Claude Code (recommended) — npm install -g @anthropic-ai/claude-code');
328
+ info(' [2] OpenAI Codex — npm install -g @openai/codex');
329
+ info(' [3] OpenCode — npm install -g opencode-ai');
330
+ info(' [s] Skip for now');
331
+ info('');
332
+ const choice = await prompt(' Your choice: ');
333
+ if (choice === '1' || choice === '') {
334
+ const r = await runSpawn('npm', ['install', '-g', '@anthropic-ai/claude-code']);
335
+ if (r.exitCode !== 0) {
336
+ fail('Install failed');
337
+ info(r.stderr.trim().split('\n').slice(0, 5).join('\n'));
338
+ return false;
339
+ }
340
+ if (!(await binaryExists('claude'))) {
341
+ fail('claude binary not found on PATH after install — check your npm global bin directory');
342
+ return false;
343
+ }
344
+ ok('Claude Code installed');
345
+ info('');
346
+ info('Next: run `claude login` to authenticate (browser OAuth).');
347
+ info('Or set ANTHROPIC_API_KEY in your shell profile.');
348
+ return true;
349
+ }
350
+ if (choice === '2') {
351
+ const r = await runSpawn('npm', ['install', '-g', '@openai/codex']);
352
+ if (r.exitCode !== 0) {
353
+ fail('Install failed');
354
+ return false;
355
+ }
356
+ if (!(await binaryExists('codex'))) {
357
+ fail('codex binary not found on PATH after install — check your npm global bin directory');
358
+ return false;
359
+ }
360
+ ok('OpenAI Codex installed');
361
+ info('Set OPENAI_API_KEY in your shell profile to authenticate.');
362
+ return true;
363
+ }
364
+ if (choice === '3') {
365
+ const r = await runSpawn('npm', ['install', '-g', 'opencode-ai']);
366
+ if (r.exitCode !== 0) {
367
+ fail('Install failed');
368
+ return false;
369
+ }
370
+ if (!(await binaryExists('opencode'))) {
371
+ fail('opencode binary not found on PATH after install — check your npm global bin directory');
372
+ return false;
373
+ }
374
+ ok('OpenCode installed');
375
+ info('Run `opencode auth` to configure your provider.');
376
+ return true;
377
+ }
378
+ warn('Skipped. Install a coding agent before running exo import.');
379
+ return false;
380
+ }
381
+ // ── Step 5: Contentful credentials ───────────────────────────────────────────
382
+ async function setupContentfulCredentials() {
383
+ section('Step 5: Contentful credentials', '[optional]');
384
+ info(`Saved to ${exoCredentialsPath()} — loaded automatically by exo import.`);
385
+ info('');
386
+ const stored = await readExoCredentials();
387
+ const currentSpace = stored.spaceId;
388
+ const currentEnv = stored.environmentId;
389
+ const currentToken = stored.cmaToken;
390
+ const hasAny = !!(currentSpace || currentEnv || currentToken);
391
+ if (hasAny) {
392
+ info('Current values:');
393
+ if (currentSpace) {
394
+ ok(`Space ID ${currentSpace}`);
395
+ }
396
+ else {
397
+ warn('Space ID (not set)');
398
+ }
399
+ if (currentEnv) {
400
+ ok(`Environment ID ${currentEnv}`);
401
+ }
402
+ else {
403
+ warn('Environment ID (not set)');
404
+ }
405
+ if (currentToken) {
406
+ ok(`CMA Token ${'•'.repeat(Math.min(currentToken.length, 8))}...`);
407
+ }
408
+ else {
409
+ warn('CMA Token (not set)');
410
+ }
411
+ info('');
412
+ }
413
+ const allSet = !!(currentSpace && currentEnv && currentToken);
414
+ const doUpdate = await confirm(hasAny ? 'Update credentials?' : 'Configure Contentful credentials?', !allSet);
415
+ if (!doUpdate) {
416
+ if (allSet) {
417
+ ok('Credentials already configured — no changes made');
418
+ }
419
+ else {
420
+ warn('Skipped. exo import will prompt for credentials interactively.');
421
+ }
422
+ return true;
423
+ }
424
+ info('');
425
+ info('Get your CMA token: Contentful web app → Settings → API keys → Content management tokens');
426
+ info('');
427
+ const spaceIdInput = await prompt(` Space ID${currentSpace ? ` [${currentSpace}]` : ''}: `);
428
+ const spaceId = spaceIdInput || currentSpace;
429
+ const envIdInput = await prompt(` Environment ID [${currentEnv || 'master'}]: `);
430
+ const environmentId = envIdInput || currentEnv || 'master';
431
+ const tokenInput = await promptSecret(` CMA token${currentToken ? ' [press Enter to keep existing]' : ' (paste here)'}: `);
432
+ const cmaToken = tokenInput || currentToken;
433
+ if (!cmaToken || !spaceId) {
434
+ warn('Space ID and CMA token are required. Skipped.');
435
+ return false;
436
+ }
437
+ await writeExoCredentials({ spaceId, environmentId, cmaToken });
438
+ ok(`Credentials saved to ${exoCredentialsPath()}`);
439
+ info('Run exo import — the credentials step will be pre-filled automatically.');
440
+ return true;
441
+ }
442
+ // ── Step 6: Optional quality-of-life ─────────────────────────────────────────
443
+ async function setupQoL(profilePath) {
444
+ section('Step 6: Optional extras', '[optional]');
445
+ info('These are not required for exo import but improve the experience.');
446
+ info('');
447
+ // 6a: EDS_EXTRACT_CONCURRENCY
448
+ const hasConcurrency = await profileContains(profilePath, 'EDS_EXTRACT_CONCURRENCY');
449
+ if (!hasConcurrency) {
450
+ info('EDS_EXTRACT_CONCURRENCY — controls how many components are analyzed in parallel.');
451
+ info('Default is 4. Set higher (e.g. 8) on fast machines to speed up large codebases.');
452
+ const setConcurrency = await confirm('Add EDS_EXTRACT_CONCURRENCY=8 to your profile?', false);
453
+ if (setConcurrency) {
454
+ await appendToProfile(profilePath, '# exo performance\nexport EDS_EXTRACT_CONCURRENCY=8');
455
+ ok(`EDS_EXTRACT_CONCURRENCY=8 written to ${profilePath}`);
456
+ }
457
+ else {
458
+ dim(' skipped');
459
+ }
460
+ }
461
+ else {
462
+ ok('EDS_EXTRACT_CONCURRENCY — already set');
463
+ }
464
+ // 6b: NO_COLOR
465
+ info('');
466
+ info('NO_COLOR — set to 1 to disable ANSI color output (useful in CI or plain terminals).');
467
+ const setNoColor = await confirm('Add NO_COLOR=1 (disable colors) to your profile?', false);
468
+ if (setNoColor) {
469
+ const hasNoColor = await profileContains(profilePath, 'NO_COLOR');
470
+ if (!hasNoColor) {
471
+ await appendToProfile(profilePath, 'export NO_COLOR=1');
472
+ ok(`NO_COLOR=1 written to ${profilePath}`);
473
+ }
474
+ else {
475
+ warn('NO_COLOR already present in profile — skipping');
476
+ }
477
+ }
478
+ else {
479
+ dim(' skipped');
480
+ }
481
+ }
482
+ // ── Doctor checks ─────────────────────────────────────────────────────────────
483
+ async function checkNode() {
484
+ section('Checking Node.js version');
485
+ const current = process.versions.node;
486
+ const major = parseInt(current.split('.')[0], 10);
487
+ if (major < REQUIRED_NODE_MAJOR) {
488
+ fail(`Node.js v${current} — need v${REQUIRED_NODE_MAJOR}+`);
489
+ info('');
490
+ info('How to fix:');
491
+ if (await binaryExists('nvm')) {
492
+ info(` nvm install ${REQUIRED_NODE_MAJOR}`);
493
+ info(` nvm use ${REQUIRED_NODE_MAJOR}`);
494
+ info(` nvm alias default ${REQUIRED_NODE_MAJOR} # make it permanent`);
495
+ }
496
+ else if (await binaryExists('fnm')) {
497
+ info(` fnm install ${REQUIRED_NODE_MAJOR}`);
498
+ info(` fnm use ${REQUIRED_NODE_MAJOR}`);
499
+ }
500
+ else {
501
+ info(` Download Node v${REQUIRED_NODE_MAJOR} from https://nodejs.org`);
502
+ }
503
+ return false;
504
+ }
505
+ ok(`Node.js v${current}`);
506
+ return true;
507
+ }
508
+ async function checkPnpm(pkgRoot) {
509
+ section('Checking pnpm');
510
+ if (!(await binaryExists('pnpm'))) {
511
+ fail('pnpm not found');
512
+ info('How to fix:');
513
+ info(' npm install -g pnpm');
514
+ info(' # or: corepack enable pnpm');
515
+ return false;
516
+ }
517
+ const versionResult = await runSpawn('pnpm', ['--version']);
518
+ if (versionResult.exitCode !== 0) {
519
+ fail('pnpm found but not working');
520
+ info('Try reinstalling: npm install -g pnpm --force');
521
+ return false;
522
+ }
523
+ ok(`pnpm v${versionResult.stdout.trim()}`);
524
+ const pingResult = await runSpawn('pnpm', ['exec', 'node', '--version'], { cwd: pkgRoot });
525
+ if (pingResult.exitCode !== 0) {
526
+ fail('pnpm cannot execute in repo root');
527
+ info('The pnpm global store may be mismatched with your current Node version.');
528
+ info('How to fix:');
529
+ info(' npm install -g pnpm --force');
530
+ return false;
531
+ }
532
+ return true;
533
+ }
534
+ async function checkDependencies(pkgRoot) {
535
+ section('Checking dependencies (pnpm install)');
536
+ const nodeModulesExists = await access(join(pkgRoot, 'node_modules'))
537
+ .then(() => true)
538
+ .catch(() => false);
539
+ if (!nodeModulesExists) {
540
+ info('node_modules not found — running pnpm install...');
541
+ }
542
+ else {
543
+ info('Running pnpm install to ensure dependencies are up to date...');
544
+ }
545
+ const repoRoot = join(pkgRoot, '..', '..');
546
+ const result = await runSpawn('pnpm', ['install', '--frozen-lockfile'], { cwd: repoRoot });
547
+ if (result.exitCode !== 0) {
548
+ fail('pnpm install failed');
549
+ info('');
550
+ const errLines = result.stderr.trim().split('\n').slice(0, 10);
551
+ for (const line of errLines)
552
+ info(line);
553
+ info('');
554
+ info('How to fix:');
555
+ info(' • Try: pnpm install (without --frozen-lockfile) to update the lockfile');
556
+ info(' • Check that your Node version matches: cat .nvmrc');
557
+ return false;
558
+ }
559
+ ok('Dependencies installed');
560
+ return true;
561
+ }
562
+ async function checkBuild(pkgRoot) {
563
+ section('Building CLI');
564
+ info('Running pnpm build...');
565
+ const repoRoot = join(pkgRoot, '..', '..');
566
+ const result = await runSpawn('pnpm', ['--filter', '@contentful/experience-design-system-cli', 'run', 'build'], {
567
+ cwd: repoRoot,
568
+ });
569
+ if (result.exitCode !== 0) {
570
+ fail('Build failed');
571
+ info('');
572
+ const errLines = result.stderr.trim().split('\n').slice(0, 15);
573
+ for (const line of errLines)
574
+ info(line);
575
+ info('');
576
+ info('How to fix:');
577
+ info(' • Check for TypeScript errors: pnpm typecheck');
578
+ info(' • If the error is in a generated file under dist/, try: pnpm clean && pnpm build');
579
+ return false;
580
+ }
581
+ ok('Build succeeded');
582
+ return true;
583
+ }
584
+ async function checkAgent() {
585
+ section('Checking coding agent');
586
+ const agents = [
587
+ { name: 'Claude Code', binary: 'claude' },
588
+ { name: 'OpenAI Codex', binary: 'codex' },
589
+ { name: 'OpenCode', binary: 'opencode' },
590
+ ];
591
+ for (const agent of agents) {
592
+ if (await binaryExists(agent.binary)) {
593
+ ok(`${agent.name} (${agent.binary}) found`);
594
+ return true;
595
+ }
596
+ }
597
+ warn('No coding agent found on PATH');
598
+ info('The coding agent is required for the generate steps in exo import.');
599
+ info('Install one of:');
600
+ info(' • Claude Code: npm install -g @anthropic-ai/claude-code');
601
+ info(' • OpenAI Codex: npm install -g @openai/codex');
602
+ info(' • OpenCode: npm install -g opencode-ai');
603
+ return false;
604
+ }
605
+ // ── Commands ──────────────────────────────────────────────────────────────────
606
+ export function registerSetupCommand(program) {
607
+ program
608
+ .command('doctor')
609
+ .description('Check prerequisites so exo import runs without errors')
610
+ .option('--skip-build', 'Skip the pnpm install + build step (useful if already built)')
611
+ .option('--skip-agent', 'Skip the coding agent check')
612
+ .action(async (opts) => {
613
+ process.stderr.write('\x1b[1mexo doctor\x1b[0m — checking your environment\n');
614
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
615
+ const results = [];
616
+ const nodeOk = await checkNode();
617
+ results.push({ name: 'Node.js version', ok: nodeOk, required: true });
618
+ if (nodeOk) {
619
+ const pnpmOk = await checkPnpm(pkgRoot);
620
+ results.push({ name: 'pnpm', ok: pnpmOk, required: true });
621
+ if (!opts.skipBuild) {
622
+ if (pnpmOk) {
623
+ const depsOk = await checkDependencies(pkgRoot);
624
+ results.push({ name: 'dependencies', ok: depsOk, required: true });
625
+ if (depsOk) {
626
+ const buildOk = await checkBuild(pkgRoot);
627
+ results.push({ name: 'build', ok: buildOk, required: true });
628
+ }
629
+ }
630
+ }
631
+ else {
632
+ info('\nSkipping install + build (--skip-build)');
633
+ }
634
+ }
635
+ if (!opts.skipAgent) {
636
+ const agentOk = await checkAgent();
637
+ results.push({ name: 'coding agent', ok: agentOk, required: false });
638
+ }
639
+ section('Summary');
640
+ const failed = results.filter((r) => !r.ok);
641
+ const requiredFailed = failed.filter((r) => r.required);
642
+ for (const r of results) {
643
+ if (r.ok) {
644
+ ok(r.name);
645
+ }
646
+ else if (r.required) {
647
+ fail(`${r.name} — required`);
648
+ }
649
+ else {
650
+ warn(`${r.name} — optional`);
651
+ }
652
+ }
653
+ if (requiredFailed.length === 0 && failed.length === 0) {
654
+ process.stderr.write('\n\x1b[32m\x1b[1m✓ All checks passed. You are ready to run: exo import\x1b[0m\n\n');
655
+ process.exit(0);
656
+ }
657
+ else if (requiredFailed.length === 0) {
658
+ process.stderr.write('\n\x1b[33m\x1b[1m⚠ Required checks passed, but optional checks failed.\x1b[0m\n');
659
+ process.stderr.write(' You can run \x1b[1mexo import\x1b[0m but the generate steps may fail without a coding agent.\n\n');
660
+ process.exit(0);
661
+ }
662
+ else {
663
+ process.stderr.write(`\n\x1b[31m\x1b[1m✗ ${requiredFailed.length} required check${requiredFailed.length === 1 ? '' : 's'} failed.\x1b[0m\n`);
664
+ process.stderr.write(' Fix the issues above, then re-run \x1b[1mexo doctor\x1b[0m.\n\n');
665
+ process.exit(1);
666
+ }
667
+ });
668
+ program
669
+ .command('setup')
670
+ .description('Interactive setup wizard: installs prerequisites and configures credentials for exo import')
671
+ .option('--skip-build', 'Skip the pnpm install + build step')
672
+ .option('--skip-agent', 'Skip the coding agent check')
673
+ .option('--skip-credentials', 'Skip the Contentful credentials step')
674
+ .option('--skip-optional', 'Skip optional quality-of-life extras')
675
+ .action(async (opts) => {
676
+ process.stdout.write('\n\x1b[1mexo setup\x1b[0m — interactive setup wizard\n');
677
+ process.stdout.write('Sets up everything you need to run \x1b[1mexo import\x1b[0m.\n');
678
+ process.stdout.write('Required steps are marked \x1b[31m[required]\x1b[0m, optional ones \x1b[2m[optional]\x1b[0m.\n');
679
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
680
+ const repoRoot = join(pkgRoot, '..', '..');
681
+ const profilePath = await detectShellProfile();
682
+ const results = [];
683
+ // Step 1: Node
684
+ const nodeOk = await setupNode();
685
+ results.push({ name: 'Node.js 24+', passed: nodeOk, required: true });
686
+ if (!nodeOk) {
687
+ process.stdout.write('\n\x1b[33mNode.js setup requires a shell restart. Re-run exo setup afterwards.\x1b[0m\n\n');
688
+ process.exit(0);
689
+ }
690
+ // Step 2: pnpm
691
+ const pnpmOk = await setupPnpm();
692
+ results.push({ name: 'pnpm', passed: pnpmOk, required: true });
693
+ // Step 3: install + build
694
+ if (!opts.skipBuild && pnpmOk) {
695
+ const buildOk = await setupBuild(repoRoot);
696
+ results.push({ name: 'install & build', passed: buildOk, required: true });
697
+ }
698
+ else if (opts.skipBuild) {
699
+ info('\nSkipping install + build (--skip-build)');
700
+ }
701
+ // Step 4: agent
702
+ if (!opts.skipAgent) {
703
+ const agentOk = await setupAgent();
704
+ results.push({ name: 'coding agent', passed: agentOk, required: true });
705
+ }
706
+ else {
707
+ info('\nSkipping agent check (--skip-agent)');
708
+ results.push({ name: 'coding agent', passed: true, required: false });
709
+ }
710
+ // Step 5: credentials
711
+ if (!opts.skipCredentials) {
712
+ const credsOk = await setupContentfulCredentials();
713
+ results.push({ name: 'Contentful credentials', passed: credsOk, required: false });
714
+ }
715
+ else {
716
+ info('\nSkipping credentials (--skip-credentials)');
717
+ }
718
+ // Step 6: optional QoL
719
+ if (!opts.skipOptional) {
720
+ await setupQoL(profilePath);
721
+ }
722
+ // ── Summary ────────────────────────────────────────────────────────────
723
+ section('Summary');
724
+ const requiredFailed = results.filter((r) => r.required && !r.passed);
725
+ const optionalFailed = results.filter((r) => !r.required && !r.passed);
726
+ for (const r of results) {
727
+ if (r.passed) {
728
+ ok(`${r.name}`);
729
+ }
730
+ else if (r.required) {
731
+ fail(`${r.name} — required`);
732
+ }
733
+ else {
734
+ warn(`${r.name} — optional`);
735
+ }
736
+ }
737
+ process.stdout.write('\n');
738
+ if (requiredFailed.length === 0) {
739
+ process.stdout.write('\x1b[32m\x1b[1m✓ Setup complete. You can now run: exo import\x1b[0m\n');
740
+ if (optionalFailed.length > 0) {
741
+ process.stdout.write(" (Some optional steps were skipped — that's fine.)\n");
742
+ }
743
+ }
744
+ else {
745
+ process.stdout.write(`\x1b[33m\x1b[1m⚠ ${requiredFailed.length} required step${requiredFailed.length === 1 ? '' : 's'} incomplete.\x1b[0m\n`);
746
+ process.stdout.write(' Complete the steps above, then re-run \x1b[1mexo setup\x1b[0m.\n');
747
+ }
748
+ // ── Offer exo doctor ───────────────────────────────────────────────────
749
+ process.stdout.write('\n');
750
+ const runDoctor = process.stdout.isTTY &&
751
+ (await confirm('Run exo doctor now to verify your environment?', requiredFailed.length === 0));
752
+ if (runDoctor) {
753
+ process.stdout.write('\n');
754
+ const cliBin = process.argv[1] ?? fileURLToPath(import.meta.url);
755
+ const doctorResult = await runSpawn(process.execPath, [cliBin, 'doctor'], {
756
+ env: process.env,
757
+ });
758
+ process.stdout.write(doctorResult.stdout);
759
+ process.stderr.write(doctorResult.stderr);
760
+ process.exit(doctorResult.exitCode);
761
+ }
762
+ process.stdout.write('\nRun \x1b[1mexo doctor\x1b[0m at any time to re-check your environment.\n\n');
763
+ process.exit(requiredFailed.length === 0 ? 0 : 1);
764
+ });
765
+ }