agent-facets 0.3.0 → 0.3.3
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/bin/facet +1 -1
- package/package.json +16 -37
- package/{scripts/postinstall.mjs → postinstall.mjs} +1 -1
- package/.package.json.bak +0 -45
- package/.turbo/turbo-build.log +0 -3
- package/CHANGELOG.md +0 -95
- package/bunfig.toml +0 -2
- package/dist/facet +0 -0
- package/src/__tests__/cli.test.ts +0 -195
- package/src/__tests__/create-build.test.ts +0 -227
- package/src/__tests__/edit-integration.test.ts +0 -171
- package/src/__tests__/launcher.test.ts +0 -106
- package/src/__tests__/postinstall.test.ts +0 -196
- package/src/__tests__/resolve-dir.test.ts +0 -95
- package/src/commands/build.ts +0 -58
- package/src/commands/create/index.ts +0 -76
- package/src/commands/create/types.ts +0 -9
- package/src/commands/create/wizard.tsx +0 -75
- package/src/commands/create-scaffold.ts +0 -184
- package/src/commands/edit/index.ts +0 -144
- package/src/commands/edit/wizard.tsx +0 -74
- package/src/commands/resolve-dir.ts +0 -98
- package/src/commands.ts +0 -40
- package/src/help.ts +0 -43
- package/src/index.ts +0 -10
- package/src/run.ts +0 -82
- package/src/suggest.ts +0 -35
- package/src/tui/components/asset-description.tsx +0 -17
- package/src/tui/components/asset-field-picker.tsx +0 -78
- package/src/tui/components/asset-inline-input.tsx +0 -91
- package/src/tui/components/asset-item.tsx +0 -44
- package/src/tui/components/asset-section.tsx +0 -191
- package/src/tui/components/button.tsx +0 -92
- package/src/tui/components/editable-field.tsx +0 -172
- package/src/tui/components/exit-toast.tsx +0 -20
- package/src/tui/components/reconciliation-item.tsx +0 -129
- package/src/tui/components/stage-row.tsx +0 -45
- package/src/tui/components/version-selector.tsx +0 -79
- package/src/tui/context/focus-mode-context.ts +0 -36
- package/src/tui/context/focus-order-context.ts +0 -68
- package/src/tui/context/form-state-context.ts +0 -260
- package/src/tui/editor.ts +0 -40
- package/src/tui/gradient.ts +0 -1
- package/src/tui/hooks/use-exit-keys.ts +0 -75
- package/src/tui/hooks/use-navigation-keys.ts +0 -34
- package/src/tui/layouts/wizard-layout.tsx +0 -41
- package/src/tui/theme.ts +0 -1
- package/src/tui/views/build/build-view.tsx +0 -152
- package/src/tui/views/create/confirm-view.tsx +0 -74
- package/src/tui/views/create/create-view.tsx +0 -158
- package/src/tui/views/create/wizard.tsx +0 -97
- package/src/tui/views/edit/edit-confirm-view.tsx +0 -93
- package/src/tui/views/edit/edit-types.ts +0 -34
- package/src/tui/views/edit/edit-view.tsx +0 -140
- package/src/tui/views/edit/manifest-to-form.ts +0 -38
- package/src/tui/views/edit/reconciliation-view.tsx +0 -170
- package/src/tui/views/edit/use-edit-session.ts +0 -125
- package/src/tui/views/edit/wizard.tsx +0 -129
- package/src/version.ts +0 -3
- 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
|
-
}
|
package/src/tui/gradient.ts
DELETED
|
@@ -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
|
-
}
|