@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,787 @@
1
+ import { createElement } from 'react';
2
+ import { render } from 'ink';
3
+ import { access, readFile, readdir, stat } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { validateCDF, flattenDTCG, validateDTCG, buildManifest, buildFilteredManifest, } from '@contentful/experience-design-system-types';
6
+ import { ApiError, ImportApiClient } from './api-client.js';
7
+ import { openPipelineDb, loadCDFComponents } from '../session/db.js';
8
+ import { ServerPreviewApp, ServerPreviewConfirm, ServerApplyProgress, ServerApplyDone } from './tui/ServerApplyView.js';
9
+ import { SelectView, makeSelectKey } from './tui/SelectView.js';
10
+ import { useState } from 'react';
11
+ import { useInput } from 'ink';
12
+ function die(message) {
13
+ process.stderr.write(`${message}\n`);
14
+ process.exit(1);
15
+ }
16
+ async function pathExists(p) {
17
+ return access(p)
18
+ .then(() => true)
19
+ .catch(() => false);
20
+ }
21
+ async function assertFileExists(flag, p) {
22
+ if (!(await pathExists(p)))
23
+ die(`Error: file not found: ${p} (from ${flag})`);
24
+ }
25
+ async function readJsonFile(flag, p) {
26
+ let text;
27
+ try {
28
+ text = await readFile(p, 'utf8');
29
+ }
30
+ catch {
31
+ die(`Error: file not found: ${p} (from ${flag})`);
32
+ }
33
+ try {
34
+ return JSON.parse(text);
35
+ }
36
+ catch {
37
+ die(`Error: ${flag} is not valid JSON: ${p}`);
38
+ }
39
+ }
40
+ const IGNORE_TOKEN_DIRS = new Set(['node_modules', 'dist', 'build', '.next', '.nuxt', '.git']);
41
+ async function collectJsonFiles(dir) {
42
+ const results = [];
43
+ async function walk(current) {
44
+ let entries;
45
+ try {
46
+ entries = await readdir(current);
47
+ }
48
+ catch {
49
+ return;
50
+ }
51
+ await Promise.all(entries.map(async (entry) => {
52
+ if (IGNORE_TOKEN_DIRS.has(entry))
53
+ return;
54
+ const full = join(current, entry);
55
+ let s;
56
+ try {
57
+ s = await stat(full);
58
+ }
59
+ catch {
60
+ return;
61
+ }
62
+ if (s.isDirectory()) {
63
+ await walk(full);
64
+ }
65
+ else if (entry.endsWith('.json')) {
66
+ results.push(full);
67
+ }
68
+ }));
69
+ }
70
+ await walk(dir);
71
+ return results;
72
+ }
73
+ export async function readTokensFromPath(flag, p) {
74
+ let s;
75
+ try {
76
+ s = await stat(p);
77
+ }
78
+ catch {
79
+ die(`Error: file not found: ${p} (from ${flag})`);
80
+ }
81
+ if (s.isDirectory()) {
82
+ const files = await collectJsonFiles(p);
83
+ if (files.length === 0)
84
+ die(`Error: no .json files found in directory: ${p} (from ${flag})`);
85
+ const merged = {};
86
+ for (const file of files.sort()) {
87
+ let text;
88
+ try {
89
+ text = await readFile(file, 'utf8');
90
+ }
91
+ catch {
92
+ continue;
93
+ }
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(text);
97
+ }
98
+ catch {
99
+ continue;
100
+ }
101
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
102
+ Object.assign(merged, parsed);
103
+ }
104
+ }
105
+ const { valid, errors } = validateDTCG(merged);
106
+ if (!valid)
107
+ die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
108
+ return flattenDTCG(merged, '');
109
+ }
110
+ const raw = await readJsonFile(flag, p);
111
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
112
+ die(`Error: ${flag} is not valid JSON: expected an object`);
113
+ }
114
+ const { valid, errors } = validateDTCG(raw);
115
+ if (!valid)
116
+ die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
117
+ return flattenDTCG(raw, '');
118
+ }
119
+ async function resolveSharedInputs(opts) {
120
+ if (!opts.components && !opts.tokens && !opts.session) {
121
+ die('Error: at least one of --components, --tokens, or --session is required');
122
+ }
123
+ if (opts.session && opts.components) {
124
+ die('Error: --session and --components are mutually exclusive');
125
+ }
126
+ const spaceId = opts.spaceId ?? process.env.CONTENTFUL_SPACE_ID;
127
+ const environmentId = opts.environmentId ?? process.env.CONTENTFUL_ENVIRONMENT_ID;
128
+ if (!spaceId)
129
+ die('Error: --space-id is required (or set CONTENTFUL_SPACE_ID)');
130
+ if (!environmentId)
131
+ die('Error: --environment-id is required (or set CONTENTFUL_ENVIRONMENT_ID)');
132
+ opts.spaceId = spaceId;
133
+ opts.environmentId = environmentId;
134
+ const cmaToken = opts.cmaToken ?? process.env.CONTENTFUL_MANAGEMENT_TOKEN;
135
+ if (!cmaToken) {
136
+ die('Error: CMA token is required. Pass --cma-token or set CONTENTFUL_MANAGEMENT_TOKEN');
137
+ }
138
+ if (opts.components)
139
+ await assertFileExists('--components', opts.components);
140
+ let components = [];
141
+ if (opts.session) {
142
+ const db = openPipelineDb();
143
+ try {
144
+ components = loadCDFComponents(db, opts.session);
145
+ }
146
+ finally {
147
+ db.close();
148
+ }
149
+ if (components.length === 0) {
150
+ die(`Error: session '${opts.session}' has no generated components. Run generate components first.`);
151
+ }
152
+ }
153
+ else if (opts.components) {
154
+ const raw = await readJsonFile('--components', opts.components);
155
+ const result = validateCDF(raw);
156
+ if (!result.valid) {
157
+ die(`Error: --components failed schema validation: ${result.errors.map((e) => e.message).join(', ')}`);
158
+ }
159
+ components = result.components;
160
+ }
161
+ let tokens = [];
162
+ if (opts.tokens) {
163
+ tokens = await readTokensFromPath('--tokens', opts.tokens);
164
+ }
165
+ const client = new ImportApiClient({
166
+ host: opts.host,
167
+ cmaToken,
168
+ spaceId,
169
+ environmentId,
170
+ });
171
+ return { components, tokens, client };
172
+ }
173
+ // --- Output helpers ---
174
+ function isEmptyPreview(preview) {
175
+ const { components, tokens, taxonomies } = preview;
176
+ return (components.new.length === 0 &&
177
+ components.changed.length === 0 &&
178
+ components.removed.length === 0 &&
179
+ tokens.new.length === 0 &&
180
+ tokens.changed.length === 0 &&
181
+ tokens.removed.length === 0 &&
182
+ taxonomies.new.length === 0 &&
183
+ taxonomies.changed.length === 0 &&
184
+ taxonomies.removed.length === 0);
185
+ }
186
+ export function hasBreakingChangesWithImpact(preview) {
187
+ const allChanged = [...preview.components.changed, ...preview.tokens.changed];
188
+ return allChanged.some((c) => c.changeClassification?.classification === 'breaking' &&
189
+ c.impact &&
190
+ (c.impact.affectedFragments > 0 || c.impact.affectedExperiences > 0));
191
+ }
192
+ function buildPreviewOutput(preview, spaceId, environmentId) {
193
+ return {
194
+ spaceId,
195
+ environmentId,
196
+ components: {
197
+ new: preview.components.new.length,
198
+ changed: preview.components.changed.length,
199
+ unchanged: preview.components.unchanged.length,
200
+ removed: preview.components.removed.length,
201
+ breaking: preview.components.changed.filter((c) => c.changeClassification?.classification === 'breaking').length,
202
+ draftOverwrites: preview.components.changed.filter((c) => c.hasPendingDraftChanges).length,
203
+ },
204
+ tokens: {
205
+ new: preview.tokens.new.length,
206
+ changed: preview.tokens.changed.length,
207
+ unchanged: preview.tokens.unchanged.length,
208
+ removed: preview.tokens.removed.length,
209
+ draftOverwrites: preview.tokens.changed.filter((c) => c.hasPendingDraftChanges).length,
210
+ },
211
+ taxonomies: {
212
+ new: preview.taxonomies.new.length,
213
+ changed: preview.taxonomies.changed.length,
214
+ unchanged: preview.taxonomies.unchanged.length,
215
+ removed: preview.taxonomies.removed.length,
216
+ },
217
+ };
218
+ }
219
+ function buildApplyOutput(operation, spaceId, environmentId) {
220
+ const items = operation.items ?? [];
221
+ const componentItems = items.filter((i) => i.entityType === 'ComponentType');
222
+ const tokenItems = items.filter((i) => i.entityType === 'DesignToken');
223
+ function countByAction(subset) {
224
+ return {
225
+ created: subset.filter((i) => i.action === 'create' && i.status === 'succeeded').length,
226
+ updated: subset.filter((i) => i.action === 'update' && i.status === 'succeeded').length,
227
+ failed: subset.filter((i) => i.status === 'failed').length,
228
+ };
229
+ }
230
+ return {
231
+ status: operation.sys.status,
232
+ operationId: operation.sys.id,
233
+ spaceId,
234
+ environmentId,
235
+ summary: operation.summary,
236
+ componentTypes: countByAction(componentItems),
237
+ designTokens: countByAction(tokenItems),
238
+ failures: items
239
+ .filter((item) => item.status === 'failed')
240
+ .map((item) => ({
241
+ entityType: item.entityType,
242
+ entityId: item.id,
243
+ error: item.error,
244
+ })),
245
+ };
246
+ }
247
+ // --- Selection helpers ---
248
+ function getSelectableEntities(preview) {
249
+ const entities = [];
250
+ for (const token of preview.tokens.new) {
251
+ entities.push({
252
+ id: token.path ?? token.id ?? '',
253
+ kind: 'token',
254
+ status: 'new',
255
+ });
256
+ }
257
+ for (const item of preview.tokens.changed) {
258
+ entities.push({ id: item.current.id, kind: 'token', status: 'changed' });
259
+ }
260
+ for (const comp of preview.components.new) {
261
+ entities.push({
262
+ id: comp.key ?? comp.id ?? '',
263
+ kind: 'component',
264
+ status: 'new',
265
+ });
266
+ }
267
+ for (const item of preview.components.changed) {
268
+ entities.push({
269
+ id: item.current.id,
270
+ kind: 'component',
271
+ status: 'changed',
272
+ isBreaking: item.changeClassification?.classification === 'breaking',
273
+ });
274
+ }
275
+ return entities;
276
+ }
277
+ function resolveNonInteractiveSelection(entities, opts) {
278
+ const allKeys = new Set(entities.map((e) => makeSelectKey(e.kind, e.id)));
279
+ if (opts.selectAll) {
280
+ if ((opts.select ?? []).length > 0 || (opts.deselect ?? []).length > 0) {
281
+ process.stderr.write('Warning: --select-all overrides --select and --deselect\n');
282
+ }
283
+ return allKeys;
284
+ }
285
+ const hasSelectPatterns = (opts.select ?? []).length > 0;
286
+ const selected = new Set();
287
+ if (!hasSelectPatterns) {
288
+ for (const key of allKeys)
289
+ selected.add(key);
290
+ }
291
+ else {
292
+ for (const pattern of opts.select ?? []) {
293
+ for (const key of allKeys) {
294
+ if (key.includes(pattern))
295
+ selected.add(key);
296
+ }
297
+ }
298
+ }
299
+ for (const pattern of opts.deselect ?? []) {
300
+ for (const key of [...selected]) {
301
+ if (key.includes(pattern))
302
+ selected.delete(key);
303
+ }
304
+ }
305
+ return selected;
306
+ }
307
+ function SelectApp({ entities, spaceId, environmentId, onApply }) {
308
+ const allKeys = new Set(entities.map((e) => makeSelectKey(e.kind, e.id)));
309
+ const [selectedIndex, setSelectedIndex] = useState(0);
310
+ const [selected, setSelected] = useState(() => new Set(allKeys));
311
+ const [importing, setImporting] = useState(false);
312
+ useInput((input, key) => {
313
+ if (importing)
314
+ return;
315
+ if (key.upArrow) {
316
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
317
+ return;
318
+ }
319
+ if (key.downArrow) {
320
+ setSelectedIndex((prev) => Math.min(entities.length - 1, prev + 1));
321
+ return;
322
+ }
323
+ if (input === ' ') {
324
+ const entity = entities[selectedIndex];
325
+ if (!entity)
326
+ return;
327
+ const k = makeSelectKey(entity.kind, entity.id);
328
+ setSelected((prev) => {
329
+ const next = new Set(prev);
330
+ if (next.has(k))
331
+ next.delete(k);
332
+ else
333
+ next.add(k);
334
+ return next;
335
+ });
336
+ return;
337
+ }
338
+ if (input === 'a' || input === 'A') {
339
+ setSelected(new Set(allKeys));
340
+ return;
341
+ }
342
+ if (input === 'n' || input === 'N') {
343
+ setSelected(new Set());
344
+ return;
345
+ }
346
+ if ((input === 'i' || input === 'I') && selected.size > 0) {
347
+ setImporting(true);
348
+ onApply(selected);
349
+ return;
350
+ }
351
+ if (input === 'q' || input === 'Q') {
352
+ process.exit(0);
353
+ }
354
+ });
355
+ return createElement(SelectView, {
356
+ entities,
357
+ spaceId,
358
+ environmentId,
359
+ selectedIndex,
360
+ selected,
361
+ importing,
362
+ });
363
+ }
364
+ // --- Command registration ---
365
+ function collect(val, prev) {
366
+ return [...prev, val];
367
+ }
368
+ export function registerApplyCommand(program) {
369
+ const applyCmd = program
370
+ .command('apply')
371
+ .description('Preview, select, or push design system entities to Contentful ExO');
372
+ // --- apply preview ---
373
+ applyCmd
374
+ .command('preview')
375
+ .description('Show a read-only diff of what apply push would do')
376
+ .option('--components <path>', 'Path to components.json (CDF)')
377
+ .option('--tokens <path>', 'Path to tokens.json (DTCG)')
378
+ .option('--session <id>', 'Pipeline session ID to load generated components from')
379
+ .requiredOption('--space-id <id>', 'Contentful space ID')
380
+ .requiredOption('--environment-id <id>', 'Contentful environment ID')
381
+ .option('--cma-token <token>', 'CMA personal access token (or set CONTENTFUL_MANAGEMENT_TOKEN)')
382
+ .option('--host <url>', 'Override API base URL')
383
+ .action(async (opts) => {
384
+ let inputs;
385
+ try {
386
+ inputs = await resolveSharedInputs(opts);
387
+ }
388
+ catch (e) {
389
+ if (e instanceof ApiError)
390
+ die(`Error: ${e.message}`);
391
+ throw e;
392
+ }
393
+ const { components, tokens, client } = inputs;
394
+ try {
395
+ const orgId = await client.resolveOrganizationId();
396
+ client.setOrganizationId(orgId);
397
+ await client.validateEnvironment();
398
+ }
399
+ catch (e) {
400
+ if (e instanceof ApiError)
401
+ die(`Error: ${e.message}`);
402
+ const cause = e instanceof Error && e.cause instanceof Error ? e.cause.message : '';
403
+ die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
404
+ }
405
+ const manifest = buildManifest(components, tokens);
406
+ let preview;
407
+ try {
408
+ preview = await client.previewImport(manifest);
409
+ }
410
+ catch (e) {
411
+ if (e instanceof ApiError)
412
+ die(`Error: ${e.message}`);
413
+ throw e;
414
+ }
415
+ const spaceId = opts.spaceId;
416
+ const environmentId = opts.environmentId;
417
+ if (process.stdout.isTTY) {
418
+ const { waitUntilExit } = render(createElement(ServerPreviewApp, {
419
+ preview,
420
+ spaceId,
421
+ environmentId,
422
+ }));
423
+ await waitUntilExit();
424
+ }
425
+ else {
426
+ process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
427
+ process.exit(0);
428
+ }
429
+ });
430
+ // --- apply push ---
431
+ applyCmd
432
+ .command('push')
433
+ .description('Write component types and design tokens to Contentful ExO')
434
+ .option('--components <path>', 'Path to components.json (CDF)')
435
+ .option('--tokens <path>', 'Path to tokens.json (DTCG)')
436
+ .option('--session <id>', 'Pipeline session ID to load generated components from')
437
+ .requiredOption('--space-id <id>', 'Contentful space ID')
438
+ .requiredOption('--environment-id <id>', 'Contentful environment ID')
439
+ .option('--cma-token <token>', 'CMA personal access token (or set CONTENTFUL_MANAGEMENT_TOKEN)')
440
+ .option('--host <url>', 'Override API base URL')
441
+ .option('--yes', 'Skip interactive confirmation')
442
+ .option('--verbose', 'Show all entity progress including skipped/unchanged')
443
+ .option('--force', 'Skip confirmation for breaking changes (for CI)')
444
+ .option('--dry-run', 'Run preview only without applying')
445
+ .action(async (opts) => {
446
+ const isTTY = process.stdout.isTTY;
447
+ if (!isTTY && !opts.yes) {
448
+ process.stderr.write('Error: apply push requires --yes in non-interactive mode\n');
449
+ process.exit(1);
450
+ }
451
+ let inputs;
452
+ try {
453
+ inputs = await resolveSharedInputs(opts);
454
+ }
455
+ catch (e) {
456
+ if (e instanceof ApiError)
457
+ die(`Error: ${e.message}`);
458
+ throw e;
459
+ }
460
+ const { components, tokens, client } = inputs;
461
+ try {
462
+ const orgId = await client.resolveOrganizationId();
463
+ client.setOrganizationId(orgId);
464
+ await client.validateEnvironment();
465
+ }
466
+ catch (e) {
467
+ if (e instanceof ApiError)
468
+ die(`Error: ${e.message}`);
469
+ throw e;
470
+ }
471
+ const manifest = buildManifest(components, tokens);
472
+ let preview;
473
+ try {
474
+ preview = await client.previewImport(manifest);
475
+ }
476
+ catch (e) {
477
+ if (e instanceof ApiError)
478
+ die(`Error: ${e.message}`);
479
+ throw e;
480
+ }
481
+ const spaceId = opts.spaceId;
482
+ const environmentId = opts.environmentId;
483
+ // --- Dry run: print preview and exit ---
484
+ if (opts.dryRun) {
485
+ if (isTTY) {
486
+ const { waitUntilExit } = render(createElement(ServerPreviewApp, {
487
+ preview,
488
+ spaceId,
489
+ environmentId,
490
+ }));
491
+ await waitUntilExit();
492
+ }
493
+ else {
494
+ process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
495
+ }
496
+ process.exit(0);
497
+ }
498
+ // --- Nothing to do: exit early without calling apply ---
499
+ if (isEmptyPreview(preview)) {
500
+ if (isTTY && !opts.yes) {
501
+ process.stderr.write('Nothing to change — design system is up to date.\n');
502
+ }
503
+ else {
504
+ process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
505
+ }
506
+ process.exit(0);
507
+ }
508
+ const breakingWithImpact = hasBreakingChangesWithImpact(preview);
509
+ // --- Non-interactive: require --force for breaking changes ---
510
+ if (!isTTY || opts.yes) {
511
+ if (breakingWithImpact && !opts.force) {
512
+ process.stderr.write('Error: breaking changes with downstream impact detected. Use --force to acknowledge.\n');
513
+ process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
514
+ process.exit(1);
515
+ }
516
+ const verbose = opts.verbose ?? false;
517
+ if (verbose) {
518
+ process.stderr.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
519
+ }
520
+ let operation;
521
+ try {
522
+ operation = await client.applyImport(manifest, breakingWithImpact || opts.force === true);
523
+ }
524
+ catch (e) {
525
+ if (e instanceof ApiError)
526
+ die(`Error: ${e.message}`);
527
+ throw e;
528
+ }
529
+ process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
530
+ try {
531
+ operation = await client.pollOperation(operation.sys.id);
532
+ }
533
+ catch (e) {
534
+ if (e instanceof ApiError)
535
+ die(`Error: ${e.message}`);
536
+ throw e;
537
+ }
538
+ const summary = buildApplyOutput(operation, spaceId, environmentId);
539
+ process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
540
+ process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
541
+ return;
542
+ }
543
+ // --- Interactive (TTY, no --yes) flow ---
544
+ await new Promise((resolvePromise) => {
545
+ const runApply = async (acknowledge) => {
546
+ instance.rerender(createElement(ServerApplyProgress, {
547
+ spaceId,
548
+ environmentId,
549
+ status: 'applying',
550
+ }));
551
+ let operation;
552
+ try {
553
+ operation = await client.applyImport(manifest, acknowledge);
554
+ }
555
+ catch (e) {
556
+ if (e instanceof ApiError) {
557
+ instance.rerender(createElement(ServerApplyProgress, {
558
+ spaceId,
559
+ environmentId,
560
+ status: 'error',
561
+ error: e.message,
562
+ }));
563
+ return;
564
+ }
565
+ throw e;
566
+ }
567
+ instance.rerender(createElement(ServerApplyProgress, {
568
+ spaceId,
569
+ environmentId,
570
+ status: 'polling',
571
+ operationId: operation.sys.id,
572
+ }));
573
+ try {
574
+ operation = await client.pollOperation(operation.sys.id);
575
+ }
576
+ catch (e) {
577
+ if (e instanceof ApiError) {
578
+ instance.rerender(createElement(ServerApplyProgress, {
579
+ spaceId,
580
+ environmentId,
581
+ status: 'error',
582
+ error: e.message,
583
+ }));
584
+ return;
585
+ }
586
+ throw e;
587
+ }
588
+ instance.rerender(createElement(ServerApplyDone, {
589
+ operation,
590
+ spaceId,
591
+ environmentId,
592
+ }));
593
+ resolvePromise();
594
+ };
595
+ const instance = render(createElement(ServerPreviewConfirm, {
596
+ preview,
597
+ spaceId,
598
+ environmentId,
599
+ breakingWithImpact,
600
+ onConfirm: (acknowledge) => {
601
+ void runApply(acknowledge);
602
+ },
603
+ onCancel: () => {
604
+ process.exit(0);
605
+ },
606
+ }));
607
+ void instance.waitUntilExit().then(() => resolvePromise());
608
+ });
609
+ });
610
+ // --- apply select ---
611
+ applyCmd
612
+ .command('select')
613
+ .description('Select a subset of entities and push to Contentful ExO')
614
+ .option('--components <path>', 'Path to components.json (CDF)')
615
+ .option('--tokens <path>', 'Path to tokens.json (DTCG)')
616
+ .option('--session <id>', 'Pipeline session ID to load generated components from')
617
+ .requiredOption('--space-id <id>', 'Contentful space ID')
618
+ .requiredOption('--environment-id <id>', 'Contentful environment ID')
619
+ .option('--cma-token <token>', 'CMA personal access token (or set CONTENTFUL_MANAGEMENT_TOKEN)')
620
+ .option('--host <url>', 'Override API base URL')
621
+ .option('--select-all', 'Select all entities without launching TUI')
622
+ .option('--select <pattern>', 'Select entities by ID pattern (repeatable)', collect, [])
623
+ .option('--deselect <pattern>', 'Deselect entities by ID pattern (repeatable)', collect, [])
624
+ .option('--force', 'Skip confirmation for breaking changes')
625
+ .action(async (opts) => {
626
+ const nonInteractive = opts.selectAll || (opts.select ?? []).length > 0 || (opts.deselect ?? []).length > 0;
627
+ if (!nonInteractive && !process.stdout.isTTY) {
628
+ die('Error: apply select requires an interactive terminal unless --select-all, --select, or --deselect is provided');
629
+ }
630
+ let inputs;
631
+ try {
632
+ inputs = await resolveSharedInputs(opts);
633
+ }
634
+ catch (e) {
635
+ if (e instanceof ApiError)
636
+ die(`Error: ${e.message}`);
637
+ throw e;
638
+ }
639
+ const { components, tokens, client } = inputs;
640
+ try {
641
+ const orgId = await client.resolveOrganizationId();
642
+ client.setOrganizationId(orgId);
643
+ await client.validateEnvironment();
644
+ }
645
+ catch (e) {
646
+ if (e instanceof ApiError)
647
+ die(`Error: ${e.message}`);
648
+ throw e;
649
+ }
650
+ const fullManifest = buildManifest(components, tokens);
651
+ let preview;
652
+ try {
653
+ preview = await client.previewImport(fullManifest);
654
+ }
655
+ catch (e) {
656
+ if (e instanceof ApiError)
657
+ die(`Error: ${e.message}`);
658
+ throw e;
659
+ }
660
+ const spaceId = opts.spaceId;
661
+ const environmentId = opts.environmentId;
662
+ const entities = getSelectableEntities(preview);
663
+ if (entities.length === 0) {
664
+ process.stderr.write('Nothing to change — design system is up to date.\n');
665
+ process.exit(0);
666
+ }
667
+ if (nonInteractive) {
668
+ const selectedKeys = resolveNonInteractiveSelection(entities, opts);
669
+ if (selectedKeys.size === 0) {
670
+ process.stderr.write('No entities matched selection criteria.\n');
671
+ process.exit(0);
672
+ }
673
+ const selectedComponentKeys = new Set();
674
+ const selectedTokenPaths = new Set();
675
+ for (const key of selectedKeys) {
676
+ const [kind, ...idParts] = key.split(':');
677
+ const id = idParts.join(':');
678
+ if (kind === 'component')
679
+ selectedComponentKeys.add(id);
680
+ else if (kind === 'token')
681
+ selectedTokenPaths.add(id);
682
+ }
683
+ const filteredManifest = buildFilteredManifest(fullManifest, selectedComponentKeys, selectedTokenPaths);
684
+ const hasBreaking = entities.some((e) => e.isBreaking && selectedKeys.has(makeSelectKey(e.kind, e.id)));
685
+ if (hasBreaking && !opts.force) {
686
+ process.stderr.write('Error: selection includes breaking changes. Use --force to acknowledge.\n');
687
+ process.exit(1);
688
+ }
689
+ let operation;
690
+ try {
691
+ operation = await client.applyImport(filteredManifest, hasBreaking || opts.force === true);
692
+ }
693
+ catch (e) {
694
+ if (e instanceof ApiError)
695
+ die(`Error: ${e.message}`);
696
+ throw e;
697
+ }
698
+ process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
699
+ try {
700
+ operation = await client.pollOperation(operation.sys.id);
701
+ }
702
+ catch (e) {
703
+ if (e instanceof ApiError)
704
+ die(`Error: ${e.message}`);
705
+ throw e;
706
+ }
707
+ const summary = buildApplyOutput(operation, spaceId, environmentId);
708
+ process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
709
+ process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
710
+ return;
711
+ }
712
+ // --- Interactive flow ---
713
+ await new Promise((resolvePromise) => {
714
+ const runSelectApply = async (selectedKeys) => {
715
+ const selectedComponentKeys = new Set();
716
+ const selectedTokenPaths = new Set();
717
+ for (const key of selectedKeys) {
718
+ const [kind, ...idParts] = key.split(':');
719
+ const id = idParts.join(':');
720
+ if (kind === 'component')
721
+ selectedComponentKeys.add(id);
722
+ else if (kind === 'token')
723
+ selectedTokenPaths.add(id);
724
+ }
725
+ const filteredManifest = buildFilteredManifest(fullManifest, selectedComponentKeys, selectedTokenPaths);
726
+ const hasBreaking = entities.some((e) => e.isBreaking && selectedKeys.has(makeSelectKey(e.kind, e.id)));
727
+ instance.rerender(createElement(ServerApplyProgress, {
728
+ spaceId,
729
+ environmentId,
730
+ status: 'applying',
731
+ }));
732
+ let operation;
733
+ try {
734
+ operation = await client.applyImport(filteredManifest, hasBreaking);
735
+ }
736
+ catch (e) {
737
+ if (e instanceof ApiError) {
738
+ instance.rerender(createElement(ServerApplyProgress, {
739
+ spaceId,
740
+ environmentId,
741
+ status: 'error',
742
+ error: e.message,
743
+ }));
744
+ return;
745
+ }
746
+ throw e;
747
+ }
748
+ instance.rerender(createElement(ServerApplyProgress, {
749
+ spaceId,
750
+ environmentId,
751
+ status: 'polling',
752
+ operationId: operation.sys.id,
753
+ }));
754
+ try {
755
+ operation = await client.pollOperation(operation.sys.id);
756
+ }
757
+ catch (e) {
758
+ if (e instanceof ApiError) {
759
+ instance.rerender(createElement(ServerApplyProgress, {
760
+ spaceId,
761
+ environmentId,
762
+ status: 'error',
763
+ error: e.message,
764
+ }));
765
+ return;
766
+ }
767
+ throw e;
768
+ }
769
+ instance.rerender(createElement(ServerApplyDone, {
770
+ operation,
771
+ spaceId,
772
+ environmentId,
773
+ }));
774
+ resolvePromise();
775
+ };
776
+ const instance = render(createElement(SelectApp, {
777
+ entities,
778
+ spaceId,
779
+ environmentId,
780
+ onApply: (selectedKeys) => {
781
+ void runSelectApply(selectedKeys);
782
+ },
783
+ }));
784
+ void instance.waitUntilExit().then(() => resolvePromise());
785
+ });
786
+ });
787
+ }