@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,17 @@
|
|
|
1
|
+
import type { RawPropDefinition, RawComponentDefinition } from '../types.js';
|
|
2
|
+
export interface PreClassification {
|
|
3
|
+
category: 'content' | 'design' | 'state' | 'exclude';
|
|
4
|
+
cdfTypeHint?: 'string' | 'enum' | 'richtext' | 'media';
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Deterministic pre-classification rule engine.
|
|
8
|
+
* Applies rules in priority order and returns on the first match.
|
|
9
|
+
*/
|
|
10
|
+
export declare function preClassifyProp(prop: RawPropDefinition): PreClassification | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Applies pre-classification to all props in a component definition.
|
|
13
|
+
* - Leaves existing category values unchanged
|
|
14
|
+
* - Sets category for content/design/state matches
|
|
15
|
+
* - Does NOT set category for 'exclude' results (leaves undefined)
|
|
16
|
+
*/
|
|
17
|
+
export declare function preClassifyComponent(component: RawComponentDefinition): RawComponentDefinition;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines whether a type string represents a simple/primitive type
|
|
3
|
+
* (string, boolean, number, or string literal union) vs a complex type
|
|
4
|
+
* (object, function, array, generic, etc.).
|
|
5
|
+
*/
|
|
6
|
+
function isSimpleType(type) {
|
|
7
|
+
const t = type.trim();
|
|
8
|
+
if (t === 'string' || t === 'boolean' || t === 'number')
|
|
9
|
+
return true;
|
|
10
|
+
// String literal union (e.g., "'a' | 'b'")
|
|
11
|
+
if (isStringLiteralUnion(t))
|
|
12
|
+
return true;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function isStringLiteralUnion(type) {
|
|
16
|
+
return type.includes('|') && type.includes("'");
|
|
17
|
+
}
|
|
18
|
+
function isBooleanType(type) {
|
|
19
|
+
return type.trim() === 'boolean';
|
|
20
|
+
}
|
|
21
|
+
function isStringType(type) {
|
|
22
|
+
return type.trim() === 'string';
|
|
23
|
+
}
|
|
24
|
+
function isNumberType(type) {
|
|
25
|
+
return type.trim() === 'number';
|
|
26
|
+
}
|
|
27
|
+
function isComplexType(type) {
|
|
28
|
+
return !isSimpleType(type);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Deterministic pre-classification rule engine.
|
|
32
|
+
* Applies rules in priority order and returns on the first match.
|
|
33
|
+
*/
|
|
34
|
+
export function preClassifyProp(prop) {
|
|
35
|
+
const { name, type } = prop;
|
|
36
|
+
// Rule 1: Event handlers
|
|
37
|
+
// name starts with `on` + uppercase, OR type contains `=> void` or `EventHandler`
|
|
38
|
+
if (/^on[A-Z]/.test(name) || type.includes('=> void') || type.includes('EventHandler')) {
|
|
39
|
+
return { category: 'exclude' };
|
|
40
|
+
}
|
|
41
|
+
// Rule 2: Refs
|
|
42
|
+
if (name === 'ref' || name === 'innerRef' || type.includes('Ref<') || type.includes('RefObject<')) {
|
|
43
|
+
return { category: 'exclude' };
|
|
44
|
+
}
|
|
45
|
+
// Rule 3: Test IDs
|
|
46
|
+
if (name === 'testId' || name === 'data-testid' || name === 'dataTestId') {
|
|
47
|
+
return { category: 'exclude' };
|
|
48
|
+
}
|
|
49
|
+
// Rule 4: Key prop
|
|
50
|
+
if (name === 'key') {
|
|
51
|
+
return { category: 'exclude' };
|
|
52
|
+
}
|
|
53
|
+
// Rule 5: Dispatch/setter
|
|
54
|
+
if (type.includes('Dispatch<') || type.includes('SetStateAction')) {
|
|
55
|
+
return { category: 'exclude' };
|
|
56
|
+
}
|
|
57
|
+
// Rule 6: className, style, styles
|
|
58
|
+
if (name === 'className' || name === 'style' || name === 'styles') {
|
|
59
|
+
return { category: 'design', cdfTypeHint: 'string' };
|
|
60
|
+
}
|
|
61
|
+
// Rule 7: String literal union
|
|
62
|
+
if (isStringLiteralUnion(type)) {
|
|
63
|
+
return { category: 'design', cdfTypeHint: 'enum' };
|
|
64
|
+
}
|
|
65
|
+
// Rule 8: Design name patterns (only for simple types)
|
|
66
|
+
if (!isComplexType(type)) {
|
|
67
|
+
const designNameStart = /^(variant|size|spacing|gap|color|bg|theme|align|layout|orientation|position)/i;
|
|
68
|
+
const designNameEnd = /(Color|Size|Variant|Style)$/;
|
|
69
|
+
if (designNameStart.test(name) || designNameEnd.test(name)) {
|
|
70
|
+
return { category: 'design', cdfTypeHint: 'string' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Rule 10: Boolean + state names (checked before rule 9 since state names
|
|
74
|
+
// like "disabled" would otherwise match the visual toggle prefix "disable")
|
|
75
|
+
if (isBooleanType(type)) {
|
|
76
|
+
const stateNames = ['disabled', 'loading', 'expanded', 'isOpen', 'selected', 'checked', 'active', 'preview'];
|
|
77
|
+
if (stateNames.includes(name)) {
|
|
78
|
+
return { category: 'state', cdfTypeHint: 'string' };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Rule 9: Boolean + visual toggle name
|
|
82
|
+
if (isBooleanType(type)) {
|
|
83
|
+
const visualToggle = /^(hide|show|enable|disable|vertical|horizontal|reverse|bold|italic|imageOn|with)/i;
|
|
84
|
+
if (visualToggle.test(name)) {
|
|
85
|
+
return { category: 'design', cdfTypeHint: 'string' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Rule 11: State identifiers
|
|
89
|
+
if (name === 'componentId' || name === 'sectionKey' || name === 'locale' || name === 'variantIndex') {
|
|
90
|
+
return { category: 'state', cdfTypeHint: 'string' };
|
|
91
|
+
}
|
|
92
|
+
// Rule 12: URL patterns (string type only)
|
|
93
|
+
if (isStringType(type)) {
|
|
94
|
+
const urlNameStart = /^(href|url|link|src)/i;
|
|
95
|
+
const urlNameEnd = /(Url|Href|Link|Src)$/;
|
|
96
|
+
if (urlNameStart.test(name) || urlNameEnd.test(name)) {
|
|
97
|
+
return { category: 'content', cdfTypeHint: 'string' };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Rule 13: Text patterns (string type only)
|
|
101
|
+
if (isStringType(type)) {
|
|
102
|
+
const textNameStart = /^(label|title|text|description|caption|heading|subheading|body|alt|name|placeholder|summary)/i;
|
|
103
|
+
const textNameEnd = /(Text|Label|Title|Name)$/;
|
|
104
|
+
if (textNameStart.test(name) || textNameEnd.test(name)) {
|
|
105
|
+
return { category: 'content', cdfTypeHint: 'string' };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Rule 14: Remaining strings
|
|
109
|
+
if (isStringType(type)) {
|
|
110
|
+
return { category: 'content', cdfTypeHint: 'string' };
|
|
111
|
+
}
|
|
112
|
+
// Rule 15: Remaining booleans
|
|
113
|
+
if (isBooleanType(type)) {
|
|
114
|
+
return { category: 'design', cdfTypeHint: 'string' };
|
|
115
|
+
}
|
|
116
|
+
// Rule 16: Remaining numbers
|
|
117
|
+
if (isNumberType(type)) {
|
|
118
|
+
return { category: 'design', cdfTypeHint: 'string' };
|
|
119
|
+
}
|
|
120
|
+
// Rule 17: Complex/object/function/array types — no hint
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Applies pre-classification to all props in a component definition.
|
|
125
|
+
* - Leaves existing category values unchanged
|
|
126
|
+
* - Sets category for content/design/state matches
|
|
127
|
+
* - Does NOT set category for 'exclude' results (leaves undefined)
|
|
128
|
+
*/
|
|
129
|
+
export function preClassifyComponent(component) {
|
|
130
|
+
const props = component.props.map((prop) => {
|
|
131
|
+
// If category is already set, leave unchanged
|
|
132
|
+
if (prop.category) {
|
|
133
|
+
return prop;
|
|
134
|
+
}
|
|
135
|
+
const result = preClassifyProp(prop);
|
|
136
|
+
// If no result or excluded, leave unchanged
|
|
137
|
+
if (!result || result.category === 'exclude') {
|
|
138
|
+
return prop;
|
|
139
|
+
}
|
|
140
|
+
// Set category hint
|
|
141
|
+
return { ...prop, category: result.category };
|
|
142
|
+
});
|
|
143
|
+
return { ...component, props };
|
|
144
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { createElement } from 'react';
|
|
4
|
+
import { render } from 'ink';
|
|
5
|
+
import { getRefineArtifactsRoot, ensureRefineSession, getRefineSessionPaths, saveReviewState } from './persistence.js';
|
|
6
|
+
import { loadReviewInput } from './parser.js';
|
|
7
|
+
import { App } from './tui/App.js';
|
|
8
|
+
import { openPipelineDb, loadRawComponents, storeRawComponents, createStep, updateStep } from '../../session/db.js';
|
|
9
|
+
const SAFE_PATH_RE = /^[a-zA-Z0-9_.$[\]=]+$/;
|
|
10
|
+
const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
11
|
+
function applyDotPath(obj, path, value) {
|
|
12
|
+
if (!SAFE_PATH_RE.test(path)) {
|
|
13
|
+
process.stderr.write(`Warning: --patch path contains invalid characters: '${path}', skipping\n`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const parts = path.split('.');
|
|
17
|
+
if (parts.some((p) => PROTO_KEYS.has(p))) {
|
|
18
|
+
process.stderr.write(`Warning: --patch path contains forbidden key: '${path}', skipping\n`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let current = obj;
|
|
22
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
23
|
+
const part = parts[i];
|
|
24
|
+
// Handle array predicate syntax: field[name=value]
|
|
25
|
+
const arrayMatch = /^(.+)\[name=(.+)\]$/.exec(part);
|
|
26
|
+
if (arrayMatch) {
|
|
27
|
+
const [, fieldName, matchValue] = arrayMatch;
|
|
28
|
+
const arr = current[fieldName];
|
|
29
|
+
if (Array.isArray(arr)) {
|
|
30
|
+
const item = arr.find((el) => el['name'] === matchValue);
|
|
31
|
+
if (item) {
|
|
32
|
+
current = item;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
process.stderr.write(`Warning: --patch array item [name=${matchValue}] not found in '${fieldName}', skipping\n`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
if (typeof current[part] !== 'object' || current[part] === null) {
|
|
42
|
+
process.stderr.write(`Warning: --patch path '${path}' — '${part}' is not an object, skipping\n`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
current = current[part];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const lastPart = parts[parts.length - 1];
|
|
49
|
+
current[lastPart] = value;
|
|
50
|
+
}
|
|
51
|
+
function applyPatch(snapshot, ops) {
|
|
52
|
+
const components = snapshot.components.map((c) => {
|
|
53
|
+
const op = ops.find((o) => o.component === c.name);
|
|
54
|
+
if (!op)
|
|
55
|
+
return c;
|
|
56
|
+
let updated = { ...c };
|
|
57
|
+
if (op.status) {
|
|
58
|
+
updated = { ...updated, status: op.status };
|
|
59
|
+
}
|
|
60
|
+
if (op.set) {
|
|
61
|
+
const editedProposal = structuredClone(updated.editedProposal);
|
|
62
|
+
for (const [path, value] of Object.entries(op.set)) {
|
|
63
|
+
applyDotPath(editedProposal, path, value);
|
|
64
|
+
}
|
|
65
|
+
updated = {
|
|
66
|
+
...updated,
|
|
67
|
+
editedProposal: editedProposal,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return updated;
|
|
71
|
+
});
|
|
72
|
+
return { ...snapshot, components };
|
|
73
|
+
}
|
|
74
|
+
async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
75
|
+
let result = { ...snapshot };
|
|
76
|
+
const rejectPatterns = [...(opts.reject ?? []), ...(opts.deselect ?? [])].map((p) => p.toLowerCase());
|
|
77
|
+
const selectPatterns = (opts.select ?? []).map((p) => p.toLowerCase());
|
|
78
|
+
const selectAll = opts.acceptAll || opts.selectAll;
|
|
79
|
+
if (selectAll || rejectPatterns.length > 0 || selectPatterns.length > 0) {
|
|
80
|
+
result = {
|
|
81
|
+
...result,
|
|
82
|
+
components: result.components.map((c) => {
|
|
83
|
+
const nameLower = c.name.toLowerCase();
|
|
84
|
+
if (rejectPatterns.some((p) => nameLower.includes(p))) {
|
|
85
|
+
return { ...c, status: 'rejected' };
|
|
86
|
+
}
|
|
87
|
+
if (selectAll || selectPatterns.some((p) => nameLower.includes(p))) {
|
|
88
|
+
return { ...c, status: 'accepted' };
|
|
89
|
+
}
|
|
90
|
+
return c;
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Apply --patch
|
|
95
|
+
if (opts.patch) {
|
|
96
|
+
let patchOps;
|
|
97
|
+
try {
|
|
98
|
+
const raw = await readFile(resolve(opts.patch), 'utf8');
|
|
99
|
+
const parsed = JSON.parse(raw);
|
|
100
|
+
if (!Array.isArray(parsed)) {
|
|
101
|
+
process.stderr.write(`Error: --patch file must be a JSON array of patch operations: ${opts.patch}\n`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
patchOps = parsed;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
process.stderr.write(`Error: cannot read or parse --patch file: ${opts.patch}\n`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Warn on unknown component names
|
|
113
|
+
const knownNames = new Set(result.components.map((c) => c.name));
|
|
114
|
+
for (const op of patchOps) {
|
|
115
|
+
if (!knownNames.has(op.component)) {
|
|
116
|
+
process.stderr.write(`Warning: --patch targets unknown component '${op.component}', skipping\n`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
result = applyPatch(result, patchOps);
|
|
120
|
+
}
|
|
121
|
+
const accepted = result.components.filter((c) => c.status === 'accepted');
|
|
122
|
+
const rejected = result.components.filter((c) => c.status === 'rejected');
|
|
123
|
+
// Persist decisions to session state so pipeline orchestrator can read them
|
|
124
|
+
await saveReviewState(paths.statePath, result);
|
|
125
|
+
// Sync edited proposals back to the DB so generation uses the user's edits
|
|
126
|
+
if (accepted.length > 0) {
|
|
127
|
+
const db = openPipelineDb();
|
|
128
|
+
try {
|
|
129
|
+
storeRawComponents(db, sessionId, accepted.map((c) => c.editedProposal));
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
db.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length}\n`);
|
|
136
|
+
}
|
|
137
|
+
function resolveSessionId(sessionFlag) {
|
|
138
|
+
if (sessionFlag)
|
|
139
|
+
return sessionFlag;
|
|
140
|
+
const db = openPipelineDb();
|
|
141
|
+
try {
|
|
142
|
+
const row = db
|
|
143
|
+
.prepare(`SELECT s.id FROM sessions s
|
|
144
|
+
JOIN steps st ON st.session_id = s.id
|
|
145
|
+
WHERE st.command = 'analyze extract'
|
|
146
|
+
AND st.status = 'complete'
|
|
147
|
+
ORDER BY st.started_at DESC
|
|
148
|
+
LIMIT 1`)
|
|
149
|
+
.get();
|
|
150
|
+
if (!row) {
|
|
151
|
+
process.stderr.write('Error: no completed analyze extract session found. Run analyze extract first, or pass --session <id>.\n');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
return row.id;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
db.close();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function registerAnalyzeEditCommand(program) {
|
|
161
|
+
program
|
|
162
|
+
.command('select')
|
|
163
|
+
.alias('edit')
|
|
164
|
+
.description('Select components for generation and optionally patch their definitions')
|
|
165
|
+
.option('--session <id>', 'Session ID from analyze extract (defaults to most recent)')
|
|
166
|
+
.option('--project-root <path>', 'Project root for resolving component source files')
|
|
167
|
+
.option('--select-all', 'Select all components without launching the TUI')
|
|
168
|
+
.option('--select <pattern>', 'Select components whose name contains pattern (repeatable)', collect, [])
|
|
169
|
+
.option('--deselect <pattern>', 'Deselect components whose name contains pattern (repeatable)', collect, [])
|
|
170
|
+
.option('--accept-all', 'Alias for --select-all', false)
|
|
171
|
+
.option('--reject <pattern>', 'Alias for --deselect <pattern> (repeatable)', collect, [])
|
|
172
|
+
.option('--patch <path>', 'Path to a JSON patch file for structured component overrides')
|
|
173
|
+
.action(async ({ session: sessionFlag, projectRoot, acceptAll, selectAll, reject, deselect, select, patch, }) => {
|
|
174
|
+
const sessionId = resolveSessionId(sessionFlag);
|
|
175
|
+
const db = openPipelineDb();
|
|
176
|
+
let rawComponents;
|
|
177
|
+
try {
|
|
178
|
+
rawComponents = loadRawComponents(db, sessionId);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
db.close();
|
|
182
|
+
}
|
|
183
|
+
if (rawComponents.length === 0) {
|
|
184
|
+
process.stderr.write(`Error: session '${sessionId}' has no raw components. Run analyze extract first.\n`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const artifactsRoot = getRefineArtifactsRoot();
|
|
189
|
+
const nonInteractive = acceptAll ||
|
|
190
|
+
selectAll ||
|
|
191
|
+
(reject ?? []).length > 0 ||
|
|
192
|
+
(deselect ?? []).length > 0 ||
|
|
193
|
+
(select ?? []).length > 0 ||
|
|
194
|
+
!!patch;
|
|
195
|
+
let paths;
|
|
196
|
+
let snapshot;
|
|
197
|
+
try {
|
|
198
|
+
snapshot = await loadReviewInput(rawComponents, {
|
|
199
|
+
reviewRoot: projectRoot,
|
|
200
|
+
});
|
|
201
|
+
paths = await getRefineSessionPaths(sessionId, artifactsRoot);
|
|
202
|
+
if (!nonInteractive) {
|
|
203
|
+
snapshot = await ensureRefineSession(sessionId, artifactsRoot, snapshot);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
await ensureRefineSession(sessionId, artifactsRoot, snapshot);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
process.stderr.write(`Error: unable to initialize refine session.\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Non-interactive path
|
|
215
|
+
if (nonInteractive) {
|
|
216
|
+
const stepDb = openPipelineDb();
|
|
217
|
+
const stepId = createStep(stepDb, sessionId, 'analyze select', { sessionId });
|
|
218
|
+
try {
|
|
219
|
+
await runNonInteractive(snapshot, { session: sessionFlag, projectRoot, acceptAll, selectAll, reject, deselect, select, patch }, paths, sessionId);
|
|
220
|
+
updateStep(stepDb, stepId, 'complete', { sessionId });
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
updateStep(stepDb, stepId, 'failed', {}, err instanceof Error ? err.message : String(err));
|
|
224
|
+
stepDb.close();
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
stepDb.close();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Test mode: print launch contract and return without launching TUI
|
|
231
|
+
if (process.env.EDS_REVIEW_TEST_MODE === '1') {
|
|
232
|
+
process.stdout.write(`session=${sessionId}\n` +
|
|
233
|
+
`session_dir=${paths.sessionDir}\n` +
|
|
234
|
+
`events.jsonl=${paths.eventsPath}\n` +
|
|
235
|
+
`current-review-state.json=${paths.statePath}\n`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (!process.stdout.isTTY) {
|
|
239
|
+
process.stderr.write('Error: analyze select requires an interactive terminal\n');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
if (process.stdout.columns !== undefined && process.stdout.columns < 60) {
|
|
243
|
+
process.stderr.write(`Error: terminal too narrow (${process.stdout.columns} cols). Resize to 60+ columns.\n`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const { waitUntilExit } = render(createElement(App, {
|
|
247
|
+
sessionId,
|
|
248
|
+
artifactsRoot,
|
|
249
|
+
reviewRoot: projectRoot,
|
|
250
|
+
}));
|
|
251
|
+
await waitUntilExit();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function collect(val, prev) {
|
|
255
|
+
return [...prev, val];
|
|
256
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { registerAnalyzeEditCommand } from './command.js';
|
|
2
|
+
export { loadReviewInput } from './parser.js';
|
|
3
|
+
export { appendReviewEvent, ensureRefineSession, getRefineArtifactsRoot, getRefineSessionPaths, saveReviewState, } from './persistence.js';
|
|
4
|
+
export { formatFinalizeContract } from './stdout.js';
|
|
5
|
+
export type { ReviewComponentRecord, ReviewComponentSummary, ReviewComponentStatus, ReviewEvent, ReviewSessionPaths, ReviewSessionSnapshot, ReviewSessionSummary, } from './types.js';
|
|
6
|
+
export { createReviewSessionSummary } from './types.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { registerAnalyzeEditCommand } from './command.js';
|
|
2
|
+
export { loadReviewInput } from './parser.js';
|
|
3
|
+
export { appendReviewEvent, ensureRefineSession, getRefineArtifactsRoot, getRefineSessionPaths, saveReviewState, } from './persistence.js';
|
|
4
|
+
export { formatFinalizeContract } from './stdout.js';
|
|
5
|
+
export { createReviewSessionSummary } from './types.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RawComponentDefinition } from '../../types.js';
|
|
2
|
+
import type { ReviewSessionSnapshot } from './types.js';
|
|
3
|
+
export type LoadReviewInputOptions = {
|
|
4
|
+
reviewRoot?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function loadReviewInput(components: RawComponentDefinition[], options?: LoadReviewInputOptions): Promise<ReviewSessionSnapshot>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
function createComponentId(name, resolvedSourcePath) {
|
|
5
|
+
const sourceHash = createHash('sha256').update(`${name}:${resolvedSourcePath}`).digest('hex').slice(0, 12);
|
|
6
|
+
return `${name}-${sourceHash}`;
|
|
7
|
+
}
|
|
8
|
+
async function resolveComponentSourcePath(source, reviewRoot) {
|
|
9
|
+
// Absolute paths stored in the DB are used directly — no reviewRoot boundary check needed.
|
|
10
|
+
if (isAbsolute(source)) {
|
|
11
|
+
try {
|
|
12
|
+
await access(source);
|
|
13
|
+
return source;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error(`Unable to access component source at ${source}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const candidate = resolve(reviewRoot, source);
|
|
20
|
+
const relativeToRoot = relative(reviewRoot, candidate);
|
|
21
|
+
if (relativeToRoot.startsWith('..') || relativeToRoot === '..' || isAbsolute(relativeToRoot)) {
|
|
22
|
+
throw new Error(`Resolved component source is outside the review root: ${source}. Pass --project-root <path> to set the correct base.`);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await access(candidate);
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`Unable to access component source at ${candidate}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function loadReviewInput(components, options = {}) {
|
|
33
|
+
const reviewRoot = resolve(options.reviewRoot ?? process.cwd());
|
|
34
|
+
const records = await Promise.all(components.map(async (component) => {
|
|
35
|
+
let resolvedSourcePath;
|
|
36
|
+
try {
|
|
37
|
+
resolvedSourcePath = await resolveComponentSourcePath(component.source, reviewRoot);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`Unable to access component source for ${component.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
id: createComponentId(component.name, resolvedSourcePath),
|
|
44
|
+
name: component.name,
|
|
45
|
+
resolvedSourcePath,
|
|
46
|
+
sourceCode: null,
|
|
47
|
+
originalProposal: component,
|
|
48
|
+
editedProposal: structuredClone(component),
|
|
49
|
+
status: 'needs-review',
|
|
50
|
+
};
|
|
51
|
+
}));
|
|
52
|
+
return { components: records };
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReviewSessionPaths, ReviewSessionSnapshot } from './types.js';
|
|
2
|
+
export declare function getRefineArtifactsRoot(): string;
|
|
3
|
+
export declare function getRefineSessionPaths(sessionId: string, artifactsRoot: string): Promise<ReviewSessionPaths>;
|
|
4
|
+
export declare function saveReviewState(statePath: string, session: ReviewSessionSnapshot): Promise<void>;
|
|
5
|
+
export declare function appendReviewEvent(eventsPath: string, event: {
|
|
6
|
+
type: string;
|
|
7
|
+
payload: Record<string, unknown>;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare function ensureRefineSession(sessionId: string, artifactsRoot: string, initialSnapshot: ReviewSessionSnapshot): Promise<ReviewSessionSnapshot>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { access, appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
export function getRefineArtifactsRoot() {
|
|
5
|
+
if (process.env.EDS_REVIEW_ARTIFACTS_DIR) {
|
|
6
|
+
return resolve(process.env.EDS_REVIEW_ARTIFACTS_DIR);
|
|
7
|
+
}
|
|
8
|
+
return resolve(homedir(), '.contentful', 'experience-design-system-cli', 'reviews');
|
|
9
|
+
}
|
|
10
|
+
export async function getRefineSessionPaths(sessionId, artifactsRoot) {
|
|
11
|
+
const sessionDir = resolve(artifactsRoot, sessionId);
|
|
12
|
+
return {
|
|
13
|
+
sessionDir,
|
|
14
|
+
eventsPath: resolve(sessionDir, 'events.jsonl'),
|
|
15
|
+
statePath: resolve(sessionDir, 'current-review-state.json'),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function saveReviewState(statePath, session) {
|
|
19
|
+
await writeFile(statePath, JSON.stringify(session, null, 2), 'utf8');
|
|
20
|
+
}
|
|
21
|
+
export async function appendReviewEvent(eventsPath, event) {
|
|
22
|
+
const record = {
|
|
23
|
+
type: event.type,
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
payload: event.payload,
|
|
26
|
+
};
|
|
27
|
+
await appendFile(eventsPath, JSON.stringify(record) + '\n', 'utf8');
|
|
28
|
+
}
|
|
29
|
+
export async function ensureRefineSession(sessionId, artifactsRoot, initialSnapshot) {
|
|
30
|
+
const paths = await getRefineSessionPaths(sessionId, artifactsRoot);
|
|
31
|
+
await mkdir(paths.sessionDir, { recursive: true });
|
|
32
|
+
try {
|
|
33
|
+
await access(paths.statePath);
|
|
34
|
+
const savedState = await readFile(paths.statePath, 'utf8');
|
|
35
|
+
return JSON.parse(savedState);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
await writeFile(paths.statePath, JSON.stringify(initialSnapshot, null, 2), 'utf8');
|
|
39
|
+
await writeFile(paths.eventsPath, '', 'utf8');
|
|
40
|
+
return initialSnapshot;
|
|
41
|
+
}
|
|
42
|
+
}
|