@canonical/summon 0.1.0
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 +439 -0
- package/generators/example/hello/index.ts +132 -0
- package/generators/example/hello/templates/README.md.ejs +20 -0
- package/generators/example/hello/templates/index.ts.ejs +9 -0
- package/generators/example/webapp/index.ts +509 -0
- package/generators/example/webapp/templates/ARCHITECTURE.md.ejs +180 -0
- package/generators/example/webapp/templates/App.tsx.ejs +86 -0
- package/generators/example/webapp/templates/README.md.ejs +154 -0
- package/generators/example/webapp/templates/app.test.ts.ejs +63 -0
- package/generators/example/webapp/templates/app.ts.ejs +132 -0
- package/generators/example/webapp/templates/feature.ts.ejs +264 -0
- package/generators/example/webapp/templates/index.html.ejs +20 -0
- package/generators/example/webapp/templates/main.tsx.ejs +43 -0
- package/generators/example/webapp/templates/styles.css.ejs +135 -0
- package/generators/init/index.ts +124 -0
- package/generators/init/templates/generator.ts.ejs +85 -0
- package/generators/init/templates/template-index.ts.ejs +9 -0
- package/generators/init/templates/template-test.ts.ejs +8 -0
- package/package.json +64 -0
- package/src/__tests__/combinators.test.ts +895 -0
- package/src/__tests__/dry-run.test.ts +927 -0
- package/src/__tests__/effect.test.ts +816 -0
- package/src/__tests__/interpreter.test.ts +673 -0
- package/src/__tests__/primitives.test.ts +970 -0
- package/src/__tests__/task.test.ts +929 -0
- package/src/__tests__/template.test.ts +666 -0
- package/src/cli-format.ts +165 -0
- package/src/cli-types.ts +53 -0
- package/src/cli.tsx +1322 -0
- package/src/combinators.ts +294 -0
- package/src/completion.ts +488 -0
- package/src/components/App.tsx +960 -0
- package/src/components/ExecutionProgress.tsx +205 -0
- package/src/components/FileTreePreview.tsx +97 -0
- package/src/components/PromptSequence.tsx +483 -0
- package/src/components/Spinner.tsx +36 -0
- package/src/components/index.ts +16 -0
- package/src/dry-run.ts +434 -0
- package/src/effect.ts +224 -0
- package/src/index.ts +266 -0
- package/src/interpreter.ts +463 -0
- package/src/primitives.ts +442 -0
- package/src/task.ts +245 -0
- package/src/template.ts +537 -0
- package/src/types.ts +453 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptSequence Component
|
|
3
|
+
*
|
|
4
|
+
* Handles interactive prompts for generators using React Ink.
|
|
5
|
+
* Features:
|
|
6
|
+
* - Progress indicator (step X of Y)
|
|
7
|
+
* - Navigate back to previous answers
|
|
8
|
+
* - Show completed answers
|
|
9
|
+
* - Group headers
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Box, Text, useInput } from "ink";
|
|
13
|
+
import SelectInput from "ink-select-input";
|
|
14
|
+
import TextInput from "ink-text-input";
|
|
15
|
+
import { useCallback, useEffect, useState } from "react";
|
|
16
|
+
import type { PromptDefinition } from "../types.js";
|
|
17
|
+
|
|
18
|
+
export interface PromptSequenceProps {
|
|
19
|
+
/** List of prompts to display */
|
|
20
|
+
prompts: PromptDefinition[];
|
|
21
|
+
/** Called when all prompts are answered */
|
|
22
|
+
onComplete: (answers: Record<string, unknown>) => void;
|
|
23
|
+
/** Called when user cancels */
|
|
24
|
+
onCancel?: () => void;
|
|
25
|
+
/** Initial answers (for resuming/editing) */
|
|
26
|
+
initialAnswers?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Helper to format answer values for display
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
const formatAnswerValue = (
|
|
34
|
+
value: unknown,
|
|
35
|
+
prompt: PromptDefinition,
|
|
36
|
+
): string => {
|
|
37
|
+
if (value === undefined || value === null) return "";
|
|
38
|
+
|
|
39
|
+
if (prompt.type === "confirm") {
|
|
40
|
+
return value ? "Yes" : "No";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (prompt.type === "select" && prompt.choices) {
|
|
44
|
+
const choice = prompt.choices.find((c) => c.value === value);
|
|
45
|
+
return choice?.label ?? String(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (prompt.type === "multiselect" && Array.isArray(value)) {
|
|
49
|
+
if (value.length === 0) return "None";
|
|
50
|
+
if (prompt.choices) {
|
|
51
|
+
return value
|
|
52
|
+
.map((v) => prompt.choices?.find((c) => c.value === v)?.label ?? v)
|
|
53
|
+
.join(", ");
|
|
54
|
+
}
|
|
55
|
+
return value.join(", ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return String(value);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Completed Answers Table Display
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
interface CompletedAnswersTableProps {
|
|
66
|
+
prompts: PromptDefinition[];
|
|
67
|
+
answers: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Display completed answers in a borderless table format with aligned columns.
|
|
72
|
+
*/
|
|
73
|
+
const CompletedAnswersTable = ({
|
|
74
|
+
prompts,
|
|
75
|
+
answers,
|
|
76
|
+
}: CompletedAnswersTableProps) => {
|
|
77
|
+
if (prompts.length === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Calculate max width for the question column (for alignment)
|
|
82
|
+
const maxQuestionWidth = Math.max(...prompts.map((p) => p.message.length));
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
86
|
+
{prompts.map((prompt) => {
|
|
87
|
+
const value = answers[prompt.name];
|
|
88
|
+
const displayValue = formatAnswerValue(value, prompt);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Box key={prompt.name}>
|
|
92
|
+
<Text color="green">✔ </Text>
|
|
93
|
+
<Text dimColor>{prompt.message.padEnd(maxQuestionWidth)}</Text>
|
|
94
|
+
<Text dimColor> </Text>
|
|
95
|
+
<Text color="cyan">{displayValue}</Text>
|
|
96
|
+
</Box>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
</Box>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// Text Prompt
|
|
105
|
+
// =============================================================================
|
|
106
|
+
|
|
107
|
+
interface TextPromptProps {
|
|
108
|
+
prompt: PromptDefinition;
|
|
109
|
+
initialValue?: string;
|
|
110
|
+
onSubmit: (value: string) => void;
|
|
111
|
+
onBack?: () => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const TextPrompt = ({
|
|
115
|
+
prompt,
|
|
116
|
+
initialValue,
|
|
117
|
+
onSubmit,
|
|
118
|
+
onBack,
|
|
119
|
+
}: TextPromptProps) => {
|
|
120
|
+
const [value, setValue] = useState(
|
|
121
|
+
initialValue ?? String(prompt.default ?? ""),
|
|
122
|
+
);
|
|
123
|
+
const [error, setError] = useState<string | null>(null);
|
|
124
|
+
|
|
125
|
+
// Reset value when prompt changes
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
setValue(initialValue ?? String(prompt.default ?? ""));
|
|
128
|
+
setError(null);
|
|
129
|
+
}, [initialValue, prompt.default]);
|
|
130
|
+
|
|
131
|
+
const handleSubmit = useCallback(
|
|
132
|
+
(val: string) => {
|
|
133
|
+
if (prompt.validate) {
|
|
134
|
+
const result = prompt.validate(val);
|
|
135
|
+
if (result !== true) {
|
|
136
|
+
setError(typeof result === "string" ? result : "Invalid input");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
onSubmit(val);
|
|
141
|
+
},
|
|
142
|
+
[prompt, onSubmit],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Handle back navigation with Escape
|
|
146
|
+
useInput((_input, key) => {
|
|
147
|
+
if (key.escape && onBack) {
|
|
148
|
+
onBack();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Box flexDirection="column">
|
|
154
|
+
<Box>
|
|
155
|
+
<Text color="magenta">› </Text>
|
|
156
|
+
<Text bold>{prompt.message}</Text>
|
|
157
|
+
</Box>
|
|
158
|
+
<Box marginLeft={2}>
|
|
159
|
+
<TextInput value={value} onChange={setValue} onSubmit={handleSubmit} />
|
|
160
|
+
</Box>
|
|
161
|
+
{error && (
|
|
162
|
+
<Box marginLeft={2}>
|
|
163
|
+
<Text color="red">✘ {error}</Text>
|
|
164
|
+
</Box>
|
|
165
|
+
)}
|
|
166
|
+
</Box>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Confirm Prompt
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
interface ConfirmPromptProps {
|
|
175
|
+
prompt: PromptDefinition;
|
|
176
|
+
onSubmit: (value: boolean) => void;
|
|
177
|
+
onBack?: () => void;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const ConfirmPrompt = ({ prompt, onSubmit, onBack }: ConfirmPromptProps) => {
|
|
181
|
+
const defaultValue = Boolean(prompt.default);
|
|
182
|
+
|
|
183
|
+
useInput((input, key) => {
|
|
184
|
+
if (input.toLowerCase() === "y") {
|
|
185
|
+
onSubmit(true);
|
|
186
|
+
} else if (input.toLowerCase() === "n") {
|
|
187
|
+
onSubmit(false);
|
|
188
|
+
} else if (key.return) {
|
|
189
|
+
onSubmit(defaultValue);
|
|
190
|
+
} else if (key.escape && onBack) {
|
|
191
|
+
onBack();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Box>
|
|
199
|
+
<Text color="magenta">› </Text>
|
|
200
|
+
<Text bold>{prompt.message} </Text>
|
|
201
|
+
<Text dimColor>({hint})</Text>
|
|
202
|
+
</Box>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// =============================================================================
|
|
207
|
+
// Select Prompt
|
|
208
|
+
// =============================================================================
|
|
209
|
+
|
|
210
|
+
interface SelectPromptProps {
|
|
211
|
+
prompt: PromptDefinition;
|
|
212
|
+
onSubmit: (value: string) => void;
|
|
213
|
+
onBack?: () => void;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const SelectPrompt = ({ prompt, onSubmit, onBack }: SelectPromptProps) => {
|
|
217
|
+
const items =
|
|
218
|
+
prompt.choices?.map((choice) => ({
|
|
219
|
+
label: choice.label,
|
|
220
|
+
value: choice.value,
|
|
221
|
+
})) ?? [];
|
|
222
|
+
|
|
223
|
+
// Find initial index based on default value
|
|
224
|
+
const initialIndex = prompt.default
|
|
225
|
+
? items.findIndex((item) => item.value === prompt.default)
|
|
226
|
+
: 0;
|
|
227
|
+
|
|
228
|
+
useInput((_input, key) => {
|
|
229
|
+
if (key.escape && onBack) {
|
|
230
|
+
onBack();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<Box flexDirection="column">
|
|
236
|
+
<Box>
|
|
237
|
+
<Text color="magenta">› </Text>
|
|
238
|
+
<Text bold>{prompt.message}</Text>
|
|
239
|
+
<Text dimColor> (↑↓ to select, enter to confirm)</Text>
|
|
240
|
+
</Box>
|
|
241
|
+
<Box marginLeft={2}>
|
|
242
|
+
<SelectInput
|
|
243
|
+
items={items}
|
|
244
|
+
initialIndex={initialIndex >= 0 ? initialIndex : 0}
|
|
245
|
+
onSelect={(item) => onSubmit(item.value)}
|
|
246
|
+
/>
|
|
247
|
+
</Box>
|
|
248
|
+
</Box>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// =============================================================================
|
|
253
|
+
// Multiselect Prompt
|
|
254
|
+
// =============================================================================
|
|
255
|
+
|
|
256
|
+
interface MultiselectPromptProps {
|
|
257
|
+
prompt: PromptDefinition;
|
|
258
|
+
onSubmit: (values: string[]) => void;
|
|
259
|
+
onBack?: () => void;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const MultiselectPrompt = ({
|
|
263
|
+
prompt,
|
|
264
|
+
onSubmit,
|
|
265
|
+
onBack,
|
|
266
|
+
}: MultiselectPromptProps) => {
|
|
267
|
+
const [selected, setSelected] = useState<Set<string>>(
|
|
268
|
+
new Set(prompt.default as string[] | undefined),
|
|
269
|
+
);
|
|
270
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
271
|
+
|
|
272
|
+
const choices = prompt.choices ?? [];
|
|
273
|
+
|
|
274
|
+
useInput((input, key) => {
|
|
275
|
+
if (key.escape && onBack) {
|
|
276
|
+
onBack();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (key.upArrow) {
|
|
280
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : choices.length - 1));
|
|
281
|
+
} else if (key.downArrow) {
|
|
282
|
+
setHighlightedIndex((prev) => (prev < choices.length - 1 ? prev + 1 : 0));
|
|
283
|
+
} else if (input === " ") {
|
|
284
|
+
const choice = choices[highlightedIndex];
|
|
285
|
+
if (choice) {
|
|
286
|
+
setSelected((prev) => {
|
|
287
|
+
const newSet = new Set(prev);
|
|
288
|
+
if (newSet.has(choice.value)) {
|
|
289
|
+
newSet.delete(choice.value);
|
|
290
|
+
} else {
|
|
291
|
+
newSet.add(choice.value);
|
|
292
|
+
}
|
|
293
|
+
return newSet;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
} else if (key.return) {
|
|
297
|
+
onSubmit(Array.from(selected));
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<Box flexDirection="column">
|
|
303
|
+
<Box>
|
|
304
|
+
<Text color="magenta">› </Text>
|
|
305
|
+
<Text bold>{prompt.message}</Text>
|
|
306
|
+
<Text dimColor> (space to toggle, enter to confirm)</Text>
|
|
307
|
+
</Box>
|
|
308
|
+
<Box marginLeft={2} flexDirection="column">
|
|
309
|
+
{choices.map((choice, i) => {
|
|
310
|
+
const isHighlighted = i === highlightedIndex;
|
|
311
|
+
const isSelected = selected.has(choice.value);
|
|
312
|
+
const pointer = isHighlighted ? "› " : " ";
|
|
313
|
+
const checkbox = isSelected ? "◉ " : "○ ";
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<Box key={choice.value}>
|
|
317
|
+
<Text color={isHighlighted ? "magenta" : undefined}>
|
|
318
|
+
{pointer}
|
|
319
|
+
</Text>
|
|
320
|
+
<Text color={isSelected ? "green" : "gray"}>{checkbox}</Text>
|
|
321
|
+
<Text color={isSelected ? undefined : "gray"}>
|
|
322
|
+
{choice.label}
|
|
323
|
+
</Text>
|
|
324
|
+
</Box>
|
|
325
|
+
);
|
|
326
|
+
})}
|
|
327
|
+
</Box>
|
|
328
|
+
</Box>
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// =============================================================================
|
|
333
|
+
// Progress Header
|
|
334
|
+
// =============================================================================
|
|
335
|
+
|
|
336
|
+
interface ProgressHeaderProps {
|
|
337
|
+
current: number;
|
|
338
|
+
total: number;
|
|
339
|
+
group?: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const ProgressHeader = ({ current, total, group }: ProgressHeaderProps) => {
|
|
343
|
+
return (
|
|
344
|
+
<Box marginBottom={1}>
|
|
345
|
+
<Text dimColor>
|
|
346
|
+
{group ? `${group} · ` : ""}Step {current} of {total}
|
|
347
|
+
{" · "}
|
|
348
|
+
<Text dimColor italic>
|
|
349
|
+
esc to go back
|
|
350
|
+
</Text>
|
|
351
|
+
</Text>
|
|
352
|
+
</Box>
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// =============================================================================
|
|
357
|
+
// Main PromptSequence Component
|
|
358
|
+
// =============================================================================
|
|
359
|
+
|
|
360
|
+
export const PromptSequence = ({
|
|
361
|
+
prompts,
|
|
362
|
+
onComplete,
|
|
363
|
+
onCancel,
|
|
364
|
+
initialAnswers,
|
|
365
|
+
}: PromptSequenceProps) => {
|
|
366
|
+
// Always start at index 0, but preserve answers for display/editing
|
|
367
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
368
|
+
const [answers, setAnswers] = useState<Record<string, unknown>>(
|
|
369
|
+
initialAnswers ?? {},
|
|
370
|
+
);
|
|
371
|
+
// Track the history of prompt indices for back navigation
|
|
372
|
+
const [history, setHistory] = useState<number[]>([]);
|
|
373
|
+
|
|
374
|
+
// Filter prompts based on `when` condition
|
|
375
|
+
const activePrompts = prompts.filter((prompt) => {
|
|
376
|
+
if (prompt.when) {
|
|
377
|
+
return prompt.when(answers);
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const currentPrompt = activePrompts[currentIndex];
|
|
383
|
+
|
|
384
|
+
const handleAnswer = useCallback(
|
|
385
|
+
(value: unknown) => {
|
|
386
|
+
if (!currentPrompt) return;
|
|
387
|
+
|
|
388
|
+
const newAnswers = { ...answers, [currentPrompt.name]: value };
|
|
389
|
+
setAnswers(newAnswers);
|
|
390
|
+
|
|
391
|
+
if (currentIndex < activePrompts.length - 1) {
|
|
392
|
+
setHistory((prev) => [...prev, currentIndex]);
|
|
393
|
+
setCurrentIndex((prev) => prev + 1);
|
|
394
|
+
} else {
|
|
395
|
+
onComplete(newAnswers);
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
[answers, currentPrompt, currentIndex, activePrompts.length, onComplete],
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const handleBack = useCallback(() => {
|
|
402
|
+
if (history.length > 0) {
|
|
403
|
+
const prevIndex = history[history.length - 1];
|
|
404
|
+
setHistory((prev) => prev.slice(0, -1));
|
|
405
|
+
setCurrentIndex(prevIndex);
|
|
406
|
+
} else if (onCancel) {
|
|
407
|
+
onCancel();
|
|
408
|
+
}
|
|
409
|
+
}, [history, onCancel]);
|
|
410
|
+
|
|
411
|
+
if (!currentPrompt) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Get previously completed prompts to display
|
|
416
|
+
const completedPrompts = activePrompts.slice(0, currentIndex);
|
|
417
|
+
|
|
418
|
+
// Render the appropriate prompt type
|
|
419
|
+
const renderCurrentPrompt = () => {
|
|
420
|
+
const existingValue = answers[currentPrompt.name];
|
|
421
|
+
|
|
422
|
+
switch (currentPrompt.type) {
|
|
423
|
+
case "text":
|
|
424
|
+
return (
|
|
425
|
+
<TextPrompt
|
|
426
|
+
prompt={currentPrompt}
|
|
427
|
+
initialValue={existingValue as string | undefined}
|
|
428
|
+
onSubmit={handleAnswer}
|
|
429
|
+
onBack={handleBack}
|
|
430
|
+
/>
|
|
431
|
+
);
|
|
432
|
+
case "confirm":
|
|
433
|
+
return (
|
|
434
|
+
<ConfirmPrompt
|
|
435
|
+
prompt={currentPrompt}
|
|
436
|
+
onSubmit={handleAnswer}
|
|
437
|
+
onBack={handleBack}
|
|
438
|
+
/>
|
|
439
|
+
);
|
|
440
|
+
case "select":
|
|
441
|
+
return (
|
|
442
|
+
<SelectPrompt
|
|
443
|
+
prompt={currentPrompt}
|
|
444
|
+
onSubmit={handleAnswer}
|
|
445
|
+
onBack={handleBack}
|
|
446
|
+
/>
|
|
447
|
+
);
|
|
448
|
+
case "multiselect":
|
|
449
|
+
return (
|
|
450
|
+
<MultiselectPrompt
|
|
451
|
+
prompt={currentPrompt}
|
|
452
|
+
onSubmit={handleAnswer}
|
|
453
|
+
onBack={handleBack}
|
|
454
|
+
/>
|
|
455
|
+
);
|
|
456
|
+
default:
|
|
457
|
+
return (
|
|
458
|
+
<Text color="red">
|
|
459
|
+
Unknown prompt type: {(currentPrompt as PromptDefinition).type}
|
|
460
|
+
</Text>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
return (
|
|
466
|
+
<Box flexDirection="column">
|
|
467
|
+
{/* Progress indicator */}
|
|
468
|
+
<ProgressHeader
|
|
469
|
+
current={currentIndex + 1}
|
|
470
|
+
total={activePrompts.length}
|
|
471
|
+
group={currentPrompt.group}
|
|
472
|
+
/>
|
|
473
|
+
|
|
474
|
+
{/* Show completed answers in table format */}
|
|
475
|
+
{completedPrompts.length > 0 && (
|
|
476
|
+
<CompletedAnswersTable prompts={completedPrompts} answers={answers} />
|
|
477
|
+
)}
|
|
478
|
+
|
|
479
|
+
{/* Current prompt */}
|
|
480
|
+
{renderCurrentPrompt()}
|
|
481
|
+
</Box>
|
|
482
|
+
);
|
|
483
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spinner Component
|
|
3
|
+
*
|
|
4
|
+
* A simple loading spinner for the CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
|
|
10
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
11
|
+
|
|
12
|
+
export interface SpinnerProps {
|
|
13
|
+
/** Color of the spinner */
|
|
14
|
+
color?: string;
|
|
15
|
+
/** Label to show next to the spinner */
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Spinner = ({ color = "cyan", label }: SpinnerProps) => {
|
|
20
|
+
const [frameIndex, setFrameIndex] = useState(0);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const timer = setInterval(() => {
|
|
24
|
+
setFrameIndex((prev) => (prev + 1) % frames.length);
|
|
25
|
+
}, 80);
|
|
26
|
+
|
|
27
|
+
return () => clearInterval(timer);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Box>
|
|
32
|
+
<Text color={color}>{frames[frameIndex]}</Text>
|
|
33
|
+
{label && <Text> {label}</Text>}
|
|
34
|
+
</Box>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Ink Components
|
|
3
|
+
*
|
|
4
|
+
* This module exports all React Ink components for the CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { AppProps, AppState } from "./App.js";
|
|
8
|
+
export { App } from "./App.js";
|
|
9
|
+
export type { ExecutionProgressProps } from "./ExecutionProgress.js";
|
|
10
|
+
export { ExecutionProgress } from "./ExecutionProgress.js";
|
|
11
|
+
export type { FileTreePreviewProps } from "./FileTreePreview.js";
|
|
12
|
+
export { FileTreePreview } from "./FileTreePreview.js";
|
|
13
|
+
export type { PromptSequenceProps } from "./PromptSequence.js";
|
|
14
|
+
export { PromptSequence } from "./PromptSequence.js";
|
|
15
|
+
export type { SpinnerProps } from "./Spinner.js";
|
|
16
|
+
export { Spinner } from "./Spinner.js";
|