@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,491 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Box, Text, useStdout } from 'ink';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { createReviewSessionDetail } from '../types.js';
6
+ import { TopBar } from './components/TopBar.js';
7
+ import { Sidebar } from './components/Sidebar.js';
8
+ import { ComponentDetail } from './components/ComponentDetail.js';
9
+ import { StatusBar } from './components/StatusBar.js';
10
+ import { HelpOverlay } from './components/HelpOverlay.js';
11
+ import { FinalizeDialog } from './components/FinalizeDialog.js';
12
+ import { QuitDialog } from './components/QuitDialog.js';
13
+ import { PreviewSummaryBar } from './components/PreviewSummaryBar.js';
14
+ import { useKeymap } from './hooks/useKeymap.js';
15
+ import { useImmediateInput } from './hooks/useImmediateInput.js';
16
+ import { useSession } from './hooks/useSession.js';
17
+ import { openPipelineDb, storeRawComponents, loadCDFComponents } from '../../../session/db.js';
18
+ import { ImportApiClient } from '../../../apply/api-client.js';
19
+ import { readTokensFromPath } from '../../../apply/manifest.js';
20
+ import { buildManifest } from '@contentful/experience-design-system-types';
21
+ export function App({ sessionId, artifactsRoot, reviewRoot }) {
22
+ const { stdout } = useStdout();
23
+ const terminalWidth = stdout?.columns ?? 80;
24
+ const { session: loadedSession, paths, loading, error: sessionError, saveState, appendEvent, } = useSession({
25
+ sessionId,
26
+ artifactsRoot,
27
+ reviewRoot,
28
+ });
29
+ const [session, setSession] = useState(null);
30
+ const [selectedId, setSelectedId] = useState(null);
31
+ const [draftsByComponentId, setDraftsByComponentId] = useState({});
32
+ const [sidebarFocused, setSidebarFocused] = useState(true);
33
+ const [editMode, setEditMode] = useState(false);
34
+ const [sourceVisible, setSourceVisible] = useState(false);
35
+ const [showHelp, setShowHelp] = useState(false);
36
+ const [showFinalizeDialog, setShowFinalizeDialog] = useState(false);
37
+ const [showQuitDialog, setShowQuitDialog] = useState(false);
38
+ const [isSaving] = useState(false);
39
+ const [saveError, setSaveError] = useState(null);
40
+ const [finalizedResult, setFinalizedResult] = useState(null);
41
+ const [sidebarScrollOffset, setSidebarScrollOffset] = useState(0);
42
+ const [jsonScrollOffset, setJsonScrollOffset] = useState(0);
43
+ const [previewAnnotations, setPreviewAnnotations] = useState(() => {
44
+ const raw = process.env['EDS_PREVIEW_ANNOTATIONS'];
45
+ if (!raw)
46
+ return {};
47
+ try {
48
+ return JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return {};
52
+ }
53
+ });
54
+ const [previewLoading, setPreviewLoading] = useState(false);
55
+ const [previewError, setPreviewError] = useState(null);
56
+ const [previewResponse, setPreviewResponse] = useState(null);
57
+ const refreshPreview = useCallback(async () => {
58
+ const cmaToken = process.env['EDS_CMA_TOKEN'];
59
+ const spaceId = process.env['EDS_SPACE_ID'];
60
+ const environmentId = process.env['EDS_ENVIRONMENT_ID'];
61
+ const tokensPath = process.env['EDS_TOKENS_PATH'];
62
+ if (!cmaToken || !spaceId || !environmentId)
63
+ return;
64
+ setPreviewLoading(true);
65
+ try {
66
+ const db = openPipelineDb();
67
+ let components = [];
68
+ try {
69
+ components = loadCDFComponents(db, sessionId);
70
+ }
71
+ finally {
72
+ db.close();
73
+ }
74
+ let tokens = [];
75
+ if (tokensPath)
76
+ tokens = await readTokensFromPath('tokens', tokensPath);
77
+ const manifest = buildManifest(components, tokens);
78
+ if (!manifest.componentsManifest)
79
+ manifest.componentsManifest = {};
80
+ const client = new ImportApiClient({ cmaToken, spaceId, environmentId });
81
+ const orgId = await client.resolveOrganizationId();
82
+ client.setOrganizationId(orgId);
83
+ const preview = await client.previewImport(manifest);
84
+ const annotations = {};
85
+ for (const item of preview.components.new) {
86
+ const name = item.name ?? '';
87
+ if (name)
88
+ annotations[name] = 'new';
89
+ }
90
+ for (const item of preview.components.removed) {
91
+ annotations[item.name] = 'removed';
92
+ }
93
+ for (const item of preview.components.changed) {
94
+ if (item.changeClassification?.classification === 'breaking') {
95
+ annotations[item.current.name] = 'breaking';
96
+ }
97
+ else {
98
+ annotations[item.current.name] = 'changed';
99
+ }
100
+ }
101
+ setPreviewAnnotations(annotations);
102
+ setPreviewResponse(preview);
103
+ setPreviewError(null);
104
+ }
105
+ catch (err) {
106
+ const msg = err instanceof Error ? err.message : String(err);
107
+ setPreviewError(msg.includes('token') ? 'Preview request failed' : msg);
108
+ }
109
+ finally {
110
+ setPreviewLoading(false);
111
+ }
112
+ }, [sessionId]);
113
+ const syncSessionToDb = useCallback((currentSession) => {
114
+ const db = openPipelineDb();
115
+ try {
116
+ db.prepare(`UPDATE raw_components SET status = 'extracted' WHERE session_id = ?`).run(sessionId);
117
+ const acceptedNames = currentSession.components
118
+ .filter((c) => c.status === 'accepted' || c.status === 'reviewed')
119
+ .map((c) => c.name);
120
+ if (acceptedNames.length > 0) {
121
+ db.prepare(`UPDATE raw_components SET status = 'generated' WHERE session_id = ? AND name IN (${acceptedNames.map(() => '?').join(',')})`).run(sessionId, ...acceptedNames);
122
+ }
123
+ }
124
+ finally {
125
+ db.close();
126
+ }
127
+ }, [sessionId]);
128
+ const previewDebounceRef = useRef(null);
129
+ const debouncedRefreshPreview = useCallback((currentSession) => {
130
+ if (previewDebounceRef.current)
131
+ clearTimeout(previewDebounceRef.current);
132
+ previewDebounceRef.current = setTimeout(() => {
133
+ syncSessionToDb(currentSession);
134
+ void refreshPreview();
135
+ }, 500);
136
+ }, [syncSessionToDb, refreshPreview]);
137
+ useEffect(() => {
138
+ return () => {
139
+ if (previewDebounceRef.current)
140
+ clearTimeout(previewDebounceRef.current);
141
+ };
142
+ }, []);
143
+ // Sync loaded session into local state
144
+ useEffect(() => {
145
+ if (loadedSession && !session) {
146
+ setSession(loadedSession);
147
+ setSelectedId(loadedSession.components[0]?.id ?? null);
148
+ }
149
+ }, [loadedSession]);
150
+ useEffect(() => {
151
+ if (session && !previewResponse) {
152
+ const raw = process.env['EDS_PREVIEW_COUNTS'];
153
+ if (raw) {
154
+ try {
155
+ const counts = JSON.parse(raw);
156
+ setPreviewResponse({
157
+ components: {
158
+ new: Array(counts.compNew).fill({}),
159
+ changed: Array(counts.compChanged).fill({}),
160
+ removed: Array(counts.compRemoved).fill({}),
161
+ unchanged: Array(counts.compUnchanged).fill({}),
162
+ },
163
+ tokens: {
164
+ new: Array(counts.tokNew).fill({}),
165
+ changed: Array(counts.tokChanged).fill({}),
166
+ removed: Array(counts.tokRemoved).fill({}),
167
+ unchanged: Array(counts.tokUnchanged).fill({}),
168
+ },
169
+ });
170
+ }
171
+ catch (err) {
172
+ process.stderr.write(`[eds] failed to parse EDS_PREVIEW_COUNTS: ${err instanceof Error ? err.message : String(err)}\n`);
173
+ }
174
+ }
175
+ }
176
+ }, [session]);
177
+ // Lazy source code loading
178
+ useEffect(() => {
179
+ if (!session || !selectedId)
180
+ return;
181
+ const selectedComponent = session.components.find((c) => c.id === selectedId);
182
+ if (!selectedComponent || selectedComponent.sourceCode !== null)
183
+ return;
184
+ readFile(selectedComponent.resolvedSourcePath, 'utf8')
185
+ .then((code) => {
186
+ setSession((prev) => {
187
+ if (!prev)
188
+ return prev;
189
+ return {
190
+ ...prev,
191
+ components: prev.components.map((c) => (c.id === selectedComponent.id ? { ...c, sourceCode: code } : c)),
192
+ };
193
+ });
194
+ })
195
+ .catch(() => {
196
+ // Leave as null; SourcePanel shows [No source available]
197
+ });
198
+ }, [selectedId]);
199
+ // SIGINT handler
200
+ useEffect(() => {
201
+ const handler = () => {
202
+ if (Object.keys(draftsByComponentId).length > 0) {
203
+ setShowQuitDialog(true);
204
+ }
205
+ else {
206
+ process.exit(1);
207
+ }
208
+ };
209
+ process.on('SIGINT', handler);
210
+ return () => {
211
+ process.off('SIGINT', handler);
212
+ };
213
+ }, [draftsByComponentId]);
214
+ const updateStatus = async (componentId, newStatus) => {
215
+ if (!session || !paths)
216
+ return;
217
+ const updatedSession = {
218
+ ...session,
219
+ components: session.components.map((c) => (c.id === componentId ? { ...c, status: newStatus } : c)),
220
+ };
221
+ setSession(updatedSession);
222
+ await saveState(updatedSession);
223
+ const component = session.components.find((c) => c.id === componentId);
224
+ await appendEvent({
225
+ type: 'status_changed',
226
+ payload: { componentId, from: component?.status, to: newStatus },
227
+ });
228
+ debouncedRefreshPreview(updatedSession);
229
+ };
230
+ const dialogOpen = showHelp || showFinalizeDialog || showQuitDialog;
231
+ useKeymap({
232
+ sidebarFocused,
233
+ editMode,
234
+ dialogOpen,
235
+ disabled: isSaving,
236
+ }, {
237
+ onSidebarUp: () => {
238
+ if (!session)
239
+ return;
240
+ const idx = session.components.findIndex((c) => c.id === selectedId);
241
+ if (idx > 0) {
242
+ setSelectedId(session.components[idx - 1].id);
243
+ setJsonScrollOffset(0);
244
+ setSidebarScrollOffset((prev) => Math.min(prev, idx - 1));
245
+ }
246
+ },
247
+ onSidebarDown: () => {
248
+ if (!session)
249
+ return;
250
+ const idx = session.components.findIndex((c) => c.id === selectedId);
251
+ if (idx < session.components.length - 1) {
252
+ setSelectedId(session.components[idx + 1].id);
253
+ setJsonScrollOffset(0);
254
+ setSidebarScrollOffset((prev) => {
255
+ const newIdx = idx + 1;
256
+ return newIdx >= prev + visibleCount ? newIdx - visibleCount + 1 : prev;
257
+ });
258
+ }
259
+ },
260
+ onSidebarSelect: () => { },
261
+ onAccept: () => {
262
+ if (selectedId)
263
+ void updateStatus(selectedId, 'accepted');
264
+ },
265
+ onReject: () => {
266
+ if (selectedId)
267
+ void updateStatus(selectedId, 'rejected');
268
+ },
269
+ onEnterEditMode: () => {
270
+ if (!session || !selectedId)
271
+ return;
272
+ const component = session.components.find((c) => c.id === selectedId);
273
+ if (!component)
274
+ return;
275
+ setEditMode(true);
276
+ setDraftsByComponentId((prev) => ({
277
+ ...prev,
278
+ [selectedId]: prev[selectedId] ?? JSON.stringify(component.editedProposal, null, 2),
279
+ }));
280
+ },
281
+ onToggleSource: () => {
282
+ if (terminalWidth < 120) {
283
+ setSaveError('Terminal too narrow for source panel (need 120+ cols)');
284
+ setTimeout(() => setSaveError(null), 3000);
285
+ return;
286
+ }
287
+ setSourceVisible((prev) => !prev);
288
+ },
289
+ onScrollUp: () => {
290
+ setJsonScrollOffset((prev) => Math.max(0, prev - 1));
291
+ },
292
+ onScrollDown: () => {
293
+ setJsonScrollOffset((prev) => prev + 1);
294
+ },
295
+ onToggleFocus: () => setSidebarFocused((prev) => !prev),
296
+ onApproveAll: async () => {
297
+ if (!session || !paths)
298
+ return;
299
+ const updatedComponents = session.components.map((c) => c.status === 'needs-review' ? { ...c, status: 'accepted' } : c);
300
+ const affected = updatedComponents.filter((c) => c.status === 'accepted').length -
301
+ session.components.filter((c) => c.status === 'accepted').length;
302
+ const updatedSession = { ...session, components: updatedComponents };
303
+ setSession(updatedSession);
304
+ await saveState(updatedSession);
305
+ await appendEvent({ type: 'approve_all', payload: { affected } });
306
+ syncSessionToDb(updatedSession);
307
+ void refreshPreview();
308
+ },
309
+ onFinalize: () => setShowFinalizeDialog(true),
310
+ onQuit: () => {
311
+ if (Object.keys(draftsByComponentId).length > 0) {
312
+ setShowQuitDialog(true);
313
+ }
314
+ else {
315
+ process.exit(1);
316
+ }
317
+ },
318
+ onToggleHelp: () => setShowHelp((prev) => !prev),
319
+ });
320
+ if (loading) {
321
+ return _jsx(Text, { children: "Loading session..." });
322
+ }
323
+ if (sessionError) {
324
+ return (_jsxs(Text, { color: "red", children: [sessionError, '\nPress q to exit.'] }));
325
+ }
326
+ if (!session || !paths) {
327
+ return _jsx(Text, { color: "red", children: "Session unavailable." });
328
+ }
329
+ if (finalizedResult) {
330
+ return _jsx(FinalizedScreen, { result: finalizedResult });
331
+ }
332
+ const sessionSummary = session.components.map((c) => ({
333
+ id: c.id,
334
+ name: c.name,
335
+ status: c.status,
336
+ previewAnnotation: previewAnnotations[c.name],
337
+ }));
338
+ const selectedRecord = session.components.find((c) => c.id === selectedId) ?? null;
339
+ const sessionDetail = selectedRecord ? createReviewSessionDetail({ ...session, components: [selectedRecord] }) : null;
340
+ const selectedDetail = sessionDetail?.components[0] ?? null;
341
+ const acceptedCount = session.components.filter((c) => c.status === 'accepted').length;
342
+ const rejectedCount = session.components.filter((c) => c.status === 'rejected').length;
343
+ const reviewedCount = session.components.filter((c) => c.status === 'reviewed').length;
344
+ const needsReviewCount = session.components.filter((c) => c.status === 'needs-review').length;
345
+ const hints = editMode
346
+ ? [
347
+ { key: 'Ctrl+S', label: 'save' },
348
+ { key: 'Esc', label: 'discard' },
349
+ ]
350
+ : dialogOpen
351
+ ? []
352
+ : [
353
+ { key: '?', label: 'help' },
354
+ { key: 'q', label: 'quit' },
355
+ ];
356
+ const collapsed = terminalWidth < 80;
357
+ const visibleCount = 20;
358
+ const longestName = session.components.reduce((max, c) => Math.max(max, c.name.length), 0);
359
+ // icon + space + name + 2 border chars; min 14, max 22
360
+ const sidebarWidth = collapsed ? 3 : Math.min(Math.max(longestName + 4, 14), 22);
361
+ const handleDraftSave = async () => {
362
+ if (!selectedId || !session || !paths)
363
+ return;
364
+ const draft = draftsByComponentId[selectedId];
365
+ if (!draft)
366
+ return;
367
+ try {
368
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
369
+ const parsed = JSON.parse(draft);
370
+ const currentStatus = session.components.find((c) => c.id === selectedId)?.status;
371
+ const newStatus = currentStatus === 'needs-review' ? 'reviewed' : currentStatus;
372
+ const updatedSession = {
373
+ ...session,
374
+ components: session.components.map((c) => c.id === selectedId
375
+ ? {
376
+ ...c,
377
+ editedProposal: parsed,
378
+ status: newStatus,
379
+ }
380
+ : c),
381
+ };
382
+ const { [selectedId]: _removed, ...remainingDrafts } = draftsByComponentId;
383
+ setSession(updatedSession);
384
+ setDraftsByComponentId(remainingDrafts);
385
+ setEditMode(false);
386
+ await saveState(updatedSession);
387
+ await appendEvent({
388
+ type: 'draft_saved',
389
+ payload: { componentId: selectedId },
390
+ });
391
+ // Sync all edited proposals to pipeline DB (keep 'generated' for preview)
392
+ const allEdited = updatedSession.components.map((c) => c.editedProposal);
393
+ const syncDb = openPipelineDb();
394
+ try {
395
+ storeRawComponents(syncDb, sessionId, allEdited, {
396
+ status: 'generated',
397
+ preserveCDF: true,
398
+ });
399
+ }
400
+ finally {
401
+ syncDb.close();
402
+ }
403
+ void refreshPreview();
404
+ }
405
+ catch {
406
+ // JSON parse error — JsonEditor already shows the error inline
407
+ }
408
+ };
409
+ const handleDraftDiscard = () => {
410
+ if (!selectedId)
411
+ return;
412
+ const { [selectedId]: _removed, ...remainingDrafts } = draftsByComponentId;
413
+ setDraftsByComponentId(remainingDrafts);
414
+ setEditMode(false);
415
+ };
416
+ const handleFinalize = async () => {
417
+ if (!session || !paths)
418
+ return;
419
+ try {
420
+ await appendEvent({
421
+ type: 'finalized',
422
+ payload: {
423
+ accepted: acceptedCount,
424
+ rejected: rejectedCount,
425
+ excluded: needsReviewCount,
426
+ },
427
+ });
428
+ // Write all components back to DB, marking accepted as 'generated'
429
+ // so loadCDFComponents (push/preview) only picks up accepted ones,
430
+ // but loadRawComponents (editor re-entry) still finds all of them
431
+ const acceptedNames = new Set(session.components.filter((c) => c.status === 'accepted').map((c) => c.name));
432
+ const db = openPipelineDb();
433
+ try {
434
+ storeRawComponents(db, sessionId, session.components.map((c) => c.editedProposal), { status: 'extracted', preserveCDF: true });
435
+ if (acceptedNames.size > 0) {
436
+ db.prepare(`UPDATE raw_components SET status = 'generated' WHERE session_id = ? AND name IN (${[...acceptedNames].map(() => '?').join(',')})`).run(sessionId, ...acceptedNames);
437
+ }
438
+ }
439
+ finally {
440
+ db.close();
441
+ }
442
+ const output = JSON.stringify({
443
+ status: 'finalized',
444
+ sessionDir: paths.sessionDir,
445
+ accepted: acceptedCount,
446
+ rejected: rejectedCount,
447
+ excluded: needsReviewCount,
448
+ }, null, 2) + '\n';
449
+ process.stdout.write(output);
450
+ setFinalizedResult({
451
+ accepted: acceptedCount,
452
+ rejected: rejectedCount,
453
+ excluded: needsReviewCount,
454
+ });
455
+ }
456
+ catch (err) {
457
+ setSaveError(String(err));
458
+ }
459
+ };
460
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TopBar, { subcommand: "analyze select", hints: hints }), showHelp && _jsx(HelpOverlay, { mode: "review", onClose: () => setShowHelp(false) }), showFinalizeDialog && (_jsx(FinalizeDialog, { accepted: acceptedCount, rejected: rejectedCount, needsReview: needsReviewCount, onConfirm: () => {
461
+ void handleFinalize();
462
+ }, onCancel: () => setShowFinalizeDialog(false) })), showQuitDialog && (_jsx(QuitDialog, { hasUnsavedDrafts: Object.keys(draftsByComponentId).length > 0, onConfirm: async () => {
463
+ if (paths) {
464
+ await appendEvent({
465
+ type: 'session_quit',
466
+ payload: { reason: 'user_quit' },
467
+ });
468
+ }
469
+ process.exit(1);
470
+ }, onCancel: () => setShowQuitDialog(false) })), !showHelp && !showFinalizeDialog && !showQuitDialog && (_jsxs(Box, { flexGrow: 1, children: [_jsx(Sidebar, { components: sessionSummary, selectedId: selectedId, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: visibleCount, onSelect: (id) => {
471
+ setSelectedId(id);
472
+ setJsonScrollOffset(0);
473
+ }, onScrollChange: setSidebarScrollOffset, collapsed: collapsed, width: sidebarWidth }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: selectedDetail ? (_jsx(ComponentDetail, { component: selectedDetail, sourceCode: selectedRecord?.sourceCode ?? null, draftValue: selectedId ? (draftsByComponentId[selectedId] ?? '') : '', editMode: editMode, sourceVisible: sourceVisible, jsonScrollOffset: jsonScrollOffset, sourceScrollX: 0, sourceScrollY: 0, terminalWidth: terminalWidth, previewAnnotation: selectedRecord ? previewAnnotations[selectedRecord.name] : undefined, onDraftChange: (value) => {
474
+ if (!selectedId)
475
+ return;
476
+ setDraftsByComponentId((prev) => ({
477
+ ...prev,
478
+ [selectedId]: value,
479
+ }));
480
+ }, onSaveDraft: () => {
481
+ void handleDraftSave();
482
+ }, onDiscardDraft: handleDraftDiscard, onScrollChange: setJsonScrollOffset })) : (_jsx(Text, { dimColor: true, children: "No component selected" })) })] })), _jsx(PreviewSummaryBar, { preview: previewResponse, loading: previewLoading }), previewError && _jsx(Text, { color: "yellow", children: '⚠ Preview: ' + previewError }), saveError && _jsx(Text, { color: "red", children: '⚠ ' + saveError }), !dialogOpen && (_jsx(StatusBar, { accepted: acceptedCount, rejected: rejectedCount, reviewed: reviewedCount, needsReview: needsReviewCount, onApproveAll: () => { }, onFinalize: () => setShowFinalizeDialog(true) }))] }));
483
+ }
484
+ function FinalizedScreen({ result, }) {
485
+ useImmediateInput((_input, key) => {
486
+ if (key.return || _input === 'q' || key.escape) {
487
+ process.exit(0);
488
+ }
489
+ });
490
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, gap: 1, children: [_jsx(Text, { bold: true, color: "green", children: "\u2713 Finalized" }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { children: [result.accepted, " accepted"] })] }), result.rejected > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "red", children: "\u2717" }), _jsxs(Text, { children: [result.rejected, " rejected"] })] })), result.excluded > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { dimColor: true, children: [result.excluded, " excluded (unresolved)"] })] }))] }), _jsx(Text, { dimColor: true, children: "Decisions saved. Ready for the next step." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[Enter / q] Exit" }) })] }));
491
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import type { PreviewAnnotation, ReviewComponentDetail } from '../../types.js';
3
+ type ComponentDetailProps = {
4
+ component: ReviewComponentDetail;
5
+ sourceCode: string | null;
6
+ draftValue: string;
7
+ editMode: boolean;
8
+ sourceVisible: boolean;
9
+ jsonScrollOffset: number;
10
+ sourceScrollX: number;
11
+ sourceScrollY: number;
12
+ terminalWidth: number;
13
+ previewAnnotation?: PreviewAnnotation;
14
+ onDraftChange: (value: string) => void;
15
+ onSaveDraft: () => void;
16
+ onDiscardDraft: () => void;
17
+ onScrollChange: (offset: number) => void;
18
+ };
19
+ export declare function ComponentDetail({ component, sourceCode, draftValue, editMode, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, onDraftChange, onSaveDraft, onDiscardDraft, }: ComponentDetailProps): React.ReactElement;
20
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { JsonPanel } from './JsonPanel.js';
4
+ import { FieldEditor } from './FieldEditor.js';
5
+ import { SourcePanel } from './SourcePanel.js';
6
+ function annotationLabel(annotation) {
7
+ switch (annotation) {
8
+ case 'breaking':
9
+ return { text: ' ⚠ breaking', color: 'red' };
10
+ case 'changed':
11
+ return { text: ' ~ changed', color: 'yellow' };
12
+ case 'new':
13
+ return { text: ' + new', color: 'green' };
14
+ case 'removed':
15
+ return { text: ' ✗ removed', color: 'red' };
16
+ default:
17
+ return null;
18
+ }
19
+ }
20
+ export function ComponentDetail({ component, sourceCode, draftValue, editMode, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, onDraftChange, onSaveDraft, onDiscardDraft, }) {
21
+ const sidebarWidth = terminalWidth < 80 ? 5 : 20;
22
+ const availableWidth = terminalWidth - sidebarWidth - 2;
23
+ const panelHeight = 20;
24
+ let originalWidth;
25
+ let editWidth;
26
+ let sourceWidth;
27
+ if (sourceVisible && terminalWidth >= 120) {
28
+ originalWidth = Math.floor((availableWidth - 4) / 3);
29
+ editWidth = originalWidth;
30
+ sourceWidth = availableWidth - originalWidth * 2 - 4;
31
+ }
32
+ else {
33
+ originalWidth = Math.floor((availableWidth - 3) / 2);
34
+ editWidth = availableWidth - originalWidth - 3;
35
+ sourceWidth = 0;
36
+ }
37
+ const originalJson = JSON.stringify(component.originalProposal, null, 2);
38
+ const editedJson = JSON.stringify(component.editedProposal, null, 2);
39
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: component.name }), (() => {
40
+ const ann = annotationLabel(previewAnnotation);
41
+ return ann ? _jsx(Text, { color: ann.color, children: ann.text }) : null;
42
+ })(), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: [sourceVisible ? '[s] hide src' : '[s] src', editMode ? '' : ' [e] edit'] })] }), _jsxs(Box, { children: [_jsx(JsonPanel, { label: "ORIGINAL (read-only)", value: originalJson, scrollOffset: jsonScrollOffset, width: originalWidth, height: panelHeight, active: false }), _jsx(Text, { children: " " }), editMode ? (_jsx(FieldEditor, { value: draftValue || editedJson, width: editWidth, height: panelHeight, onChange: onDraftChange, onSave: onSaveDraft, onDiscard: onDiscardDraft })) : (_jsx(JsonPanel, { label: "EDIT (draft)", value: draftValue || editedJson, scrollOffset: jsonScrollOffset, width: editWidth, height: panelHeight, active: true })), sourceVisible && sourceWidth > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(SourcePanel, { sourceCode: sourceCode, filePath: component.originalProposal.source, width: sourceWidth, height: panelHeight, scrollX: sourceScrollX, scrollY: sourceScrollY })] }))] }), !editMode && _jsx(Text, { dimColor: true, children: ' [a] accept [r] reject [e] edit [A] accept all [↑↓] scroll' })] }));
43
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ type FieldEditorProps = {
3
+ value: string;
4
+ width: number;
5
+ height: number;
6
+ onChange: (value: string) => void;
7
+ onSave: () => void;
8
+ onDiscard: () => void;
9
+ };
10
+ export declare function FieldEditor({ value, width, height, onChange, onSave, onDiscard, }: FieldEditorProps): React.ReactElement;
11
+ export {};