agent-facets 0.2.2 → 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 +181 -0
- package/bin/package.json +3 -0
- package/package.json +17 -37
- package/postinstall.mjs +210 -0
- package/.package.json.bak +0 -44
- package/.turbo/turbo-build.log +0 -3
- package/CHANGELOG.md +0 -85
- 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__/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,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
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
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 { DEFAULT_VERSION, 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']): 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
|
-
ids.push('create-btn')
|
|
26
|
-
|
|
27
|
-
return ids
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function CreateView({
|
|
31
|
-
onSubmit,
|
|
32
|
-
onEditDescription,
|
|
33
|
-
}: {
|
|
34
|
-
onSubmit: () => void
|
|
35
|
-
onEditDescription?: (section: import('../../context/form-state-context.ts').AssetSectionKey, name: string) => void
|
|
36
|
-
}) {
|
|
37
|
-
const { form } = useFormState()
|
|
38
|
-
const { setFocusIds, focus, focusedId } = useFocusOrder()
|
|
39
|
-
|
|
40
|
-
const validateKebab = useCallback((v: string) => {
|
|
41
|
-
if (!v) return undefined
|
|
42
|
-
if (!isValidKebabCase(v)) return 'Must be kebab-case (e.g., my-facet)'
|
|
43
|
-
return undefined
|
|
44
|
-
}, [])
|
|
45
|
-
|
|
46
|
-
// Derived state from context
|
|
47
|
-
const nameConfirmed = form.fields.name.status === 'confirmed'
|
|
48
|
-
const descriptionConfirmed = form.fields.description.status === 'confirmed'
|
|
49
|
-
const versionConfirmed = form.fields.version.status === 'confirmed'
|
|
50
|
-
|
|
51
|
-
// Settled = confirmed or has a value (being revised). Used for dimming.
|
|
52
|
-
const nameSettled = nameConfirmed || !!form.fields.name.value
|
|
53
|
-
const descriptionSettled = descriptionConfirmed || !!form.fields.description.value
|
|
54
|
-
const versionSettled = versionConfirmed || !!form.fields.version.value
|
|
55
|
-
|
|
56
|
-
const descriptionReady = nameSettled
|
|
57
|
-
const versionReady = nameSettled && descriptionSettled
|
|
58
|
-
const assetsReady = nameSettled && descriptionSettled && versionSettled
|
|
59
|
-
|
|
60
|
-
const totalAssets = form.assets.skill.items.length + form.assets.command.items.length + form.assets.agent.items.length
|
|
61
|
-
const canCreate = assetsReady && totalAssets > 0
|
|
62
|
-
|
|
63
|
-
// Recompute focus order
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const ids = computeFocusIds(form)
|
|
66
|
-
setFocusIds(ids)
|
|
67
|
-
|
|
68
|
-
if (focusedId && !ids.includes(focusedId)) {
|
|
69
|
-
focus(ids[0] ?? '')
|
|
70
|
-
}
|
|
71
|
-
}, [form, setFocusIds, focus, focusedId])
|
|
72
|
-
|
|
73
|
-
// Auto-focus name field on mount
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (!focusedId) {
|
|
76
|
-
focus('field-name')
|
|
77
|
-
}
|
|
78
|
-
}, [focusedId, focus])
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<WizardLayout>
|
|
82
|
-
<EditableField
|
|
83
|
-
field="name"
|
|
84
|
-
label="Name"
|
|
85
|
-
placeholder="my-facet"
|
|
86
|
-
hint="kebab-case"
|
|
87
|
-
validate={validateKebab}
|
|
88
|
-
onConfirm={() => focus('field-description')}
|
|
89
|
-
/>
|
|
90
|
-
|
|
91
|
-
<EditableField
|
|
92
|
-
field="description"
|
|
93
|
-
label="Description"
|
|
94
|
-
placeholder="A brief description of what this facet does"
|
|
95
|
-
dimmed={!descriptionReady}
|
|
96
|
-
onConfirm={() => focus('field-version')}
|
|
97
|
-
/>
|
|
98
|
-
|
|
99
|
-
<EditableField
|
|
100
|
-
field="version"
|
|
101
|
-
label="Version"
|
|
102
|
-
hint="SemVer N.N.N"
|
|
103
|
-
defaultValue={DEFAULT_VERSION}
|
|
104
|
-
dimmed={!versionReady}
|
|
105
|
-
validate={(v) => (/^\d+\.\d+\.\d+$/.test(v) ? undefined : `Must be SemVer (e.g., ${DEFAULT_VERSION})`)}
|
|
106
|
-
onConfirm={() => focus(`add-${ASSET_TYPES[0]}`)}
|
|
107
|
-
/>
|
|
108
|
-
|
|
109
|
-
{ASSET_TYPES.map((type) => (
|
|
110
|
-
<Box key={type} marginTop={0}>
|
|
111
|
-
<AssetSection
|
|
112
|
-
section={type}
|
|
113
|
-
label={ASSET_LABELS[type]}
|
|
114
|
-
defaultName={form.assets[type].items.length === 0 ? form.fields.name.value : undefined}
|
|
115
|
-
dimmed={!assetsReady}
|
|
116
|
-
onEditDescription={onEditDescription}
|
|
117
|
-
validate={(v) => {
|
|
118
|
-
if (!isValidKebabCase(v)) return 'Must be kebab-case'
|
|
119
|
-
const editing = form.assets[type].editing
|
|
120
|
-
if (form.assets[type].items.some((item) => item === v && item !== editing)) return `"${v}" already exists`
|
|
121
|
-
return undefined
|
|
122
|
-
}}
|
|
123
|
-
/>
|
|
124
|
-
</Box>
|
|
125
|
-
))}
|
|
126
|
-
|
|
127
|
-
<Box marginTop={1}>
|
|
128
|
-
<Button
|
|
129
|
-
id="create-btn"
|
|
130
|
-
label="[ Create facet ]"
|
|
131
|
-
color="green"
|
|
132
|
-
disabled={!canCreate}
|
|
133
|
-
gradient={canCreate}
|
|
134
|
-
animateGradient={canCreate && focusedId === 'create-btn'}
|
|
135
|
-
onPress={onSubmit}
|
|
136
|
-
/>
|
|
137
|
-
</Box>
|
|
138
|
-
|
|
139
|
-
{!canCreate && (
|
|
140
|
-
<Box marginLeft={2}>
|
|
141
|
-
<Text dimColor>
|
|
142
|
-
{!nameConfirmed
|
|
143
|
-
? 'Enter a name to continue'
|
|
144
|
-
: !descriptionConfirmed
|
|
145
|
-
? 'Enter a description to continue'
|
|
146
|
-
: !versionConfirmed
|
|
147
|
-
? 'Enter a version to continue'
|
|
148
|
-
: 'Add at least one skill, agent, or command'}
|
|
149
|
-
</Text>
|
|
150
|
-
</Box>
|
|
151
|
-
)}
|
|
152
|
-
|
|
153
|
-
<Box marginTop={1}>
|
|
154
|
-
<Text dimColor>↑ ↓ to navigate, Enter to select/edit, Esc Esc to exit</Text>
|
|
155
|
-
</Box>
|
|
156
|
-
</WizardLayout>
|
|
157
|
-
)
|
|
158
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { useApp } from 'ink'
|
|
2
|
-
import { useCallback, useEffect, 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, useFocusOrder } from '../../context/focus-order-context.ts'
|
|
6
|
-
import type { AssetSectionKey, FormState } from '../../context/form-state-context.ts'
|
|
7
|
-
import { FormStateProvider, useFormState } from '../../context/form-state-context.ts'
|
|
8
|
-
import { useExitKeys } from '../../hooks/use-exit-keys.ts'
|
|
9
|
-
import { useNavigationKeys } from '../../hooks/use-navigation-keys.ts'
|
|
10
|
-
import { ConfirmView } from './confirm-view.tsx'
|
|
11
|
-
import { CreateView } from './create-view.tsx'
|
|
12
|
-
|
|
13
|
-
export interface WizardSnapshot {
|
|
14
|
-
form: FormState
|
|
15
|
-
focusedId: string | null
|
|
16
|
-
selectedItem?: {
|
|
17
|
-
section: AssetSectionKey
|
|
18
|
-
name: string
|
|
19
|
-
field: 'name' | 'description'
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface CreateWizardProps {
|
|
24
|
-
onComplete: (opts: CreateOptions) => void
|
|
25
|
-
onCancel: () => void
|
|
26
|
-
snapshot?: WizardSnapshot
|
|
27
|
-
onSnapshot?: (snapshot: WizardSnapshot) => void
|
|
28
|
-
onRequestEditor?: (section: AssetSectionKey, name: string, description: string) => void
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function CreateWizardInner({ onComplete, onCancel, snapshot, onSnapshot, onRequestEditor }: CreateWizardProps) {
|
|
32
|
-
const { exit } = useApp()
|
|
33
|
-
const { setMode } = useFocusMode()
|
|
34
|
-
const { form, toCreateOptions } = useFormState()
|
|
35
|
-
const { focusedId } = useFocusOrder()
|
|
36
|
-
|
|
37
|
-
const [confirming, setConfirming] = useState(false)
|
|
38
|
-
|
|
39
|
-
// Keep the parent informed of current state for snapshot
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
onSnapshot?.({ form, focusedId, selectedItem: snapshot?.selectedItem })
|
|
42
|
-
}, [form, focusedId, onSnapshot, snapshot?.selectedItem])
|
|
43
|
-
|
|
44
|
-
const cancel = useCallback(() => {
|
|
45
|
-
onCancel()
|
|
46
|
-
exit()
|
|
47
|
-
}, [onCancel, exit])
|
|
48
|
-
|
|
49
|
-
useExitKeys(cancel)
|
|
50
|
-
useNavigationKeys()
|
|
51
|
-
|
|
52
|
-
const handleEditDescription = useCallback(
|
|
53
|
-
(section: AssetSectionKey, name: string) => {
|
|
54
|
-
const description = form.assets[section].descriptions[name] ?? ''
|
|
55
|
-
onRequestEditor?.(section, name, description)
|
|
56
|
-
},
|
|
57
|
-
[form, onRequestEditor],
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
if (confirming) {
|
|
61
|
-
return (
|
|
62
|
-
<ConfirmView
|
|
63
|
-
opts={toCreateOptions()}
|
|
64
|
-
onConfirm={() => {
|
|
65
|
-
onComplete(toCreateOptions())
|
|
66
|
-
exit()
|
|
67
|
-
}}
|
|
68
|
-
onBack={() => {
|
|
69
|
-
setConfirming(false)
|
|
70
|
-
setMode('form-navigation')
|
|
71
|
-
}}
|
|
72
|
-
/>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<CreateView
|
|
78
|
-
onSubmit={() => {
|
|
79
|
-
setConfirming(true)
|
|
80
|
-
setMode('form-confirmation')
|
|
81
|
-
}}
|
|
82
|
-
onEditDescription={handleEditDescription}
|
|
83
|
-
/>
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function CreateWizard(props: CreateWizardProps) {
|
|
88
|
-
return (
|
|
89
|
-
<FocusModeProvider>
|
|
90
|
-
<FocusOrderProvider initialFocusId={props.snapshot?.focusedId}>
|
|
91
|
-
<FormStateProvider initialState={props.snapshot?.form}>
|
|
92
|
-
<CreateWizardInner {...props} />
|
|
93
|
-
</FormStateProvider>
|
|
94
|
-
</FocusOrderProvider>
|
|
95
|
-
</FocusModeProvider>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
import { useEffect } from 'react'
|
|
3
|
-
import { truncateDescription } from '../../components/asset-description.tsx'
|
|
4
|
-
import { Button } from '../../components/button.tsx'
|
|
5
|
-
import { useFocusOrder } from '../../context/focus-order-context.ts'
|
|
6
|
-
import type { AssetSectionKey } from '../../context/form-state-context.ts'
|
|
7
|
-
import { useFormState } from '../../context/form-state-context.ts'
|
|
8
|
-
import { THEME } from '../../theme.ts'
|
|
9
|
-
|
|
10
|
-
const ASSET_TYPES: AssetSectionKey[] = ['skill', 'command', 'agent']
|
|
11
|
-
const ASSET_LABELS: Record<AssetSectionKey, string> = {
|
|
12
|
-
skill: 'Skills',
|
|
13
|
-
command: 'Commands',
|
|
14
|
-
agent: 'Agents',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function EditConfirmView({ onConfirm, onBack }: { onConfirm: () => void; onBack: () => void }) {
|
|
18
|
-
const { form } = useFormState()
|
|
19
|
-
const { setFocusIds, focus, focusedId } = useFocusOrder()
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
setFocusIds(['edit-apply-btn', 'edit-back-btn'])
|
|
23
|
-
focus('edit-apply-btn')
|
|
24
|
-
}, [setFocusIds, focus])
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<Box flexDirection="column" padding={1} gap={1}>
|
|
28
|
-
<Text bold color={THEME.brand}>
|
|
29
|
-
Review changes
|
|
30
|
-
</Text>
|
|
31
|
-
|
|
32
|
-
<Box flexDirection="column">
|
|
33
|
-
<Box gap={1}>
|
|
34
|
-
<Text bold>Name:</Text>
|
|
35
|
-
<Text>{form.fields.name.value || '(none)'}</Text>
|
|
36
|
-
</Box>
|
|
37
|
-
<Box gap={1}>
|
|
38
|
-
<Text bold>Description:</Text>
|
|
39
|
-
<Text>{truncateDescription(form.fields.description.value || '(none)')}</Text>
|
|
40
|
-
</Box>
|
|
41
|
-
<Box gap={1}>
|
|
42
|
-
<Text bold>Version:</Text>
|
|
43
|
-
<Text>{form.fields.version.value || '(none)'}</Text>
|
|
44
|
-
</Box>
|
|
45
|
-
</Box>
|
|
46
|
-
|
|
47
|
-
{ASSET_TYPES.map((type) => {
|
|
48
|
-
const section = form.assets[type]
|
|
49
|
-
return (
|
|
50
|
-
<Box key={type} flexDirection="column">
|
|
51
|
-
<Text bold>{ASSET_LABELS[type]}:</Text>
|
|
52
|
-
{section.items.length === 0 ? (
|
|
53
|
-
<Box marginLeft={2}>
|
|
54
|
-
<Text dimColor>(none)</Text>
|
|
55
|
-
</Box>
|
|
56
|
-
) : (
|
|
57
|
-
section.items.map((item) => {
|
|
58
|
-
const desc = section.descriptions[item] ?? `A ${item} ${type}`
|
|
59
|
-
return (
|
|
60
|
-
<Box key={item} flexDirection="column" marginLeft={2}>
|
|
61
|
-
<Box gap={1}>
|
|
62
|
-
<Text color={THEME.success}>●</Text>
|
|
63
|
-
<Text>{item}</Text>
|
|
64
|
-
</Box>
|
|
65
|
-
<Box marginLeft={3}>
|
|
66
|
-
<Text dimColor>{truncateDescription(desc)}</Text>
|
|
67
|
-
</Box>
|
|
68
|
-
</Box>
|
|
69
|
-
)
|
|
70
|
-
})
|
|
71
|
-
)}
|
|
72
|
-
</Box>
|
|
73
|
-
)
|
|
74
|
-
})}
|
|
75
|
-
|
|
76
|
-
<Box marginTop={1} gap={2}>
|
|
77
|
-
<Button
|
|
78
|
-
id="edit-apply-btn"
|
|
79
|
-
label="[ Yes, apply ]"
|
|
80
|
-
color={THEME.success}
|
|
81
|
-
gradient={focusedId === 'edit-apply-btn'}
|
|
82
|
-
animateGradient={focusedId === 'edit-apply-btn'}
|
|
83
|
-
onPress={onConfirm}
|
|
84
|
-
/>
|
|
85
|
-
<Button id="edit-back-btn" label="[ No, go back ]" color={THEME.warning} onPress={onBack} />
|
|
86
|
-
</Box>
|
|
87
|
-
|
|
88
|
-
<Box>
|
|
89
|
-
<Text dimColor>← → switch · Enter confirm</Text>
|
|
90
|
-
</Box>
|
|
91
|
-
</Box>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { AssetType, FacetManifest } from '@agent-facets/core'
|
|
2
|
-
|
|
3
|
-
/** A reconciliation item that needs a resolution before editing can proceed. */
|
|
4
|
-
export type ReconciliationItem =
|
|
5
|
-
| { kind: 'addition'; type: AssetType; name: string; path: string }
|
|
6
|
-
| { kind: 'missing'; type: AssetType; name: string; expectedPath: string }
|
|
7
|
-
| { kind: 'front-matter'; type: AssetType; name: string; path: string }
|
|
8
|
-
|
|
9
|
-
/** The resolution chosen for a reconciliation item. */
|
|
10
|
-
export type ReconciliationResolution =
|
|
11
|
-
| { action: 'add-to-manifest' }
|
|
12
|
-
| { action: 'ignore' }
|
|
13
|
-
| { action: 'scaffold-template' }
|
|
14
|
-
| { action: 'remove-from-manifest' }
|
|
15
|
-
| { action: 'strip-front-matter' }
|
|
16
|
-
|
|
17
|
-
/** All data needed to run the edit TUI. */
|
|
18
|
-
export interface EditContext {
|
|
19
|
-
rootDir: string
|
|
20
|
-
manifest: FacetManifest
|
|
21
|
-
reconciliationItems: ReconciliationItem[]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** The result of the edit wizard — either applied changes or cancelled. */
|
|
25
|
-
export type EditResult =
|
|
26
|
-
| { outcome: 'applied'; manifest: FacetManifest; operations: EditOperation[] }
|
|
27
|
-
| { outcome: 'cancelled' }
|
|
28
|
-
|
|
29
|
-
/** A file operation to perform on confirmation. */
|
|
30
|
-
export type EditOperation =
|
|
31
|
-
| { op: 'write-manifest' }
|
|
32
|
-
| { op: 'scaffold'; type: AssetType; name: string }
|
|
33
|
-
| { op: 'delete-file'; type: AssetType; name: string }
|
|
34
|
-
| { op: 'strip-front-matter'; type: AssetType; name: string; path: string }
|