@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.
- package/README.md +532 -0
- package/bin/cli.js +58 -0
- package/dist/package.json +56 -0
- package/dist/src/analyze/command.d.ts +3 -0
- package/dist/src/analyze/command.js +175 -0
- package/dist/src/analyze/extract/astro.d.ts +5 -0
- package/dist/src/analyze/extract/astro.js +280 -0
- package/dist/src/analyze/extract/pipeline.d.ts +6 -0
- package/dist/src/analyze/extract/pipeline.js +298 -0
- package/dist/src/analyze/extract/react.d.ts +2 -0
- package/dist/src/analyze/extract/react.js +1949 -0
- package/dist/src/analyze/extract/slot-detection.d.ts +35 -0
- package/dist/src/analyze/extract/slot-detection.js +101 -0
- package/dist/src/analyze/extract/stencil.d.ts +2 -0
- package/dist/src/analyze/extract/stencil.js +293 -0
- package/dist/src/analyze/extract/tsx-shared.d.ts +8 -0
- package/dist/src/analyze/extract/tsx-shared.js +263 -0
- package/dist/src/analyze/extract/vue-tsx.d.ts +2 -0
- package/dist/src/analyze/extract/vue-tsx.js +498 -0
- package/dist/src/analyze/extract/vue.d.ts +5 -0
- package/dist/src/analyze/extract/vue.js +647 -0
- package/dist/src/analyze/extract/web-components.d.ts +2 -0
- package/dist/src/analyze/extract/web-components.js +866 -0
- package/dist/src/analyze/pre-classify.d.ts +17 -0
- package/dist/src/analyze/pre-classify.js +144 -0
- package/dist/src/analyze/select/command.d.ts +2 -0
- package/dist/src/analyze/select/command.js +256 -0
- package/dist/src/analyze/select/index.d.ts +6 -0
- package/dist/src/analyze/select/index.js +5 -0
- package/dist/src/analyze/select/parser.d.ts +6 -0
- package/dist/src/analyze/select/parser.js +53 -0
- package/dist/src/analyze/select/persistence.d.ts +9 -0
- package/dist/src/analyze/select/persistence.js +42 -0
- package/dist/src/analyze/select/stdout.d.ts +7 -0
- package/dist/src/analyze/select/stdout.js +3 -0
- package/dist/src/analyze/select/tui/App.d.ts +8 -0
- package/dist/src/analyze/select/tui/App.js +491 -0
- package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +20 -0
- package/dist/src/analyze/select/tui/components/ComponentDetail.js +43 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.js +531 -0
- package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +10 -0
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +15 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +7 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +11 -0
- package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/JsonEditor.js +154 -0
- package/dist/src/analyze/select/tui/components/JsonPanel.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/JsonPanel.js +62 -0
- package/dist/src/analyze/select/tui/components/PreviewSummaryBar.d.ts +8 -0
- package/dist/src/analyze/select/tui/components/PreviewSummaryBar.js +29 -0
- package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +8 -0
- package/dist/src/analyze/select/tui/components/QuitDialog.js +14 -0
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +15 -0
- package/dist/src/analyze/select/tui/components/Sidebar.js +48 -0
- package/dist/src/analyze/select/tui/components/SourcePanel.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/SourcePanel.js +52 -0
- package/dist/src/analyze/select/tui/components/StatusBar.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/StatusBar.js +6 -0
- package/dist/src/analyze/select/tui/components/TopBar.d.ts +10 -0
- package/dist/src/analyze/select/tui/components/TopBar.js +5 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +24 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +68 -0
- package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +24 -0
- package/dist/src/analyze/select/tui/hooks/useKeymap.js +67 -0
- package/dist/src/analyze/select/tui/hooks/useSession.d.ts +19 -0
- package/dist/src/analyze/select/tui/hooks/useSession.js +52 -0
- package/dist/src/analyze/select/tui/hooks/useUndo.d.ts +8 -0
- package/dist/src/analyze/select/tui/hooks/useUndo.js +26 -0
- package/dist/src/analyze/select/types.d.ts +46 -0
- package/dist/src/analyze/select/types.js +20 -0
- package/dist/src/analyze/select-agent/command.d.ts +2 -0
- package/dist/src/analyze/select-agent/command.js +208 -0
- package/dist/src/analyze/tui/AnalyzeView.d.ts +24 -0
- package/dist/src/analyze/tui/AnalyzeView.js +38 -0
- package/dist/src/apply/api-client.d.ts +35 -0
- package/dist/src/apply/api-client.js +143 -0
- package/dist/src/apply/command.d.ts +6 -0
- package/dist/src/apply/command.js +787 -0
- package/dist/src/apply/manifest.d.ts +1 -0
- package/dist/src/apply/manifest.js +1 -0
- package/dist/src/apply/tui/SelectView.d.ts +18 -0
- package/dist/src/apply/tui/SelectView.js +34 -0
- package/dist/src/apply/tui/ServerApplyView.d.ts +32 -0
- package/dist/src/apply/tui/ServerApplyView.js +42 -0
- package/dist/src/apply/tui/ServerPreviewView.d.ts +9 -0
- package/dist/src/apply/tui/ServerPreviewView.js +21 -0
- package/dist/src/credentials-store.d.ts +8 -0
- package/dist/src/credentials-store.js +30 -0
- package/dist/src/generate/agent-runner.d.ts +86 -0
- package/dist/src/generate/agent-runner.js +314 -0
- package/dist/src/generate/command.d.ts +2 -0
- package/dist/src/generate/command.js +545 -0
- package/dist/src/generate/edit/command.d.ts +2 -0
- package/dist/src/generate/edit/command.js +126 -0
- package/dist/src/generate/prompt-builder.d.ts +18 -0
- package/dist/src/generate/prompt-builder.js +202 -0
- package/dist/src/generate/tui/GenerateView.d.ts +12 -0
- package/dist/src/generate/tui/GenerateView.js +10 -0
- package/dist/src/import/command.d.ts +2 -0
- package/dist/src/import/command.js +96 -0
- package/dist/src/import/orchestrator.d.ts +37 -0
- package/dist/src/import/orchestrator.js +374 -0
- package/dist/src/import/path-utils.d.ts +15 -0
- package/dist/src/import/path-utils.js +30 -0
- package/dist/src/import/tui/WizardApp.d.ts +10 -0
- package/dist/src/import/tui/WizardApp.js +906 -0
- package/dist/src/import/tui/steps/CredentialsStep.d.ts +15 -0
- package/dist/src/import/tui/steps/CredentialsStep.js +79 -0
- package/dist/src/import/tui/steps/DoneStep.d.ts +20 -0
- package/dist/src/import/tui/steps/DoneStep.js +17 -0
- package/dist/src/import/tui/steps/ErrorStep.d.ts +8 -0
- package/dist/src/import/tui/steps/ErrorStep.js +11 -0
- package/dist/src/import/tui/steps/GateStep.d.ts +14 -0
- package/dist/src/import/tui/steps/GateStep.js +20 -0
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +8 -0
- package/dist/src/import/tui/steps/GenerateReviewStep.js +208 -0
- package/dist/src/import/tui/steps/PathValidationStep.d.ts +10 -0
- package/dist/src/import/tui/steps/PathValidationStep.js +151 -0
- package/dist/src/import/tui/steps/PreviewStep.d.ts +21 -0
- package/dist/src/import/tui/steps/PreviewStep.js +36 -0
- package/dist/src/import/tui/steps/RunningStep.d.ts +10 -0
- package/dist/src/import/tui/steps/RunningStep.js +20 -0
- package/dist/src/import/tui/steps/TokenInputStep.d.ts +8 -0
- package/dist/src/import/tui/steps/TokenInputStep.js +70 -0
- package/dist/src/import/tui/steps/WelcomeStep.d.ts +7 -0
- package/dist/src/import/tui/steps/WelcomeStep.js +33 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +15 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.js +121 -0
- package/dist/src/import/tui/steps/preview-diff.d.ts +10 -0
- package/dist/src/import/tui/steps/preview-diff.js +132 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/output/format.d.ts +23 -0
- package/dist/src/output/format.js +110 -0
- package/dist/src/print/command.d.ts +2 -0
- package/dist/src/print/command.js +199 -0
- package/dist/src/print/validate/tui/ValidateView.d.ts +15 -0
- package/dist/src/print/validate/tui/ValidateView.js +37 -0
- package/dist/src/print/validate/validators/cdf-validator.d.ts +2 -0
- package/dist/src/print/validate/validators/cdf-validator.js +104 -0
- package/dist/src/print/validate/validators/dtcg-validator.d.ts +2 -0
- package/dist/src/print/validate/validators/dtcg-validator.js +110 -0
- package/dist/src/print/validate/validators/format-errors.d.ts +12 -0
- package/dist/src/print/validate/validators/format-errors.js +18 -0
- package/dist/src/program.d.ts +2 -0
- package/dist/src/program.js +25 -0
- package/dist/src/session/command.d.ts +2 -0
- package/dist/src/session/command.js +261 -0
- package/dist/src/session/db.d.ts +111 -0
- package/dist/src/session/db.js +1114 -0
- package/dist/src/session/migration.d.ts +4 -0
- package/dist/src/session/migration.js +117 -0
- package/dist/src/session/session-id.d.ts +1 -0
- package/dist/src/session/session-id.js +212 -0
- package/dist/src/session/stats.d.ts +27 -0
- package/dist/src/session/stats.js +89 -0
- package/dist/src/setup/command.d.ts +2 -0
- package/dist/src/setup/command.js +765 -0
- package/dist/src/types.d.ts +48 -0
- package/dist/src/types.js +1 -0
- package/package.json +55 -0
- package/skills/generate-components.md +361 -0
- package/skills/generate-tokens.md +194 -0
- 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,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,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,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
|
+
}
|