agent-facets 0.3.0 → 0.3.5

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 (60) hide show
  1. package/bin/facet +1 -1
  2. package/package.json +16 -37
  3. package/{scripts/postinstall.mjs → postinstall.mjs} +1 -1
  4. package/.package.json.bak +0 -45
  5. package/.turbo/turbo-build.log +0 -3
  6. package/CHANGELOG.md +0 -95
  7. package/bunfig.toml +0 -2
  8. package/dist/facet +0 -0
  9. package/src/__tests__/cli.test.ts +0 -195
  10. package/src/__tests__/create-build.test.ts +0 -227
  11. package/src/__tests__/edit-integration.test.ts +0 -171
  12. package/src/__tests__/launcher.test.ts +0 -106
  13. package/src/__tests__/postinstall.test.ts +0 -196
  14. package/src/__tests__/resolve-dir.test.ts +0 -95
  15. package/src/commands/build.ts +0 -58
  16. package/src/commands/create/index.ts +0 -76
  17. package/src/commands/create/types.ts +0 -9
  18. package/src/commands/create/wizard.tsx +0 -75
  19. package/src/commands/create-scaffold.ts +0 -184
  20. package/src/commands/edit/index.ts +0 -144
  21. package/src/commands/edit/wizard.tsx +0 -74
  22. package/src/commands/resolve-dir.ts +0 -98
  23. package/src/commands.ts +0 -40
  24. package/src/help.ts +0 -43
  25. package/src/index.ts +0 -10
  26. package/src/run.ts +0 -82
  27. package/src/suggest.ts +0 -35
  28. package/src/tui/components/asset-description.tsx +0 -17
  29. package/src/tui/components/asset-field-picker.tsx +0 -78
  30. package/src/tui/components/asset-inline-input.tsx +0 -91
  31. package/src/tui/components/asset-item.tsx +0 -44
  32. package/src/tui/components/asset-section.tsx +0 -191
  33. package/src/tui/components/button.tsx +0 -92
  34. package/src/tui/components/editable-field.tsx +0 -172
  35. package/src/tui/components/exit-toast.tsx +0 -20
  36. package/src/tui/components/reconciliation-item.tsx +0 -129
  37. package/src/tui/components/stage-row.tsx +0 -45
  38. package/src/tui/components/version-selector.tsx +0 -79
  39. package/src/tui/context/focus-mode-context.ts +0 -36
  40. package/src/tui/context/focus-order-context.ts +0 -68
  41. package/src/tui/context/form-state-context.ts +0 -260
  42. package/src/tui/editor.ts +0 -40
  43. package/src/tui/gradient.ts +0 -1
  44. package/src/tui/hooks/use-exit-keys.ts +0 -75
  45. package/src/tui/hooks/use-navigation-keys.ts +0 -34
  46. package/src/tui/layouts/wizard-layout.tsx +0 -41
  47. package/src/tui/theme.ts +0 -1
  48. package/src/tui/views/build/build-view.tsx +0 -152
  49. package/src/tui/views/create/confirm-view.tsx +0 -74
  50. package/src/tui/views/create/create-view.tsx +0 -158
  51. package/src/tui/views/create/wizard.tsx +0 -97
  52. package/src/tui/views/edit/edit-confirm-view.tsx +0 -93
  53. package/src/tui/views/edit/edit-types.ts +0 -34
  54. package/src/tui/views/edit/edit-view.tsx +0 -140
  55. package/src/tui/views/edit/manifest-to-form.ts +0 -38
  56. package/src/tui/views/edit/reconciliation-view.tsx +0 -170
  57. package/src/tui/views/edit/use-edit-session.ts +0 -125
  58. package/src/tui/views/edit/wizard.tsx +0 -129
  59. package/src/version.ts +0 -3
  60. package/tsconfig.json +0 -4
@@ -1,260 +0,0 @@
1
- import type { ReactNode } from 'react'
2
- import { createContext, createElement, useCallback, useContext, useMemo, useState } from 'react'
3
- import type { CreateOptions } from '../../commands/create-scaffold.ts'
4
- import { isValidKebabCase } from '../../commands/create-scaffold.ts'
5
-
6
- // --- Types ---
7
-
8
- export type FieldStatus = 'empty' | 'editing' | 'confirmed'
9
- export type RequiredFieldKey = 'name' | 'description' | 'version'
10
- export type AssetSectionKey = 'skill' | 'command' | 'agent'
11
-
12
- export interface FieldState {
13
- value: string
14
- status: FieldStatus
15
- }
16
-
17
- export interface AssetSectionState {
18
- items: string[]
19
- descriptions: Record<string, string>
20
- editing?: string
21
- adding: boolean
22
- }
23
-
24
- export interface FormState {
25
- fields: {
26
- name: FieldState
27
- description: FieldState
28
- version: FieldState
29
- }
30
- assets: {
31
- skill: AssetSectionState
32
- command: AssetSectionState
33
- agent: AssetSectionState
34
- }
35
- }
36
-
37
- // --- Context value ---
38
-
39
- interface FormStateContextValue {
40
- form: FormState
41
-
42
- // Field operations
43
- setFieldValue: (field: RequiredFieldKey, value: string) => void
44
- setFieldStatus: (field: RequiredFieldKey, status: FieldStatus) => void
45
-
46
- // Asset operations
47
- addAsset: (section: AssetSectionKey, name: string) => void
48
- removeAsset: (section: AssetSectionKey, name: string) => void
49
- renameAsset: (section: AssetSectionKey, oldName: string, newName: string) => void
50
- setAssetDescription: (section: AssetSectionKey, name: string, description: string) => void
51
- setAssetAdding: (section: AssetSectionKey, adding: boolean) => void
52
- setAssetEditing: (section: AssetSectionKey, name?: string) => void
53
-
54
- // Build CreateOptions for scaffold
55
- toCreateOptions: () => CreateOptions
56
- }
57
-
58
- // --- Defaults ---
59
-
60
- const defaultAssetSection: AssetSectionState = {
61
- items: [],
62
- descriptions: {},
63
- editing: undefined,
64
- adding: false,
65
- }
66
-
67
- const defaultForm: FormState = {
68
- fields: {
69
- name: { value: '', status: 'empty' },
70
- description: { value: '', status: 'empty' },
71
- version: { value: '', status: 'empty' },
72
- },
73
- assets: {
74
- skill: { ...defaultAssetSection },
75
- command: { ...defaultAssetSection },
76
- agent: { ...defaultAssetSection },
77
- },
78
- }
79
-
80
- const FormStateContext = createContext<FormStateContextValue>({
81
- form: defaultForm,
82
- setFieldValue: () => {},
83
- setFieldStatus: () => {},
84
- addAsset: () => {},
85
- removeAsset: () => {},
86
- renameAsset: () => {},
87
- setAssetDescription: () => {},
88
- setAssetAdding: () => {},
89
- setAssetEditing: () => {},
90
- toCreateOptions: () => ({ name: '', version: '', description: '', skills: [], commands: [], agents: [] }),
91
- })
92
-
93
- // --- Provider ---
94
-
95
- export function FormStateProvider({ children, initialState }: { children: ReactNode; initialState?: FormState }) {
96
- const [form, setForm] = useState<FormState>(initialState ?? defaultForm)
97
-
98
- const setFieldValue = useCallback((field: RequiredFieldKey, value: string) => {
99
- setForm((prev) => ({
100
- ...prev,
101
- fields: {
102
- ...prev.fields,
103
- [field]: { ...prev.fields[field], value },
104
- },
105
- }))
106
- }, [])
107
-
108
- const setFieldStatus = useCallback((field: RequiredFieldKey, status: FieldStatus) => {
109
- setForm((prev) => ({
110
- ...prev,
111
- fields: {
112
- ...prev.fields,
113
- [field]: { ...prev.fields[field], status },
114
- },
115
- }))
116
- }, [])
117
-
118
- const addAsset = useCallback((section: AssetSectionKey, name: string) => {
119
- if (!isValidKebabCase(name)) return
120
- setForm((prev) => {
121
- const current = prev.assets[section]
122
- if (current.items.includes(name)) return prev
123
- const defaultDesc = `The ${name} ${section} description`
124
- return {
125
- ...prev,
126
- assets: {
127
- ...prev.assets,
128
- [section]: {
129
- ...current,
130
- items: [...current.items, name],
131
- descriptions: { ...current.descriptions, [name]: defaultDesc },
132
- },
133
- },
134
- }
135
- })
136
- }, [])
137
-
138
- const removeAsset = useCallback((section: AssetSectionKey, name: string) => {
139
- setForm((prev) => {
140
- const current = prev.assets[section]
141
- const { [name]: _, ...remainingDescs } = current.descriptions
142
- return {
143
- ...prev,
144
- assets: {
145
- ...prev.assets,
146
- [section]: {
147
- ...current,
148
- items: current.items.filter((item) => item !== name),
149
- descriptions: remainingDescs,
150
- editing: current.editing === name ? undefined : current.editing,
151
- },
152
- },
153
- }
154
- })
155
- }, [])
156
-
157
- const renameAsset = useCallback((section: AssetSectionKey, oldName: string, newName: string) => {
158
- if (!isValidKebabCase(newName)) return
159
- setForm((prev) => {
160
- const current = prev.assets[section]
161
- if (current.items.includes(newName)) return prev
162
- const { [oldName]: desc, ...restDescs } = current.descriptions
163
- return {
164
- ...prev,
165
- assets: {
166
- ...prev.assets,
167
- [section]: {
168
- ...current,
169
- items: current.items.map((item) => (item === oldName ? newName : item)),
170
- descriptions: { ...restDescs, [newName]: desc ?? `A ${newName} ${section}` },
171
- editing: current.editing === oldName ? newName : current.editing,
172
- },
173
- },
174
- }
175
- })
176
- }, [])
177
-
178
- const setAssetDescription = useCallback((section: AssetSectionKey, name: string, description: string) => {
179
- setForm((prev) => {
180
- const current = prev.assets[section]
181
- return {
182
- ...prev,
183
- assets: {
184
- ...prev.assets,
185
- [section]: {
186
- ...current,
187
- descriptions: { ...current.descriptions, [name]: description },
188
- },
189
- },
190
- }
191
- })
192
- }, [])
193
-
194
- const setAssetAdding = useCallback((section: AssetSectionKey, adding: boolean) => {
195
- setForm((prev) => ({
196
- ...prev,
197
- assets: {
198
- ...prev.assets,
199
- [section]: { ...prev.assets[section], adding },
200
- },
201
- }))
202
- }, [])
203
-
204
- const setAssetEditing = useCallback((section: AssetSectionKey, name?: string) => {
205
- setForm((prev) => ({
206
- ...prev,
207
- assets: {
208
- ...prev.assets,
209
- [section]: { ...prev.assets[section], editing: name },
210
- },
211
- }))
212
- }, [])
213
-
214
- const toCreateOptions = useCallback(
215
- (): CreateOptions => ({
216
- name: form.fields.name.value,
217
- version: form.fields.version.value,
218
- description: form.fields.description.value,
219
- skills: form.assets.skill.items,
220
- commands: form.assets.command.items,
221
- agents: form.assets.agent.items,
222
- }),
223
- [form],
224
- )
225
-
226
- const value = useMemo<FormStateContextValue>(
227
- () => ({
228
- form,
229
- setFieldValue,
230
- setFieldStatus,
231
- addAsset,
232
- removeAsset,
233
- renameAsset,
234
- setAssetDescription,
235
- setAssetAdding,
236
- setAssetEditing,
237
- toCreateOptions,
238
- }),
239
- [
240
- form,
241
- setFieldValue,
242
- setFieldStatus,
243
- addAsset,
244
- removeAsset,
245
- renameAsset,
246
- setAssetDescription,
247
- setAssetAdding,
248
- setAssetEditing,
249
- toCreateOptions,
250
- ],
251
- )
252
-
253
- return createElement(FormStateContext.Provider, { value }, children)
254
- }
255
-
256
- // --- Hook ---
257
-
258
- export function useFormState() {
259
- return useContext(FormStateContext)
260
- }
package/src/tui/editor.ts DELETED
@@ -1,40 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
- import { readFileSync, unlinkSync, writeFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
-
6
- /**
7
- * Opens the user's preferred terminal editor with the given content.
8
- * Returns the edited content after the editor closes.
9
- *
10
- * This is synchronous — it blocks the event loop while the editor is open.
11
- * Callers must ensure the TUI is in a safe state before calling.
12
- *
13
- * Uses $VISUAL, $EDITOR, or falls back to 'vi'.
14
- */
15
- export function openInEditorSync(content: string, filename = 'description.md'): string | null {
16
- const editor = process.env.VISUAL || process.env.EDITOR || 'vi'
17
- const tmpFile = join(tmpdir(), `facet-${Date.now()}-${filename}`)
18
-
19
- writeFileSync(tmpFile, content, 'utf-8')
20
-
21
- // Split editor command to support args (e.g., "code --wait")
22
- const [cmd, ...args] = editor.split(' ')
23
- if (!cmd) return null
24
-
25
- const result = spawnSync(cmd, [...args, tmpFile], {
26
- stdio: 'inherit',
27
- })
28
-
29
- if (result.status !== 0) return null
30
-
31
- const edited = readFileSync(tmpFile, 'utf-8')
32
-
33
- try {
34
- unlinkSync(tmpFile)
35
- } catch {
36
- // Ignore cleanup failures
37
- }
38
-
39
- return edited
40
- }
@@ -1 +0,0 @@
1
- export { GRADIENT_STOPS, getAnimatedGradient } from '@agent-facets/brand'
@@ -1,75 +0,0 @@
1
- import { useInput } from 'ink'
2
- import { useCallback, useEffect, useRef } from 'react'
3
- import { useFocusMode } from '../context/focus-mode-context.ts'
4
-
5
- const EXIT_WINDOW_MS = 3000
6
-
7
- /**
8
- * Handles exit via double-tap Escape with a visible countdown toast.
9
- *
10
- * First Escape (when not editing): sets mode to 'exit-modal', starts countdown.
11
- * Second Escape (while in exit-modal): exits.
12
- * After 3 seconds: returns to 'form-navigation'.
13
- * Any other key while in exit-modal: returns to previous mode.
14
- */
15
- export function useExitKeys(onExit: () => void) {
16
- const { mode, setMode, setExitSecondsLeft } = useFocusMode()
17
- const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
18
- const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null)
19
- const previousModeRef = useRef(mode)
20
-
21
- const clearTimers = useCallback(() => {
22
- if (timerRef.current) {
23
- clearTimeout(timerRef.current)
24
- timerRef.current = null
25
- }
26
- if (countdownRef.current) {
27
- clearInterval(countdownRef.current)
28
- countdownRef.current = null
29
- }
30
- }, [])
31
-
32
- const dismiss = useCallback(() => {
33
- setExitSecondsLeft(0)
34
- clearTimers()
35
- setMode(previousModeRef.current === 'exit-modal' ? 'form-navigation' : previousModeRef.current)
36
- }, [clearTimers, setMode, setExitSecondsLeft])
37
-
38
- const startExitWindow = useCallback(() => {
39
- previousModeRef.current = mode
40
- setMode('exit-modal')
41
- setExitSecondsLeft(3)
42
-
43
- clearTimers()
44
-
45
- countdownRef.current = setInterval(() => {
46
- setExitSecondsLeft((prev: number) => {
47
- if (prev <= 1) return 0
48
- return prev - 1
49
- })
50
- }, 1000)
51
-
52
- timerRef.current = setTimeout(() => {
53
- dismiss()
54
- }, EXIT_WINDOW_MS)
55
- }, [clearTimers, dismiss, mode, setMode, setExitSecondsLeft])
56
-
57
- useEffect(() => clearTimers, [clearTimers])
58
-
59
- useInput((_input, key) => {
60
- if (key.escape) {
61
- if (mode === 'exit-modal') {
62
- clearTimers()
63
- onExit()
64
- } else if (mode !== 'field-revision') {
65
- startExitWindow()
66
- }
67
- return
68
- }
69
-
70
- // Any other key while in exit-modal dismisses it
71
- if (mode === 'exit-modal') {
72
- dismiss()
73
- }
74
- })
75
- }
@@ -1,34 +0,0 @@
1
- import { useInput } from 'ink'
2
- import { useFocusMode } from '../context/focus-mode-context.ts'
3
- import { useFocusOrder } from '../context/focus-order-context.ts'
4
-
5
- export function useNavigationKeys() {
6
- const { mode } = useFocusMode()
7
- const { focusNext, focusPrevious } = useFocusOrder()
8
-
9
- const isActive = mode === 'form-navigation' || mode === 'form-confirmation'
10
-
11
- useInput(
12
- (_input, key) => {
13
- if (key.downArrow || key.tab) {
14
- focusNext()
15
- return
16
- }
17
- if (key.upArrow || (key.shift && key.tab)) {
18
- focusPrevious()
19
- return
20
- }
21
- if (mode === 'form-confirmation') {
22
- if (key.rightArrow) {
23
- focusNext()
24
- return
25
- }
26
- if (key.leftArrow) {
27
- focusPrevious()
28
- return
29
- }
30
- }
31
- },
32
- { isActive },
33
- )
34
- }
@@ -1,41 +0,0 @@
1
- import { Box, Text } from 'ink'
2
- import Gradient from 'ink-gradient'
3
- import type { ReactNode } from 'react'
4
- import { useEffect, useState } from 'react'
5
- import { ExitFooter } from '../components/exit-toast.tsx'
6
- import { GRADIENT_STOPS, getAnimatedGradient } from '../gradient.ts'
7
- import { THEME } from '../theme.ts'
8
-
9
- const ANIMATION_INTERVAL_MS = 75
10
-
11
- function AnimatedGradientText({ text }: { text: string }) {
12
- const [offset, setOffset] = useState(0)
13
-
14
- useEffect(() => {
15
- const interval = setInterval(() => {
16
- setOffset((prev) => (prev + 1) % GRADIENT_STOPS.length)
17
- }, ANIMATION_INTERVAL_MS)
18
- return () => clearInterval(interval)
19
- }, [])
20
-
21
- return (
22
- <Gradient colors={getAnimatedGradient(offset)}>
23
- <Text bold>{text}</Text>
24
- </Gradient>
25
- )
26
- }
27
-
28
- export function WizardLayout({ children }: { children: ReactNode }) {
29
- return (
30
- <Box flexDirection="column" padding={1} gap={1}>
31
- <Box borderStyle="round" borderColor={THEME.brand} paddingX={2} gap={1}>
32
- <Text bold color={THEME.brand}>
33
- Create a new
34
- </Text>
35
- <AnimatedGradientText text="FACET" />
36
- </Box>
37
- {children}
38
- <ExitFooter />
39
- </Box>
40
- )
41
- }
package/src/tui/theme.ts DELETED
@@ -1 +0,0 @@
1
- export { THEME } from '@agent-facets/brand'
@@ -1,152 +0,0 @@
1
- import {
2
- BUILD_STAGES,
3
- type BuildProgress,
4
- type BuildStage,
5
- runBuildPipeline,
6
- writeBuildOutput,
7
- } from '@agent-facets/core'
8
- import { Box, Text, useApp } from 'ink'
9
- import { useCallback, useEffect, useMemo, useState } from 'react'
10
- import type { Stage } from '../../components/stage-row.tsx'
11
- import { StageRow } from '../../components/stage-row.tsx'
12
- import { THEME } from '../../theme.ts'
13
-
14
- interface BuildViewResult {
15
- name: string
16
- version: string
17
- files: string[]
18
- archiveFilename: string
19
- integrity: string
20
- warnings: string[]
21
- }
22
-
23
- export function BuildView({
24
- rootDir,
25
- onSuccess,
26
- onFailure,
27
- }: {
28
- rootDir: string
29
- onSuccess?: (name: string, version: string, fileCount: number, integrity: string) => void
30
- onFailure?: (errorCount: number) => void
31
- }) {
32
- const { exit } = useApp()
33
- const [stages, setStages] = useState<Stage[]>(BUILD_STAGES.map((label) => ({ label, status: 'pending' as const })))
34
- const [result, setResult] = useState<BuildViewResult | null>(null)
35
- const [warnings, setWarnings] = useState<string[]>([])
36
-
37
- // Deferred exit: set this to an Error to exit after the next render cycle,
38
- // ensuring error/stage state updates are painted before Ink unmounts.
39
- const [pendingExit, setPendingExit] = useState<Error | null>(null)
40
-
41
- useEffect(() => {
42
- if (pendingExit) {
43
- exit(pendingExit)
44
- }
45
- }, [pendingExit, exit])
46
-
47
- // Build a stable lookup from stage label to index
48
- const stageIndexMap = useMemo(() => Object.fromEntries(BUILD_STAGES.map((label, i) => [label, i])), [])
49
-
50
- const updateStage = useCallback(
51
- (label: BuildStage, update: Partial<Stage>) => {
52
- const index = stageIndexMap[label]
53
- if (index !== undefined) {
54
- setStages((prev) => prev.map((s, i) => (i === index ? { ...s, ...update } : s)))
55
- }
56
- },
57
- [stageIndexMap],
58
- )
59
-
60
- useEffect(() => {
61
- async function run() {
62
- const pipelineResult = await runBuildPipeline(rootDir, (progress: BuildProgress) => {
63
- updateStage(progress.stage, {
64
- status: progress.status === 'running' ? 'running' : progress.status === 'done' ? 'done' : 'failed',
65
- })
66
- })
67
-
68
- setWarnings(pipelineResult.warnings)
69
-
70
- if (!pipelineResult.ok) {
71
- const formatted = pipelineResult.errors.map((e) => e.message)
72
- // Find the stage that failed and attach errors to it
73
- setStages((prev) => prev.map((s) => (s.status === 'failed' ? { ...s, errors: formatted } : s)))
74
- onFailure?.(pipelineResult.errors.length)
75
- // Defer exit so React renders the errors and failed stage status first
76
- setPendingExit(new Error('Build failed'))
77
- return
78
- }
79
-
80
- // Writing output stage — handled here, not by the pipeline
81
- updateStage('Writing output', { status: 'running' })
82
- try {
83
- await writeBuildOutput(pipelineResult, rootDir)
84
-
85
- const files = Object.keys(pipelineResult.assetHashes).sort()
86
-
87
- updateStage('Writing output', { status: 'done' })
88
- setResult({
89
- name: pipelineResult.data.name,
90
- version: pipelineResult.data.version,
91
- files,
92
- archiveFilename: pipelineResult.archiveFilename,
93
- integrity: pipelineResult.integrity,
94
- warnings: pipelineResult.warnings,
95
- })
96
- onSuccess?.(pipelineResult.data.name, pipelineResult.data.version, files.length, pipelineResult.integrity)
97
- exit()
98
- } catch (err) {
99
- updateStage('Writing output', { status: 'failed', detail: String(err) })
100
- setPendingExit(err instanceof Error ? err : new Error(String(err)))
101
- }
102
- }
103
-
104
- run()
105
- }, [rootDir, exit, onSuccess, onFailure, updateStage])
106
-
107
- return (
108
- <Box flexDirection="column" padding={1} gap={1}>
109
- <Text bold color={THEME.brand}>
110
- Building facet...
111
- </Text>
112
-
113
- <Box flexDirection="column">
114
- {stages.map((s) => (
115
- <StageRow key={s.label} stage={s} />
116
- ))}
117
- </Box>
118
-
119
- {warnings.length > 0 && (
120
- <Box flexDirection="column">
121
- {warnings.map((w) => (
122
- <Text key={w} color={THEME.warning}>
123
- {' '}
124
- ⚠ {w}
125
- </Text>
126
- ))}
127
- </Box>
128
- )}
129
-
130
- {result && (
131
- <Box flexDirection="column">
132
- <Text color={THEME.success} bold>
133
- Built successfully → dist/
134
- </Text>
135
- <Text> {result.archiveFilename}</Text>
136
- <Text color={THEME.hint}> Archive contents:</Text>
137
- {result.files.map((f) => (
138
- <Text key={f}> {f}</Text>
139
- ))}
140
- <Box marginTop={1}>
141
- <Text color={THEME.hint}>
142
- {result.files.length} asset{result.files.length !== 1 ? 's' : ''} · {result.integrity}
143
- </Text>
144
- </Box>
145
- <Box marginTop={1}>
146
- <Text color={THEME.hint}>Next: facet publish (coming soon)</Text>
147
- </Box>
148
- </Box>
149
- )}
150
- </Box>
151
- )
152
- }
@@ -1,74 +0,0 @@
1
- import { Box, Text } from 'ink'
2
- import { useEffect } from 'react'
3
- import type { CreateOptions } from '../../../commands/create-scaffold.ts'
4
- import { previewFiles } from '../../../commands/create-scaffold.ts'
5
- import { Button } from '../../components/button.tsx'
6
- import { useFocusOrder } from '../../context/focus-order-context.ts'
7
- import { WizardLayout } from '../../layouts/wizard-layout.tsx'
8
- import { THEME } from '../../theme.ts'
9
-
10
- function SummaryField({ label, value }: { label: string; value: string }) {
11
- return (
12
- <Box gap={1}>
13
- <Text color={THEME.success}>✓</Text>
14
- <Text bold>{label}:</Text>
15
- <Text>{value}</Text>
16
- </Box>
17
- )
18
- }
19
-
20
- export function ConfirmView({
21
- opts,
22
- onConfirm,
23
- onBack,
24
- }: {
25
- opts: CreateOptions
26
- onConfirm: () => void
27
- onBack: () => void
28
- }) {
29
- const files = previewFiles(opts)
30
- const { setFocusIds, focus, focusedId } = useFocusOrder()
31
-
32
- useEffect(() => {
33
- setFocusIds(['confirm-yes', 'confirm-no'])
34
- focus('confirm-yes')
35
- }, [setFocusIds, focus])
36
-
37
- return (
38
- <WizardLayout>
39
- <Box flexDirection="column" marginLeft={2}>
40
- <SummaryField label="Name" value={opts.name} />
41
- <SummaryField label="Description" value={opts.description} />
42
- <SummaryField label="Version" value={opts.version} />
43
- {opts.skills.length > 0 && <SummaryField label="Skills" value={opts.skills.join(', ')} />}
44
- {opts.agents.length > 0 && <SummaryField label="Agents" value={opts.agents.join(', ')} />}
45
- {opts.commands.length > 0 && <SummaryField label="Commands" value={opts.commands.join(', ')} />}
46
- </Box>
47
-
48
- <Box flexDirection="column" borderStyle="round" borderColor={THEME.success} paddingX={2} paddingY={1} gap={0}>
49
- <Text bold color={THEME.success}>
50
- Files to create:
51
- </Text>
52
- {files.map((f) => (
53
- <Text key={f}> {f}</Text>
54
- ))}
55
- </Box>
56
-
57
- <Box gap={2} marginTop={1}>
58
- <Button
59
- id="confirm-yes"
60
- label="[ Yes, create ]"
61
- color={THEME.success}
62
- gradient={focusedId === 'confirm-yes'}
63
- animateGradient={focusedId === 'confirm-yes'}
64
- onPress={onConfirm}
65
- />
66
- <Button id="confirm-no" label="[ No, go back ]" color={THEME.warning} onPress={onBack} />
67
- </Box>
68
-
69
- <Box marginTop={1}>
70
- <Text dimColor>← → to switch, Enter to confirm</Text>
71
- </Box>
72
- </WizardLayout>
73
- )
74
- }