@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,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type RunningStepProps = {
|
|
3
|
+
stepNumber: number;
|
|
4
|
+
totalSteps: number;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
detail?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function RunningStep({ stepNumber, totalSteps, title, description, detail, }: RunningStepProps): React.ReactElement;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
5
|
+
export function RunningStep({ stepNumber, totalSteps, title, description, detail, }) {
|
|
6
|
+
const [frame, setFrame] = useState(0);
|
|
7
|
+
const [elapsed, setElapsed] = useState(0);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const spinner = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
|
|
10
|
+
const timer = setInterval(() => setElapsed((s) => s + 1), 1000);
|
|
11
|
+
return () => {
|
|
12
|
+
clearInterval(spinner);
|
|
13
|
+
clearInterval(timer);
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
const mins = Math.floor(elapsed / 60);
|
|
17
|
+
const secs = elapsed % 60;
|
|
18
|
+
const elapsedStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: '─'.repeat(40) }), _jsxs(Box, { gap: 1, children: [_jsxs(Text, { bold: true, children: ["Step ", stepNumber, " of ", totalSteps] }), _jsx(Text, { bold: true, children: "\u2014" }), _jsx(Text, { bold: true, children: title })] }), _jsx(Text, { dimColor: true, children: '─'.repeat(40) })] }), _jsx(Text, { children: description }), _jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsx(Text, { dimColor: true, children: detail ?? 'Running...' })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Elapsed: ", elapsedStr] }) })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type TokenInputStepProps = {
|
|
3
|
+
onConfirm: (rawTokensPath: string) => void;
|
|
4
|
+
onSkip: () => void;
|
|
5
|
+
onQuit: () => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function TokenInputStep({ onConfirm, onSkip, onQuit }: TokenInputStepProps): React.ReactElement;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { statSync } from 'node:fs';
|
|
5
|
+
import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
|
|
6
|
+
import { normalizePath } from '../../path-utils.js';
|
|
7
|
+
export function TokenInputStep({ onConfirm, onSkip, onQuit }) {
|
|
8
|
+
const [inputValue, setInputValue] = useState('');
|
|
9
|
+
const [cursorVisible, setCursorVisible] = useState(true);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const [resolvedPath, setResolvedPath] = useState(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const interval = setInterval(() => setCursorVisible((v) => !v), 500);
|
|
14
|
+
return () => clearInterval(interval);
|
|
15
|
+
}, []);
|
|
16
|
+
useImmediateInput((input, key) => {
|
|
17
|
+
if (key.return) {
|
|
18
|
+
const trimmed = inputValue.trim();
|
|
19
|
+
if (!trimmed) {
|
|
20
|
+
onSkip();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const normalized = normalizePath(trimmed);
|
|
24
|
+
let stat;
|
|
25
|
+
try {
|
|
26
|
+
stat = statSync(normalized);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
setError(`Path not found: ${normalized}`);
|
|
30
|
+
setResolvedPath(normalized);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (stat.isDirectory()) {
|
|
34
|
+
setError(`That's a directory — provide a path to a token file (e.g. tokens.json), not a folder.`);
|
|
35
|
+
setResolvedPath(normalized);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!stat.isFile()) {
|
|
39
|
+
setError(`Not a regular file: ${normalized}`);
|
|
40
|
+
setResolvedPath(normalized);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setError(null);
|
|
44
|
+
setResolvedPath(normalized);
|
|
45
|
+
onConfirm(normalized);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (input === 's') {
|
|
49
|
+
onSkip();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (key.escape || input === 'q') {
|
|
53
|
+
onQuit();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key.backspace || key.delete) {
|
|
57
|
+
setInputValue((v) => v.slice(0, -1));
|
|
58
|
+
setError(null);
|
|
59
|
+
setResolvedPath(null);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (input && !key.ctrl && !key.meta) {
|
|
63
|
+
setInputValue((v) => v + input);
|
|
64
|
+
setError(null);
|
|
65
|
+
setResolvedPath(null);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const displayValue = inputValue + (cursorVisible ? '█' : ' ');
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Design tokens" }), _jsx(Text, { dimColor: true, children: "Point me to your raw token file (e.g. ~/design-tokens/tokens.json). You can use ~, relative, or absolute paths. Claude will map it to DTCG format." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, gap: 0, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "?" }), _jsx(Text, { children: "Token path (file or directory):" }), _jsx(Text, { children: displayValue })] }), resolvedPath && resolvedPath !== inputValue.trim() && !error && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2192 ", resolvedPath] }) })), error && (_jsxs(Box, { marginTop: 0, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["\u2717 ", error] }), resolvedPath && _jsxs(Text, { dimColor: true, children: [" Resolved to: ", resolvedPath] })] }))] }), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "[Enter] Confirm path" }), _jsx(Text, { dimColor: true, children: "[s] Skip tokens" }), _jsx(Text, { dimColor: true, children: "[q] Quit" })] })] }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
|
|
5
|
+
export function WelcomeStep({ onContinue, onQuit }) {
|
|
6
|
+
const [inputValue, setInputValue] = useState('');
|
|
7
|
+
const [cursorVisible, setCursorVisible] = useState(true);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const interval = setInterval(() => setCursorVisible((v) => !v), 500);
|
|
10
|
+
return () => clearInterval(interval);
|
|
11
|
+
}, []);
|
|
12
|
+
useImmediateInput((input, key) => {
|
|
13
|
+
if (key.return) {
|
|
14
|
+
const trimmed = inputValue.trim();
|
|
15
|
+
if (trimmed)
|
|
16
|
+
onContinue(trimmed);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (key.escape || input === 'q') {
|
|
20
|
+
onQuit();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (key.backspace || key.delete) {
|
|
24
|
+
setInputValue((v) => v.slice(0, -1));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (input && !key.ctrl && !key.meta) {
|
|
28
|
+
setInputValue((v) => v + input);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const displayValue = inputValue + (cursorVisible ? '█' : ' ');
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "green", children: "\uD83D\uDC4B Hey! Let's import your design system into Contentful." }), _jsx(Text, { dimColor: true, children: "I'll walk you through 5 steps to get your components into Contentful ExO." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, gap: 0, children: [_jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "Step 1" }), _jsx(Text, { dimColor: true, children: "Extract components from your codebase" })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "Step 2" }), _jsx(Text, { dimColor: true, children: "Review what was extracted" })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "Step 3" }), _jsx(Text, { dimColor: true, children: "Generate CDF definitions with Claude" })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "Step 4" }), _jsx(Text, { dimColor: true, children: "Review generated definitions" })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "Step 5" }), _jsx(Text, { dimColor: true, children: "Push to Contentful" })] }), _jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, gap: 0, children: [_jsx(Text, { children: "Where is your component library?" }), _jsxs(Box, { gap: 1, marginTop: 0, children: [_jsx(Text, { color: "cyan", children: "?" }), _jsx(Text, { children: "Project path:" }), _jsx(Text, { children: displayValue })] })] }), _jsxs(Box, { marginTop: 1, gap: 3, children: [_jsx(Text, { dimColor: true, children: "[Enter] Continue" }), _jsx(Text, { dimColor: true, children: "[q] Quit" })] })] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
|
|
3
|
+
type WizardPreviewStepProps = {
|
|
4
|
+
preview: ServerPreviewResponse;
|
|
5
|
+
spaceId: string;
|
|
6
|
+
environmentId: string;
|
|
7
|
+
stepNumber: number;
|
|
8
|
+
totalSteps: number;
|
|
9
|
+
onConfirm: (acknowledge: boolean) => void;
|
|
10
|
+
onEdit?: () => void;
|
|
11
|
+
onSaveFiles?: () => void;
|
|
12
|
+
onQuit: () => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber, totalSteps, onConfirm, onEdit, onSaveFiles, onQuit, }: WizardPreviewStepProps): React.ReactElement;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
|
+
import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
|
|
5
|
+
import { hasBreakingChangesWithImpact } from '../../../apply/manifest.js';
|
|
6
|
+
import { computeComponentDiffLines } from './preview-diff.js';
|
|
7
|
+
export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber, totalSteps, onConfirm, onEdit, onSaveFiles, onQuit, }) {
|
|
8
|
+
const breakingWithImpact = hasBreakingChangesWithImpact(preview);
|
|
9
|
+
const [diffExpanded, setDiffExpanded] = useState(false);
|
|
10
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
11
|
+
const { stdout } = useStdout();
|
|
12
|
+
const terminalRows = stdout?.rows ?? 40;
|
|
13
|
+
const viewportHeight = Math.max(terminalRows - 14, 10);
|
|
14
|
+
const allDiffLines = useMemo(() => {
|
|
15
|
+
if (!diffExpanded)
|
|
16
|
+
return [];
|
|
17
|
+
const lines = [];
|
|
18
|
+
const { components, tokens } = preview;
|
|
19
|
+
for (const item of components.new) {
|
|
20
|
+
const name = item.key ??
|
|
21
|
+
item.$name ??
|
|
22
|
+
'unknown';
|
|
23
|
+
lines.push({ key: `comp-new-${name}`, element: _jsxs(Text, { color: "green", children: [" + ", name] }) });
|
|
24
|
+
}
|
|
25
|
+
for (const item of components.removed) {
|
|
26
|
+
lines.push({ key: `comp-rm-${item.name}`, element: _jsxs(Text, { color: "red", children: [" - ", item.name] }) });
|
|
27
|
+
}
|
|
28
|
+
for (const item of components.changed) {
|
|
29
|
+
lines.push({
|
|
30
|
+
key: `comp-h-${item.current.name}`,
|
|
31
|
+
element: (_jsxs(Text, { color: "yellow", children: [' ', "~ ", item.current.name, item.hasPendingDraftChanges ? _jsx(Text, { color: "yellow", children: " \u26A1 has pending draft changes" }) : null] })),
|
|
32
|
+
});
|
|
33
|
+
if (item.changeClassification?.classification === 'breaking') {
|
|
34
|
+
const reasons = item.changeClassification.breakingChanges
|
|
35
|
+
.map((bc) => `${bc.propertyId}: ${bc.reason}`)
|
|
36
|
+
.join(', ');
|
|
37
|
+
lines.push({ key: `comp-b-${item.current.name}`, element: _jsxs(Text, { color: "red", children: [" \u26A0 BREAKING: ", reasons] }) });
|
|
38
|
+
}
|
|
39
|
+
const diffLines = computeComponentDiffLines(item.current, item.proposed, item.changeClassification);
|
|
40
|
+
for (const d of diffLines) {
|
|
41
|
+
lines.push({ key: `comp-d-${item.current.name}-${d.key}`, element: _jsxs(Text, { color: d.color, children: [" ", d.text] }) });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const item of tokens.new) {
|
|
45
|
+
const name = item.name ??
|
|
46
|
+
item.path ??
|
|
47
|
+
'unknown';
|
|
48
|
+
lines.push({ key: `tok-new-${name}`, element: _jsxs(Text, { color: "green", children: [" + ", name] }) });
|
|
49
|
+
}
|
|
50
|
+
for (const item of tokens.removed) {
|
|
51
|
+
lines.push({ key: `tok-rm-${item.name}`, element: _jsxs(Text, { color: "red", children: [" - ", item.name] }) });
|
|
52
|
+
}
|
|
53
|
+
for (const item of tokens.changed) {
|
|
54
|
+
const tokenName = item.current.name;
|
|
55
|
+
lines.push({
|
|
56
|
+
key: `tok-h-${tokenName}`,
|
|
57
|
+
element: (_jsxs(Text, { color: "yellow", children: [' ', "~ ", tokenName, item.hasPendingDraftChanges ? _jsx(Text, { color: "yellow", children: " \u26A1 has pending draft changes" }) : null] })),
|
|
58
|
+
});
|
|
59
|
+
if (item.changeClassification?.classification === 'breaking') {
|
|
60
|
+
const reasons = item.changeClassification.breakingChanges
|
|
61
|
+
.map((bc) => `${bc.propertyId}: ${bc.reason}`)
|
|
62
|
+
.join(', ');
|
|
63
|
+
lines.push({ key: `tok-b-${tokenName}`, element: _jsxs(Text, { color: "red", children: [" \u26A0 BREAKING: ", reasons] }) });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return lines;
|
|
67
|
+
}, [diffExpanded, preview]);
|
|
68
|
+
const maxScroll = Math.max(0, allDiffLines.length - viewportHeight);
|
|
69
|
+
useImmediateInput((input, key) => {
|
|
70
|
+
if (key.return) {
|
|
71
|
+
onConfirm(breakingWithImpact);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (input === 'd' || input === 'D') {
|
|
75
|
+
setDiffExpanded((prev) => !prev);
|
|
76
|
+
setScrollOffset(0);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (diffExpanded) {
|
|
80
|
+
if (input === 'j' || key.downArrow) {
|
|
81
|
+
setScrollOffset((prev) => Math.min(prev + 1, maxScroll));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (input === 'k' || key.upArrow) {
|
|
85
|
+
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (input === 'f') {
|
|
89
|
+
setScrollOffset((prev) => Math.min(prev + viewportHeight, maxScroll));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (input === 'b') {
|
|
93
|
+
setScrollOffset((prev) => Math.max(prev - viewportHeight, 0));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if ((input === 'e' || input === 'E') && onEdit) {
|
|
98
|
+
onEdit();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (input === 's' && onSaveFiles) {
|
|
102
|
+
onSaveFiles();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (input === 'q' || key.escape) {
|
|
106
|
+
onQuit();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const { components, tokens } = preview;
|
|
111
|
+
const hasComponents = components.new.length + components.changed.length + components.removed.length > 0;
|
|
112
|
+
const hasTokens = tokens.new.length + tokens.changed.length + tokens.removed.length > 0;
|
|
113
|
+
const hasAnything = hasComponents || hasTokens;
|
|
114
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: '─'.repeat(40) }), _jsxs(Box, { gap: 1, children: [_jsxs(Text, { bold: true, children: ["Step ", stepNumber, " of ", totalSteps] }), _jsx(Text, { bold: true, children: "\u2014" }), _jsx(Text, { bold: true, children: "Push to Contentful" })] }), _jsx(Text, { dimColor: true, children: '─'.repeat(40) })] }), hasAnything ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Here's what will happen in your space:" }), hasComponents && (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Box, { gap: 1, marginTop: 1, children: _jsx(Text, { bold: true, dimColor: true, children: "ComponentTypes" }) }), components.new.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", children: " \uFF0B" }), _jsxs(Text, { children: [components.new.length, " will be created"] })] }), components.new.map((item, i) => {
|
|
115
|
+
const name = item.key ?? item.$name ?? 'unknown';
|
|
116
|
+
return (_jsxs(Text, { color: "green", children: [' ', "+ ", name] }, `new-${i}`));
|
|
117
|
+
})] })), components.changed.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "yellow", children: " \uFF5E" }), _jsxs(Text, { children: [components.changed.length, " will be updated"] })] }), components.changed.map((item, i) => {
|
|
118
|
+
const isBreaking = item.changeClassification?.classification === 'breaking';
|
|
119
|
+
return (_jsxs(Text, { color: isBreaking ? 'red' : 'yellow', children: [' ', isBreaking ? '⚠' : '~', " ", item.current.name] }, `chg-${i}`));
|
|
120
|
+
})] })), components.removed.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "red", children: " \u2717" }), _jsxs(Text, { children: [components.removed.length, " will be removed"] })] }), components.removed.map((item, i) => (_jsxs(Text, { color: "red", children: [' ', "\u2717 ", item.name] }, `rm-${i}`)))] })), components.unchanged.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: " \u00B7" }), _jsxs(Text, { dimColor: true, children: [components.unchanged.length, " unchanged"] })] }))] })), hasTokens && (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Box, { gap: 1, marginTop: 1, children: _jsx(Text, { bold: true, dimColor: true, children: "Design Tokens" }) }), tokens.new.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", children: " \uFF0B" }), _jsxs(Text, { children: [tokens.new.length, " will be created"] })] })), tokens.changed.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "yellow", children: " \uFF5E" }), _jsxs(Text, { children: [tokens.changed.length, " will be updated"] })] })), tokens.removed.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "red", children: " \u2717" }), _jsxs(Text, { children: [tokens.removed.length, " will be removed"] })] })), tokens.unchanged.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: " \u00B7" }), _jsxs(Text, { dimColor: true, children: [tokens.unchanged.length, " unchanged"] })] }))] })), diffExpanded && allDiffLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '─'.repeat(40) }), _jsxs(Text, { dimColor: true, children: [' ', "Diff (", allDiffLines.length, " lines) \u2014 line ", scrollOffset + 1, "\u2013", Math.min(scrollOffset + viewportHeight, allDiffLines.length), " of ", allDiffLines.length] }), _jsx(Box, { flexDirection: "column", children: allDiffLines.slice(scrollOffset, scrollOffset + viewportHeight).map((line) => (_jsx(Box, { children: line.element }, line.key))) }), maxScroll > 0 && _jsx(Text, { dimColor: true, children: " \u2195 j/k to scroll, f/b to page" })] }))] })) : (_jsx(Text, { dimColor: true, children: "Nothing to push \u2014 everything is already up to date." })), _jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Space:" }), _jsx(Text, { children: spaceId }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { dimColor: true, children: "Environment:" }), _jsx(Text, { children: environmentId })] }), breakingWithImpact && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", bold: true, children: "\u26A0 Breaking changes will affect downstream entities. Press Enter to acknowledge and apply." }) })), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "[Enter] Push to Contentful" }), _jsxs(Text, { dimColor: true, children: ["[d] ", diffExpanded ? 'Hide' : 'Show', " diff"] }), diffExpanded && _jsx(Text, { dimColor: true, children: "[j/k] Scroll [f/b] Page" }), onEdit && _jsx(Text, { dimColor: true, children: "[e] Edit definitions" }), onSaveFiles && _jsx(Text, { dimColor: true, children: "[s] Save files instead" }), _jsx(Text, { dimColor: true, children: "[q] Cancel" })] })] }));
|
|
121
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChangeClassification, ComponentTypeSummary, PropertySummary } from '@contentful/experience-design-system-types';
|
|
2
|
+
export interface PropertyDiffLine {
|
|
3
|
+
key: string;
|
|
4
|
+
color: 'green' | 'red' | 'yellow';
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function computeComponentDiffLines(current: ComponentTypeSummary, proposed: Record<string, unknown>, changeClassification?: ChangeClassification): PropertyDiffLine[];
|
|
8
|
+
export declare function diffProperty(name: string, old: PropertySummary, newDef: Record<string, unknown>): PropertyDiffLine[];
|
|
9
|
+
export declare function formatOldProp(prop: PropertySummary): string;
|
|
10
|
+
export declare function formatPropDef(def: Record<string, unknown>): string;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export function computeComponentDiffLines(current, proposed, changeClassification) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
// Use fullProperties keys as authoritative server state (includes state props, not just content+design)
|
|
4
|
+
const currentProps = current.fullProperties
|
|
5
|
+
? new Set(Object.keys(current.fullProperties))
|
|
6
|
+
: new Set([...current.contentProperties, ...current.designProperties]);
|
|
7
|
+
const proposedProperties = (proposed['$properties'] ?? {});
|
|
8
|
+
const proposedProps = new Set(Object.keys(proposedProperties));
|
|
9
|
+
const oldProps = current.fullProperties;
|
|
10
|
+
// Added props
|
|
11
|
+
for (const name of [...proposedProps].sort()) {
|
|
12
|
+
if (!currentProps.has(name)) {
|
|
13
|
+
const def = proposedProperties[name];
|
|
14
|
+
lines.push({ key: `${name}-add`, color: 'green', text: `+ ${name}: ${formatPropDef(def)}` });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Removed props
|
|
18
|
+
for (const name of [...currentProps].sort()) {
|
|
19
|
+
if (!proposedProps.has(name)) {
|
|
20
|
+
if (oldProps?.[name]) {
|
|
21
|
+
lines.push({ key: `${name}-rm`, color: 'red', text: `- ${name}: ${formatOldProp(oldProps[name])}` });
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const cat = current.contentProperties.includes(name) ? 'content' : 'design';
|
|
25
|
+
lines.push({ key: `${name}-rm`, color: 'red', text: `- ${name} (${cat})` });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Modified props — compare old vs new definitions
|
|
30
|
+
const handledProps = new Set();
|
|
31
|
+
for (const name of [...currentProps].sort()) {
|
|
32
|
+
if (!proposedProps.has(name))
|
|
33
|
+
continue;
|
|
34
|
+
const newDef = proposedProperties[name];
|
|
35
|
+
const oldDef = oldProps?.[name];
|
|
36
|
+
if (!oldDef)
|
|
37
|
+
continue;
|
|
38
|
+
const diffs = diffProperty(name, oldDef, newDef);
|
|
39
|
+
if (diffs.length > 0) {
|
|
40
|
+
handledProps.add(name);
|
|
41
|
+
for (const d of diffs)
|
|
42
|
+
lines.push(d);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// If no old definitions available, fall back to breaking change reasons
|
|
46
|
+
if (!oldProps && changeClassification?.breakingChanges) {
|
|
47
|
+
for (const bc of changeClassification.breakingChanges) {
|
|
48
|
+
if (!currentProps.has(bc.propertyId) || !proposedProps.has(bc.propertyId))
|
|
49
|
+
continue;
|
|
50
|
+
if (handledProps.has(bc.propertyId))
|
|
51
|
+
continue;
|
|
52
|
+
handledProps.add(bc.propertyId);
|
|
53
|
+
const def = proposedProperties[bc.propertyId];
|
|
54
|
+
lines.push({ key: `${bc.propertyId}-old`, color: 'red', text: `- ${bc.propertyId}: ${bc.reason}` });
|
|
55
|
+
lines.push({ key: `${bc.propertyId}-new`, color: 'green', text: `+ ${bc.propertyId}: ${formatPropDef(def)}` });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Slot diffs
|
|
59
|
+
const currentSlots = new Set(current.slots);
|
|
60
|
+
const proposedSlots = (proposed['$slots'] ?? {});
|
|
61
|
+
const proposedSlotNames = new Set(Object.keys(proposedSlots));
|
|
62
|
+
for (const name of [...proposedSlotNames].sort()) {
|
|
63
|
+
if (!currentSlots.has(name)) {
|
|
64
|
+
lines.push({ key: `slot-${name}-add`, color: 'green', text: `+ slot: ${name}` });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const name of [...currentSlots].sort()) {
|
|
68
|
+
if (!proposedSlotNames.has(name)) {
|
|
69
|
+
lines.push({ key: `slot-${name}-rm`, color: 'red', text: `- slot: ${name}` });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
function normalizeType(type) {
|
|
75
|
+
switch (type.toLowerCase()) {
|
|
76
|
+
case 'boolean':
|
|
77
|
+
return 'boolean';
|
|
78
|
+
case 'number':
|
|
79
|
+
return 'number';
|
|
80
|
+
case 'symbol':
|
|
81
|
+
case 'enum':
|
|
82
|
+
return 'symbol';
|
|
83
|
+
case 'string':
|
|
84
|
+
case 'text':
|
|
85
|
+
case 'richtext':
|
|
86
|
+
case 'media':
|
|
87
|
+
return 'string';
|
|
88
|
+
default:
|
|
89
|
+
return type.toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function diffProperty(name, old, newDef) {
|
|
93
|
+
const diffs = [];
|
|
94
|
+
const newType = newDef['$type'] ?? '';
|
|
95
|
+
const newCategory = newDef['$category'] ?? '';
|
|
96
|
+
const newRequired = newDef['$required'] === true;
|
|
97
|
+
const newDefault = newDef['$default'];
|
|
98
|
+
const typeChanged = old.type !== '' && newType !== '' && normalizeType(old.type) !== normalizeType(newType);
|
|
99
|
+
const categoryChanged = old.category !== newCategory;
|
|
100
|
+
const requiredChanged = old.required !== newRequired;
|
|
101
|
+
const defaultChanged = JSON.stringify(old.default) !== JSON.stringify(newDefault);
|
|
102
|
+
if (!typeChanged && !categoryChanged && !requiredChanged && !defaultChanged)
|
|
103
|
+
return [];
|
|
104
|
+
diffs.push({ key: `${name}-old`, color: 'red', text: `- ${name}: ${formatOldProp(old)}` });
|
|
105
|
+
diffs.push({ key: `${name}-new`, color: 'green', text: `+ ${name}: ${formatPropDef(newDef)}` });
|
|
106
|
+
return diffs;
|
|
107
|
+
}
|
|
108
|
+
export function formatOldProp(prop) {
|
|
109
|
+
const parts = [];
|
|
110
|
+
if (prop.type)
|
|
111
|
+
parts.push(prop.type);
|
|
112
|
+
if (prop.category)
|
|
113
|
+
parts.push(prop.category);
|
|
114
|
+
parts.push(prop.required ? 'required' : 'optional');
|
|
115
|
+
if (prop.default !== undefined)
|
|
116
|
+
parts.push(`default=${JSON.stringify(prop.default)}`);
|
|
117
|
+
return parts.join(', ');
|
|
118
|
+
}
|
|
119
|
+
export function formatPropDef(def) {
|
|
120
|
+
const parts = [];
|
|
121
|
+
if (def['$type'])
|
|
122
|
+
parts.push(def['$type']);
|
|
123
|
+
if (def['$category'])
|
|
124
|
+
parts.push(def['$category']);
|
|
125
|
+
if (def['$required'] === true)
|
|
126
|
+
parts.push('required');
|
|
127
|
+
else
|
|
128
|
+
parts.push('optional');
|
|
129
|
+
if (def['$default'] !== undefined)
|
|
130
|
+
parts.push(`default=${JSON.stringify(def['$default'])}`);
|
|
131
|
+
return parts.join(', ');
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const c: {
|
|
2
|
+
green: (s: string) => string;
|
|
3
|
+
red: (s: string) => string;
|
|
4
|
+
cyan: (s: string) => string;
|
|
5
|
+
yellow: (s: string) => string;
|
|
6
|
+
dim: (s: string) => string;
|
|
7
|
+
bold: (s: string) => string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Processes streaming agent output line by line.
|
|
11
|
+
*
|
|
12
|
+
* Tool-call JSON lines are translated into human-readable summaries.
|
|
13
|
+
* Prose (reasoning text) is shown only when verbose=true.
|
|
14
|
+
*/
|
|
15
|
+
export declare class OutputFormatter {
|
|
16
|
+
private _buf;
|
|
17
|
+
private _verbose;
|
|
18
|
+
private _write;
|
|
19
|
+
constructor(verbose: boolean, write?: (s: string) => void);
|
|
20
|
+
push(chunk: string): void;
|
|
21
|
+
flush(): void;
|
|
22
|
+
private _processLine;
|
|
23
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Color support — checked once at module load.
|
|
2
|
+
// Honors NO_COLOR (https://no-color.org) and FORCE_COLOR (set by orchestrator for non-TTY subprocesses).
|
|
3
|
+
function detectColors() {
|
|
4
|
+
if (process.env['NO_COLOR'] !== undefined)
|
|
5
|
+
return false;
|
|
6
|
+
if (process.env['FORCE_COLOR'] !== undefined)
|
|
7
|
+
return true;
|
|
8
|
+
return process.stderr.isTTY === true;
|
|
9
|
+
}
|
|
10
|
+
const col = detectColors();
|
|
11
|
+
export const c = {
|
|
12
|
+
green: (s) => (col ? `\x1b[32m${s}\x1b[0m` : s),
|
|
13
|
+
red: (s) => (col ? `\x1b[31m${s}\x1b[0m` : s),
|
|
14
|
+
cyan: (s) => (col ? `\x1b[36m${s}\x1b[0m` : s),
|
|
15
|
+
yellow: (s) => (col ? `\x1b[33m${s}\x1b[0m` : s),
|
|
16
|
+
dim: (s) => (col ? `\x1b[2m${s}\x1b[0m` : s),
|
|
17
|
+
bold: (s) => (col ? `\x1b[1m${s}\x1b[0m` : s),
|
|
18
|
+
};
|
|
19
|
+
// Formats a single parsed tool-call object into a human-readable line, or returns null to suppress it.
|
|
20
|
+
function formatToolCall(obj) {
|
|
21
|
+
switch (obj['tool']) {
|
|
22
|
+
case 'classify_component': {
|
|
23
|
+
const desc = typeof obj['description'] === 'string' ? obj['description'] : null;
|
|
24
|
+
return desc ? ` ${c.dim('→')} ${c.dim(desc)}` : null;
|
|
25
|
+
}
|
|
26
|
+
case 'classify_prop': {
|
|
27
|
+
const prop = String(obj['prop'] ?? '');
|
|
28
|
+
const type = String(obj['cdf_type'] ?? '');
|
|
29
|
+
const cat = String(obj['cdf_category'] ?? '');
|
|
30
|
+
return ` ${c.green('+')} ${prop} ${c.dim(`${type} ${cat}`)}`;
|
|
31
|
+
}
|
|
32
|
+
case 'exclude_prop': {
|
|
33
|
+
const prop = String(obj['prop'] ?? '');
|
|
34
|
+
const reason = typeof obj['reason'] === 'string' ? obj['reason'] : '';
|
|
35
|
+
return ` ${c.dim('–')} ${prop} ${c.dim(reason)}`;
|
|
36
|
+
}
|
|
37
|
+
case 'classify_slot': {
|
|
38
|
+
const slot = String(obj['slot'] ?? '');
|
|
39
|
+
const desc = typeof obj['description'] === 'string' ? obj['description'] : '';
|
|
40
|
+
const req = obj['required'] === true ? c.dim(' required') : '';
|
|
41
|
+
return ` ${c.cyan('◈')} ${slot}${req} ${c.dim(desc)}`;
|
|
42
|
+
}
|
|
43
|
+
case 'select_component': {
|
|
44
|
+
const name = String(obj['name'] ?? '');
|
|
45
|
+
const reason = typeof obj['reason'] === 'string' ? obj['reason'] : '';
|
|
46
|
+
return ` ${c.green('+')} ${name} ${c.dim(reason)}`;
|
|
47
|
+
}
|
|
48
|
+
case 'reject_component': {
|
|
49
|
+
const name = String(obj['name'] ?? '');
|
|
50
|
+
const reason = typeof obj['reason'] === 'string' ? obj['reason'] : '';
|
|
51
|
+
return ` ${c.dim('–')} ${name} ${c.dim(reason)}`;
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Processes streaming agent output line by line.
|
|
59
|
+
*
|
|
60
|
+
* Tool-call JSON lines are translated into human-readable summaries.
|
|
61
|
+
* Prose (reasoning text) is shown only when verbose=true.
|
|
62
|
+
*/
|
|
63
|
+
export class OutputFormatter {
|
|
64
|
+
_buf = '';
|
|
65
|
+
_verbose;
|
|
66
|
+
_write;
|
|
67
|
+
constructor(verbose, write = (s) => process.stderr.write(s)) {
|
|
68
|
+
this._verbose = verbose;
|
|
69
|
+
this._write = write;
|
|
70
|
+
}
|
|
71
|
+
push(chunk) {
|
|
72
|
+
this._buf += chunk;
|
|
73
|
+
// Process all complete lines; keep any trailing partial line in the buffer.
|
|
74
|
+
const nl = this._buf.lastIndexOf('\n');
|
|
75
|
+
if (nl === -1)
|
|
76
|
+
return;
|
|
77
|
+
const complete = this._buf.slice(0, nl + 1);
|
|
78
|
+
this._buf = this._buf.slice(nl + 1);
|
|
79
|
+
for (const line of complete.split('\n')) {
|
|
80
|
+
this._processLine(line);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
flush() {
|
|
84
|
+
if (this._buf.trim())
|
|
85
|
+
this._processLine(this._buf);
|
|
86
|
+
this._buf = '';
|
|
87
|
+
}
|
|
88
|
+
_processLine(line) {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (!trimmed)
|
|
91
|
+
return;
|
|
92
|
+
if (trimmed.startsWith('{')) {
|
|
93
|
+
try {
|
|
94
|
+
const obj = JSON.parse(trimmed);
|
|
95
|
+
if (typeof obj['tool'] === 'string') {
|
|
96
|
+
const formatted = formatToolCall(obj);
|
|
97
|
+
if (formatted !== null)
|
|
98
|
+
this._write(formatted + '\n');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// not JSON — fall through to prose handling
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (this._verbose) {
|
|
107
|
+
this._write(c.dim(' ' + trimmed) + '\n');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|