agent-facets 0.1.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/CHANGELOG.md +30 -0
- package/bunfig.toml +2 -0
- package/dist/facet +0 -0
- package/dist/facet-ink-test +0 -0
- package/package.json +41 -0
- package/src/__tests__/cli.test.ts +152 -0
- package/src/__tests__/create-build.test.ts +207 -0
- package/src/commands/build.ts +45 -0
- package/src/commands/create/index.ts +30 -0
- package/src/commands/create/types.ts +9 -0
- package/src/commands/create/wizard.tsx +24 -0
- package/src/commands/create-scaffold.ts +180 -0
- package/src/commands.ts +31 -0
- package/src/help.ts +36 -0
- package/src/index.ts +9 -0
- package/src/run.ts +55 -0
- package/src/suggest.ts +35 -0
- package/src/tui/components/asset-inline-input.tsx +79 -0
- package/src/tui/components/asset-item.tsx +48 -0
- package/src/tui/components/asset-section.tsx +145 -0
- package/src/tui/components/button.tsx +92 -0
- package/src/tui/components/editable-field.tsx +172 -0
- package/src/tui/components/exit-toast.tsx +20 -0
- package/src/tui/components/stage-row.tsx +33 -0
- package/src/tui/components/version-selector.tsx +79 -0
- package/src/tui/context/focus-mode-context.ts +36 -0
- package/src/tui/context/focus-order-context.ts +62 -0
- package/src/tui/context/form-state-context.ts +229 -0
- package/src/tui/gradient.ts +1 -0
- package/src/tui/hooks/use-exit-keys.ts +75 -0
- package/src/tui/hooks/use-navigation-keys.ts +34 -0
- package/src/tui/layouts/wizard-layout.tsx +41 -0
- package/src/tui/theme.ts +1 -0
- package/src/tui/views/build/build-view.tsx +153 -0
- package/src/tui/views/create/confirm-view.tsx +74 -0
- package/src/tui/views/create/create-view.tsx +154 -0
- package/src/tui/views/create/wizard.tsx +68 -0
- package/src/version.ts +3 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Box, Text } from 'ink'
|
|
2
|
+
import { useCallback, useEffect } from 'react'
|
|
3
|
+
import { ASSET_LABELS, ASSET_TYPES } from '../../../commands/create/types.ts'
|
|
4
|
+
import { isValidKebabCase } from '../../../commands/create-scaffold.ts'
|
|
5
|
+
import { AssetSection } from '../../components/asset-section.tsx'
|
|
6
|
+
import { Button } from '../../components/button.tsx'
|
|
7
|
+
import { EditableField } from '../../components/editable-field.tsx'
|
|
8
|
+
import { useFocusOrder } from '../../context/focus-order-context.ts'
|
|
9
|
+
import { useFormState } from '../../context/form-state-context.ts'
|
|
10
|
+
import { WizardLayout } from '../../layouts/wizard-layout.tsx'
|
|
11
|
+
|
|
12
|
+
function computeFocusIds(form: ReturnType<typeof useFormState>['form'], hasAnyAsset: boolean): string[] {
|
|
13
|
+
const ids: string[] = ['field-name', 'field-description', 'field-version']
|
|
14
|
+
|
|
15
|
+
for (const type of ASSET_TYPES) {
|
|
16
|
+
const section = form.assets[type]
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < section.items.length; i++) {
|
|
19
|
+
ids.push(`item-${type}-${i}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ids.push(`add-${type}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (hasAnyAsset) {
|
|
26
|
+
ids.push('create-btn')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return ids
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function CreateView({ onSubmit }: { onSubmit: () => void }) {
|
|
33
|
+
const { form } = useFormState()
|
|
34
|
+
const { setFocusIds, focus, focusedId } = useFocusOrder()
|
|
35
|
+
|
|
36
|
+
const validateKebab = useCallback((v: string) => {
|
|
37
|
+
if (!v) return undefined
|
|
38
|
+
if (!isValidKebabCase(v)) return 'Must be kebab-case (e.g., my-facet)'
|
|
39
|
+
return undefined
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
// Derived state from context
|
|
43
|
+
const nameConfirmed = form.fields.name.status === 'confirmed'
|
|
44
|
+
const descriptionConfirmed = form.fields.description.status === 'confirmed'
|
|
45
|
+
const versionConfirmed = form.fields.version.status === 'confirmed'
|
|
46
|
+
|
|
47
|
+
// Settled = confirmed or has a value (being revised). Used for dimming.
|
|
48
|
+
const nameSettled = nameConfirmed || !!form.fields.name.value
|
|
49
|
+
const descriptionSettled = descriptionConfirmed || !!form.fields.description.value
|
|
50
|
+
const versionSettled = versionConfirmed || !!form.fields.version.value
|
|
51
|
+
|
|
52
|
+
const descriptionReady = nameSettled
|
|
53
|
+
const versionReady = nameSettled && descriptionSettled
|
|
54
|
+
const assetsReady = nameSettled && descriptionSettled && versionSettled
|
|
55
|
+
|
|
56
|
+
const totalAssets = form.assets.skill.items.length + form.assets.command.items.length + form.assets.agent.items.length
|
|
57
|
+
const hasAnyAsset = totalAssets > 0
|
|
58
|
+
const canCreate = assetsReady && hasAnyAsset
|
|
59
|
+
|
|
60
|
+
// Recompute focus order
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const ids = computeFocusIds(form, hasAnyAsset)
|
|
63
|
+
setFocusIds(ids)
|
|
64
|
+
|
|
65
|
+
if (focusedId && !ids.includes(focusedId)) {
|
|
66
|
+
focus(ids[0] ?? '')
|
|
67
|
+
}
|
|
68
|
+
}, [form, hasAnyAsset, setFocusIds, focus, focusedId])
|
|
69
|
+
|
|
70
|
+
// Auto-focus name field on mount
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!focusedId) {
|
|
73
|
+
focus('field-name')
|
|
74
|
+
}
|
|
75
|
+
}, [focusedId, focus])
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<WizardLayout>
|
|
79
|
+
<EditableField
|
|
80
|
+
field="name"
|
|
81
|
+
label="Name"
|
|
82
|
+
placeholder="my-facet"
|
|
83
|
+
hint="kebab-case"
|
|
84
|
+
validate={validateKebab}
|
|
85
|
+
onConfirm={() => focus('field-description')}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<EditableField
|
|
89
|
+
field="description"
|
|
90
|
+
label="Description"
|
|
91
|
+
placeholder="A brief description of what this facet does"
|
|
92
|
+
dimmed={!descriptionReady}
|
|
93
|
+
onConfirm={() => focus('field-version')}
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<EditableField
|
|
97
|
+
field="version"
|
|
98
|
+
label="Version"
|
|
99
|
+
hint="SemVer N.N.N"
|
|
100
|
+
defaultValue="0.1.0"
|
|
101
|
+
dimmed={!versionReady}
|
|
102
|
+
validate={(v) => (/^\d+\.\d+\.\d+$/.test(v) ? undefined : 'Must be SemVer (e.g., 0.1.0)')}
|
|
103
|
+
onConfirm={() => focus(`add-${ASSET_TYPES[0]}`)}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
{ASSET_TYPES.map((type) => (
|
|
107
|
+
<Box key={type} marginTop={0}>
|
|
108
|
+
<AssetSection
|
|
109
|
+
section={type}
|
|
110
|
+
label={ASSET_LABELS[type]}
|
|
111
|
+
defaultName={form.assets[type].items.length === 0 ? form.fields.name.value : undefined}
|
|
112
|
+
dimmed={!assetsReady}
|
|
113
|
+
validate={(v) => {
|
|
114
|
+
if (!isValidKebabCase(v)) return 'Must be kebab-case'
|
|
115
|
+
const editing = form.assets[type].editing
|
|
116
|
+
if (form.assets[type].items.some((item) => item === v && item !== editing)) return `"${v}" already exists`
|
|
117
|
+
return undefined
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
</Box>
|
|
121
|
+
))}
|
|
122
|
+
|
|
123
|
+
<Box marginTop={1}>
|
|
124
|
+
<Button
|
|
125
|
+
id="create-btn"
|
|
126
|
+
label="[ Create facet ]"
|
|
127
|
+
color="green"
|
|
128
|
+
disabled={!canCreate}
|
|
129
|
+
gradient={canCreate}
|
|
130
|
+
animateGradient={canCreate && focusedId === 'create-btn'}
|
|
131
|
+
onPress={onSubmit}
|
|
132
|
+
/>
|
|
133
|
+
</Box>
|
|
134
|
+
|
|
135
|
+
{!canCreate && (
|
|
136
|
+
<Box marginLeft={2}>
|
|
137
|
+
<Text dimColor>
|
|
138
|
+
{!nameConfirmed
|
|
139
|
+
? 'Enter a name to continue'
|
|
140
|
+
: !descriptionConfirmed
|
|
141
|
+
? 'Enter a description to continue'
|
|
142
|
+
: !versionConfirmed
|
|
143
|
+
? 'Enter a version to continue'
|
|
144
|
+
: 'Add at least one skill, command, or agent'}
|
|
145
|
+
</Text>
|
|
146
|
+
</Box>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
<Box marginTop={1}>
|
|
150
|
+
<Text dimColor>↑ ↓ to navigate, Enter to select/edit, Esc Esc to exit</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
</WizardLayout>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useApp } from 'ink'
|
|
2
|
+
import { useCallback, useState } from 'react'
|
|
3
|
+
import type { CreateOptions } from '../../../commands/create-scaffold.ts'
|
|
4
|
+
import { FocusModeProvider, useFocusMode } from '../../context/focus-mode-context.ts'
|
|
5
|
+
import { FocusOrderProvider } from '../../context/focus-order-context.ts'
|
|
6
|
+
import { FormStateProvider, useFormState } from '../../context/form-state-context.ts'
|
|
7
|
+
import { useExitKeys } from '../../hooks/use-exit-keys.ts'
|
|
8
|
+
import { useNavigationKeys } from '../../hooks/use-navigation-keys.ts'
|
|
9
|
+
import { ConfirmView } from './confirm-view.tsx'
|
|
10
|
+
import { CreateView } from './create-view.tsx'
|
|
11
|
+
|
|
12
|
+
export interface CreateWizardProps {
|
|
13
|
+
onComplete: (opts: CreateOptions) => void
|
|
14
|
+
onCancel: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function CreateWizardInner({ onComplete, onCancel }: CreateWizardProps) {
|
|
18
|
+
const { exit } = useApp()
|
|
19
|
+
const { setMode } = useFocusMode()
|
|
20
|
+
const { toCreateOptions } = useFormState()
|
|
21
|
+
|
|
22
|
+
const [confirming, setConfirming] = useState(false)
|
|
23
|
+
|
|
24
|
+
const cancel = useCallback(() => {
|
|
25
|
+
onCancel()
|
|
26
|
+
exit()
|
|
27
|
+
}, [onCancel, exit])
|
|
28
|
+
|
|
29
|
+
useExitKeys(cancel)
|
|
30
|
+
useNavigationKeys()
|
|
31
|
+
|
|
32
|
+
if (confirming) {
|
|
33
|
+
return (
|
|
34
|
+
<ConfirmView
|
|
35
|
+
opts={toCreateOptions()}
|
|
36
|
+
onConfirm={() => {
|
|
37
|
+
onComplete(toCreateOptions())
|
|
38
|
+
exit()
|
|
39
|
+
}}
|
|
40
|
+
onBack={() => {
|
|
41
|
+
setConfirming(false)
|
|
42
|
+
setMode('form-navigation')
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<CreateView
|
|
50
|
+
onSubmit={() => {
|
|
51
|
+
setConfirming(true)
|
|
52
|
+
setMode('form-confirmation')
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function CreateWizard(props: CreateWizardProps) {
|
|
59
|
+
return (
|
|
60
|
+
<FocusModeProvider>
|
|
61
|
+
<FocusOrderProvider>
|
|
62
|
+
<FormStateProvider>
|
|
63
|
+
<CreateWizardInner {...props} />
|
|
64
|
+
</FormStateProvider>
|
|
65
|
+
</FocusOrderProvider>
|
|
66
|
+
</FocusModeProvider>
|
|
67
|
+
)
|
|
68
|
+
}
|
package/src/version.ts
ADDED
package/tsconfig.json
ADDED