@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,199 @@
1
+ import { createElement } from 'react';
2
+ import { render } from 'ink';
3
+ import { access, mkdir, stat, writeFile } from 'node:fs/promises';
4
+ import { basename, resolve } from 'node:path';
5
+ import { openPipelineDb, loadCDFComponents, loadDTCGTokens, findLatestSessionForCommand } from '../session/db.js';
6
+ import { validateCDFFile } from './validate/validators/cdf-validator.js';
7
+ import { validateDTCGTokenFile } from './validate/validators/dtcg-validator.js';
8
+ import { formatDiagnostics } from './validate/validators/format-errors.js';
9
+ import { ValidateView } from './validate/tui/ValidateView.js';
10
+ function die(message) {
11
+ process.stderr.write(`${message}\n`);
12
+ process.exit(1);
13
+ }
14
+ async function pathExists(p) {
15
+ return access(p)
16
+ .then(() => true)
17
+ .catch(() => false);
18
+ }
19
+ async function assertOutIsNotDirectory(outPath) {
20
+ if (await pathExists(outPath)) {
21
+ const s = await stat(outPath);
22
+ if (s.isDirectory())
23
+ die(`Error: --out must be a file path, not a directory: ${outPath}`);
24
+ }
25
+ }
26
+ function resolveSession(sessionFlag, command) {
27
+ const db = openPipelineDb();
28
+ try {
29
+ const sessionId = sessionFlag ?? findLatestSessionForCommand(db, command);
30
+ if (!sessionId) {
31
+ const hint = command === 'generate components' ? 'generate components' : 'generate tokens';
32
+ die(`Error: no completed ${hint} session found. Run ${hint} first, or pass --session <id>.`);
33
+ }
34
+ return sessionId;
35
+ }
36
+ finally {
37
+ db.close();
38
+ }
39
+ }
40
+ function rebuildDTCGTree(groups, tokens) {
41
+ const root = {};
42
+ // Apply group descriptions
43
+ for (const group of groups) {
44
+ const segments = group.path.split('.');
45
+ let node = root;
46
+ for (const seg of segments) {
47
+ if (typeof node[seg] !== 'object' || node[seg] === null) {
48
+ node[seg] = {};
49
+ }
50
+ node = node[seg];
51
+ }
52
+ if (group.$description) {
53
+ node['$description'] = group.$description;
54
+ }
55
+ }
56
+ // Place leaf tokens
57
+ for (const token of tokens) {
58
+ const segments = token.path.split('.');
59
+ const leafKey = segments[segments.length - 1];
60
+ const parentSegments = segments.slice(0, -1);
61
+ let node = root;
62
+ for (const seg of parentSegments) {
63
+ if (typeof node[seg] !== 'object' || node[seg] === null) {
64
+ node[seg] = {};
65
+ }
66
+ node = node[seg];
67
+ }
68
+ const leaf = { $type: token.$type, $value: token.$value };
69
+ if (token.$description)
70
+ leaf['$description'] = token.$description;
71
+ node[leafKey] = leaf;
72
+ }
73
+ return root;
74
+ }
75
+ export function registerPrintCommand(program) {
76
+ const print = program.command('print').description('Write pipeline artifacts to JSON files or validate them');
77
+ // print components
78
+ print
79
+ .command('components')
80
+ .description('Write generated components to a CDF JSON file')
81
+ .option('--session <id>', 'Session ID to print from (default: most recent generate components session)')
82
+ .option('--out <path>', 'Output file path', 'components.json')
83
+ .action(async (opts) => {
84
+ const outPath = resolve(opts.out);
85
+ await assertOutIsNotDirectory(outPath);
86
+ const sessionId = resolveSession(opts.session, 'generate components');
87
+ const db = openPipelineDb();
88
+ let components;
89
+ let generateStepStatus = null;
90
+ try {
91
+ components = loadCDFComponents(db, sessionId);
92
+ const stepRow = db
93
+ .prepare(`SELECT status FROM steps WHERE session_id = ? AND command = 'generate components' ORDER BY id DESC LIMIT 1`)
94
+ .get(sessionId);
95
+ generateStepStatus = stepRow?.status ?? null;
96
+ }
97
+ finally {
98
+ db.close();
99
+ }
100
+ if (components.length === 0) {
101
+ die(`Error: no generated components in session '${sessionId}'. Run generate components first.`);
102
+ }
103
+ if (generateStepStatus === 'failed') {
104
+ process.stderr.write(`Warning: session '${sessionId}' generate step failed — output may be incomplete (${components.length} components found)\n`);
105
+ }
106
+ const cdfObj = { $schema: 'https://contentful.com/schemas/cdf/v1' };
107
+ const missingDescription = [];
108
+ for (const { key, entry } of components) {
109
+ cdfObj[key] = entry;
110
+ if (!entry.$description)
111
+ missingDescription.push(key);
112
+ }
113
+ if (missingDescription.length > 0) {
114
+ process.stderr.write(`Warning: ${missingDescription.length} component${missingDescription.length === 1 ? '' : 's'} missing $description (will fail at apply push): ${missingDescription.join(', ')}\n`);
115
+ }
116
+ await mkdir(resolve(outPath, '..'), { recursive: true });
117
+ await writeFile(outPath, `${JSON.stringify(cdfObj, null, 2)}\n`);
118
+ process.stdout.write(`wrote ${basename(outPath)} (${components.length} component${components.length === 1 ? '' : 's'})\n`);
119
+ });
120
+ // print tokens
121
+ print
122
+ .command('tokens')
123
+ .description('Write generated tokens to a DTCG JSON file')
124
+ .option('--session <id>', 'Session ID to print from (default: most recent generate tokens session)')
125
+ .option('--out <path>', 'Output file path', 'tokens.json')
126
+ .action(async (opts) => {
127
+ const outPath = resolve(opts.out);
128
+ await assertOutIsNotDirectory(outPath);
129
+ const sessionId = resolveSession(opts.session, 'generate tokens');
130
+ const db = openPipelineDb();
131
+ let result;
132
+ try {
133
+ result = loadDTCGTokens(db, sessionId);
134
+ }
135
+ finally {
136
+ db.close();
137
+ }
138
+ if (result.tokens.length === 0) {
139
+ die(`Error: no generated tokens in session '${sessionId}'. Run generate tokens first.`);
140
+ }
141
+ const tree = rebuildDTCGTree(result.groups, result.tokens);
142
+ await mkdir(resolve(outPath, '..'), { recursive: true });
143
+ await writeFile(outPath, `${JSON.stringify(tree, null, 2)}\n`);
144
+ process.stdout.write(`wrote ${basename(outPath)} (${result.tokens.length} token${result.tokens.length === 1 ? '' : 's'})\n`);
145
+ });
146
+ // print validate
147
+ print
148
+ .command('validate')
149
+ .description('Validate CDF or DTCG files against their schemas')
150
+ .option('--components <path>', 'Path to CDF components file')
151
+ .option('--tokens <path>', 'Path to DTCG tokens file')
152
+ .action(async (opts) => {
153
+ if (!opts.components && !opts.tokens) {
154
+ process.stderr.write('Error: at least one of --components or --tokens is required.\n\nUsage: print validate [--components <path>] [--tokens <path>]\n');
155
+ process.exit(1);
156
+ }
157
+ const viewResults = [];
158
+ if (opts.components) {
159
+ const r = await validateCDFFile(opts.components);
160
+ viewResults.push({
161
+ filePath: opts.components,
162
+ format: 'CDF v1',
163
+ valid: r.valid,
164
+ summary: r.summary,
165
+ diagnostics: r.diagnostics,
166
+ });
167
+ }
168
+ if (opts.tokens) {
169
+ const r = await validateDTCGTokenFile(opts.tokens);
170
+ viewResults.push({
171
+ filePath: opts.tokens,
172
+ format: 'DTCG',
173
+ valid: r.valid,
174
+ summary: r.summary,
175
+ diagnostics: r.diagnostics,
176
+ });
177
+ }
178
+ const failed = viewResults.some((r) => !r.valid);
179
+ const exitCode = failed ? 1 : 0;
180
+ if (process.stdout.isTTY) {
181
+ const { waitUntilExit } = render(createElement(ValidateView, {
182
+ results: viewResults,
183
+ onExit: () => process.exit(exitCode),
184
+ }));
185
+ await waitUntilExit();
186
+ }
187
+ else {
188
+ const output = viewResults
189
+ .map((r) => formatDiagnostics({
190
+ valid: r.valid,
191
+ summary: r.summary ?? r.filePath,
192
+ diagnostics: r.diagnostics,
193
+ }))
194
+ .join('\n\n');
195
+ process.stdout.write(output + '\n');
196
+ process.exit(exitCode);
197
+ }
198
+ });
199
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import type { ValidationDiagnostic } from '../validators/format-errors.js';
3
+ export type ValidateViewEntry = {
4
+ filePath: string;
5
+ format: 'CDF v1' | 'DTCG';
6
+ valid: boolean;
7
+ summary?: string;
8
+ diagnostics: ValidationDiagnostic[];
9
+ };
10
+ type ValidateViewProps = {
11
+ results: ValidateViewEntry[];
12
+ onExit: () => void;
13
+ };
14
+ export declare function ValidateView({ results, onExit }: ValidateViewProps): React.ReactElement;
15
+ export {};
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { TopBar } from '../../../analyze/select/tui/components/TopBar.js';
5
+ import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
6
+ export function ValidateView({ results, onExit }) {
7
+ const [scrollOffset, setScrollOffset] = useState(0);
8
+ const allValid = results.every((r) => r.valid);
9
+ useImmediateInput((input, key) => {
10
+ if (input === 'q' || key.return) {
11
+ onExit();
12
+ return;
13
+ }
14
+ if (key.upArrow || input === 'k') {
15
+ setScrollOffset((o) => Math.max(0, o - 1));
16
+ }
17
+ else if (key.downArrow || input === 'j') {
18
+ setScrollOffset((o) => o + 1);
19
+ }
20
+ else if (input === 'g') {
21
+ setScrollOffset(0);
22
+ }
23
+ else if (input === 'G') {
24
+ setScrollOffset(100);
25
+ }
26
+ });
27
+ const invalidResults = results.filter((r) => !r.valid);
28
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TopBar, { subcommand: "validate", hints: [
29
+ { key: '?', label: 'help' },
30
+ { key: 'q', label: 'quit' },
31
+ ] }), _jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [results.map((r) => {
32
+ const summaryText = r.summary || (r.valid ? 'valid' : (r.diagnostics[0]?.message ?? 'invalid'));
33
+ return (_jsxs(Box, { children: [_jsx(Text, { color: r.valid ? 'green' : 'red', children: r.valid ? '✓' : '✗' }), _jsx(Text, { children: ' ' + r.filePath.split('/').pop() }), _jsx(Text, { dimColor: true, children: ' ' + r.format + ' ' }), _jsx(Text, { color: r.valid ? 'green' : 'red', children: summaryText })] }, r.filePath));
34
+ }), invalidResults.length > 0 && (_jsx(_Fragment, { children: invalidResults.map((r) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { bold: true, children: r.filePath.split('/').pop() + ' errors' }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { children: " " }), r.diagnostics.slice(scrollOffset).map((d, i) => (_jsx(Box, { children: _jsx(Text, { color: "red", children: ' ✗ ' + d.path + ': ' + d.message }) }, i)))] }, r.filePath))) }))] }), _jsxs(Box, { borderStyle: "single", paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: allValid
35
+ ? 'All files valid'
36
+ : invalidResults.length + ' file' + (invalidResults.length === 1 ? '' : 's') + ' invalid' }), _jsx(Text, { dimColor: true, children: allValid ? '[q]' : 'scroll ↑↓ [q]' })] })] }));
37
+ }
@@ -0,0 +1,2 @@
1
+ import type { ValidationResult } from './format-errors.js';
2
+ export declare function validateCDFFile(filePath: string): Promise<ValidationResult>;
@@ -0,0 +1,104 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { validateCDF } from '@contentful/experience-design-system-types';
3
+ function extractValue(input, path) {
4
+ if (path === '/')
5
+ return undefined;
6
+ const parts = path.split('/').filter(Boolean);
7
+ let current = input;
8
+ for (const part of parts) {
9
+ if (typeof current !== 'object' || current === null)
10
+ return undefined;
11
+ current = current[part];
12
+ }
13
+ if (current === undefined)
14
+ return undefined;
15
+ return typeof current === 'string' ? current : JSON.stringify(current);
16
+ }
17
+ function inferParamsShape(expected) {
18
+ if (!expected)
19
+ return null;
20
+ try {
21
+ return JSON.parse(expected);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ function rewriteDiagnostic(error, input) {
28
+ const params = inferParamsShape(error.expected);
29
+ if (!params) {
30
+ return { ...error };
31
+ }
32
+ let { path } = error;
33
+ let message = error.message;
34
+ let expected = error.expected;
35
+ let actual = error.actual;
36
+ if ('missingProperty' in params) {
37
+ const prop = params.missingProperty;
38
+ path = `${path}/${prop}`;
39
+ message = `Missing required property "${prop}"`;
40
+ expected = undefined;
41
+ actual = undefined;
42
+ }
43
+ else if ('allowedValues' in params) {
44
+ const values = params.allowedValues;
45
+ const fieldName = path.split('/').pop() ?? path;
46
+ message = `Invalid ${fieldName.replace(/^\$/, '')}`;
47
+ expected = values.join(', ');
48
+ actual = extractValue(input, path);
49
+ }
50
+ else if ('allowedValue' in params) {
51
+ message = 'Invalid value';
52
+ expected = String(params.allowedValue);
53
+ actual = extractValue(input, path);
54
+ }
55
+ else {
56
+ expected = JSON.stringify(params);
57
+ }
58
+ return {
59
+ path,
60
+ message,
61
+ ...(expected !== undefined && { expected }),
62
+ ...(actual !== undefined && { actual }),
63
+ };
64
+ }
65
+ export async function validateCDFFile(filePath) {
66
+ let content;
67
+ try {
68
+ content = await readFile(filePath, 'utf-8');
69
+ }
70
+ catch (err) {
71
+ return {
72
+ valid: false,
73
+ summary: '',
74
+ diagnostics: [{ path: filePath, message: err.message }],
75
+ };
76
+ }
77
+ let parsed;
78
+ try {
79
+ parsed = JSON.parse(content);
80
+ }
81
+ catch (err) {
82
+ return {
83
+ valid: false,
84
+ summary: '',
85
+ diagnostics: [{ path: filePath, message: `Invalid JSON: ${err.message}` }],
86
+ };
87
+ }
88
+ const cdfResult = validateCDF(parsed);
89
+ if (!cdfResult.valid) {
90
+ const diagnostics = cdfResult.errors
91
+ .filter((e) => {
92
+ const params = inferParamsShape(e.expected);
93
+ return !params || !('passingSchemas' in params);
94
+ })
95
+ .map((e) => rewriteDiagnostic(e, parsed));
96
+ return { valid: false, summary: '', diagnostics };
97
+ }
98
+ const count = cdfResult.components.length;
99
+ return {
100
+ valid: true,
101
+ summary: `Valid CDF v1 — ${count} component${count === 1 ? '' : 's'} found`,
102
+ diagnostics: [],
103
+ };
104
+ }
@@ -0,0 +1,2 @@
1
+ import type { ValidationResult } from './format-errors.js';
2
+ export declare function validateDTCGTokenFile(filePath: string): Promise<ValidationResult>;
@@ -0,0 +1,110 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { DESIGN_TOKEN_TYPES } from '@contentful/experience-design-system-types';
3
+ /*
4
+ * Strict DTCG subset validation.
5
+ *
6
+ * This validator is intentionally stricter than both the W3C DTCG spec and the
7
+ * TypeScript types in @contentful/experience-design-system-types:
8
+ *
9
+ * - No group-level type inheritance. The DTCG spec allows $type on a group to
10
+ * be inherited by descendants. This validator requires explicit $type on
11
+ * every leaf token.
12
+ *
13
+ * - No string values at non-$ keys. DTCGTokenGroupNode allows string | undefined
14
+ * at arbitrary keys, but this validator rejects non-object values at non-$
15
+ * positions.
16
+ *
17
+ * Both restrictions exist because agent-generated files should be maximally
18
+ * explicit and self-describing.
19
+ */
20
+ function walkDTCG(obj, path, diagnostics, counts) {
21
+ for (const [key, value] of Object.entries(obj)) {
22
+ if (key.startsWith('$'))
23
+ continue;
24
+ const currentPath = `${path}/${key}`;
25
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
26
+ diagnostics.push({
27
+ path: currentPath,
28
+ message: `Expected object, got ${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value}`,
29
+ });
30
+ continue;
31
+ }
32
+ const node = value;
33
+ const hasValue = '$value' in node;
34
+ const hasType = '$type' in node;
35
+ if (hasValue) {
36
+ counts.tokens++;
37
+ if (!hasType) {
38
+ diagnostics.push({
39
+ path: currentPath,
40
+ message: 'Token has "$value" but missing required "$type"',
41
+ });
42
+ }
43
+ else if (typeof node.$type !== 'string' || !DESIGN_TOKEN_TYPES.includes(node.$type)) {
44
+ diagnostics.push({
45
+ path: `${currentPath}/$type`,
46
+ message: 'Invalid design token type',
47
+ expected: DESIGN_TOKEN_TYPES.join(', '),
48
+ actual: String(node.$type),
49
+ });
50
+ }
51
+ }
52
+ else if (hasType) {
53
+ diagnostics.push({
54
+ path: currentPath,
55
+ message: 'Node has "$type" but missing required "$value" (leaf tokens must have both)',
56
+ });
57
+ }
58
+ else {
59
+ counts.groups++;
60
+ walkDTCG(node, currentPath, diagnostics, counts);
61
+ }
62
+ }
63
+ }
64
+ export async function validateDTCGTokenFile(filePath) {
65
+ let content;
66
+ try {
67
+ content = await readFile(filePath, 'utf-8');
68
+ }
69
+ catch (err) {
70
+ return {
71
+ valid: false,
72
+ summary: '',
73
+ diagnostics: [{ path: filePath, message: err.message }],
74
+ };
75
+ }
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(content);
79
+ }
80
+ catch (err) {
81
+ return {
82
+ valid: false,
83
+ summary: '',
84
+ diagnostics: [{ path: filePath, message: `Invalid JSON: ${err.message}` }],
85
+ };
86
+ }
87
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
88
+ return {
89
+ valid: false,
90
+ summary: '',
91
+ diagnostics: [
92
+ {
93
+ path: '/',
94
+ message: `Expected object, got ${parsed === null ? 'null' : Array.isArray(parsed) ? 'array' : typeof parsed}`,
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ const diagnostics = [];
100
+ const counts = { tokens: 0, groups: 0 };
101
+ walkDTCG(parsed, '', diagnostics, counts);
102
+ if (diagnostics.length > 0) {
103
+ return { valid: false, summary: '', diagnostics };
104
+ }
105
+ return {
106
+ valid: true,
107
+ summary: `Valid DTCG token file — ${counts.tokens} token${counts.tokens === 1 ? '' : 's'} in ${counts.groups} group${counts.groups === 1 ? '' : 's'}`,
108
+ diagnostics: [],
109
+ };
110
+ }
@@ -0,0 +1,12 @@
1
+ export interface ValidationDiagnostic {
2
+ path: string;
3
+ message: string;
4
+ expected?: string;
5
+ actual?: string;
6
+ }
7
+ export interface ValidationResult {
8
+ valid: boolean;
9
+ summary: string;
10
+ diagnostics: ValidationDiagnostic[];
11
+ }
12
+ export declare function formatDiagnostics(result: ValidationResult): string;
@@ -0,0 +1,18 @@
1
+ export function formatDiagnostics(result) {
2
+ if (result.valid) {
3
+ return `✓ ${result.summary}`;
4
+ }
5
+ const count = result.diagnostics.length;
6
+ const header = `✗ ${count} error${count === 1 ? '' : 's'} found`;
7
+ const entries = result.diagnostics.map((d, i) => {
8
+ const lines = [` ${i + 1}. ${d.path}`, ` ${d.message}`];
9
+ if (d.expected !== undefined) {
10
+ lines.push(` expected: ${d.expected}`);
11
+ }
12
+ if (d.actual !== undefined) {
13
+ lines.push(` actual: ${d.actual}`);
14
+ }
15
+ return lines.join('\n');
16
+ });
17
+ return `${header}\n\n${entries.join('\n\n')}`;
18
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createProgram(): Command;
@@ -0,0 +1,25 @@
1
+ import { createRequire } from 'node:module';
2
+ import { Command } from 'commander';
3
+ import { registerAnalyzeCommand } from './analyze/command.js';
4
+ import { registerGenerateCommand } from './generate/command.js';
5
+ import { registerApplyCommand } from './apply/command.js';
6
+ import { registerSessionCommand } from './session/command.js';
7
+ import { registerPrintCommand } from './print/command.js';
8
+ import { registerImportCommand } from './import/command.js';
9
+ import { registerSetupCommand } from './setup/command.js';
10
+ const require = createRequire(import.meta.url);
11
+ const pkg = require('../package.json');
12
+ export function createProgram() {
13
+ const program = new Command()
14
+ .name('experience-design-system-cli')
15
+ .description('Static analysis, validation, generation, and import of Contentful design system artifacts')
16
+ .version(pkg.version, '--version', 'Print version number');
17
+ registerAnalyzeCommand(program);
18
+ registerGenerateCommand(program);
19
+ registerPrintCommand(program);
20
+ registerApplyCommand(program);
21
+ registerSessionCommand(program);
22
+ registerImportCommand(program);
23
+ registerSetupCommand(program);
24
+ return program;
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerSessionCommand(program: Command): void;