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,140 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
import { useCallback, useEffect } from 'react'
|
|
3
|
-
import { DEFAULT_VERSION, isValidKebabCase } from '../../../commands/create-scaffold.ts'
|
|
4
|
-
import { AssetSection } from '../../components/asset-section.tsx'
|
|
5
|
-
import { Button } from '../../components/button.tsx'
|
|
6
|
-
import { EditableField } from '../../components/editable-field.tsx'
|
|
7
|
-
import { useFocusOrder } from '../../context/focus-order-context.ts'
|
|
8
|
-
import type { AssetSectionKey } from '../../context/form-state-context.ts'
|
|
9
|
-
import { useFormState } from '../../context/form-state-context.ts'
|
|
10
|
-
import { THEME } from '../../theme.ts'
|
|
11
|
-
|
|
12
|
-
const ASSET_TYPES: AssetSectionKey[] = ['skill', 'command', 'agent']
|
|
13
|
-
const ASSET_LABELS: Record<AssetSectionKey, string> = {
|
|
14
|
-
skill: 'Skills',
|
|
15
|
-
command: 'Commands',
|
|
16
|
-
agent: 'Agents',
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function computeFocusIds(form: ReturnType<typeof useFormState>['form']): string[] {
|
|
20
|
-
const ids: string[] = ['field-name', 'field-description', 'field-version']
|
|
21
|
-
|
|
22
|
-
for (const type of ASSET_TYPES) {
|
|
23
|
-
const section = form.assets[type]
|
|
24
|
-
|
|
25
|
-
for (let i = 0; i < section.items.length; i++) {
|
|
26
|
-
ids.push(`item-${type}-${i}`)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
ids.push(`add-${type}`)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
ids.push('edit-confirm-btn')
|
|
33
|
-
return ids
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function EditView({
|
|
37
|
-
onSubmit,
|
|
38
|
-
onEditDescription,
|
|
39
|
-
}: {
|
|
40
|
-
onSubmit: () => void
|
|
41
|
-
onEditDescription?: (section: AssetSectionKey, name: string) => void
|
|
42
|
-
}) {
|
|
43
|
-
const { form } = useFormState()
|
|
44
|
-
const { setFocusIds, focus, focusedId } = useFocusOrder()
|
|
45
|
-
|
|
46
|
-
const validateKebab = useCallback((v: string) => {
|
|
47
|
-
if (!v) return undefined
|
|
48
|
-
if (!isValidKebabCase(v)) return 'Must be kebab-case (e.g., my-facet)'
|
|
49
|
-
return undefined
|
|
50
|
-
}, [])
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
const ids = computeFocusIds(form)
|
|
54
|
-
setFocusIds(ids)
|
|
55
|
-
|
|
56
|
-
if (focusedId && !ids.includes(focusedId)) {
|
|
57
|
-
focus(ids[0] ?? '')
|
|
58
|
-
}
|
|
59
|
-
}, [form, setFocusIds, focus, focusedId])
|
|
60
|
-
|
|
61
|
-
const totalAssets = form.assets.skill.items.length + form.assets.agent.items.length + form.assets.command.items.length
|
|
62
|
-
const canConfirm = totalAssets > 0
|
|
63
|
-
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
if (!focusedId) {
|
|
66
|
-
focus('field-name')
|
|
67
|
-
}
|
|
68
|
-
}, [focusedId, focus])
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<Box flexDirection="column" padding={1} gap={1}>
|
|
72
|
-
<Text bold color={THEME.brand}>
|
|
73
|
-
Edit facet
|
|
74
|
-
</Text>
|
|
75
|
-
|
|
76
|
-
<Box flexDirection="column">
|
|
77
|
-
<EditableField
|
|
78
|
-
field="name"
|
|
79
|
-
label="Name"
|
|
80
|
-
placeholder="my-facet"
|
|
81
|
-
hint="kebab-case"
|
|
82
|
-
validate={validateKebab}
|
|
83
|
-
onConfirm={() => focus('field-description')}
|
|
84
|
-
/>
|
|
85
|
-
|
|
86
|
-
<EditableField
|
|
87
|
-
field="description"
|
|
88
|
-
label="Description"
|
|
89
|
-
placeholder="A brief description"
|
|
90
|
-
onConfirm={() => focus('field-version')}
|
|
91
|
-
/>
|
|
92
|
-
|
|
93
|
-
<EditableField
|
|
94
|
-
field="version"
|
|
95
|
-
label="Version"
|
|
96
|
-
hint="SemVer N.N.N"
|
|
97
|
-
validate={(v) => (/^\d+\.\d+\.\d+$/.test(v) ? undefined : `Must be SemVer (e.g., ${DEFAULT_VERSION})`)}
|
|
98
|
-
onConfirm={() => focus(`add-${ASSET_TYPES[0]}`)}
|
|
99
|
-
/>
|
|
100
|
-
</Box>
|
|
101
|
-
|
|
102
|
-
{ASSET_TYPES.map((type) => (
|
|
103
|
-
<Box key={type} marginTop={0}>
|
|
104
|
-
<AssetSection
|
|
105
|
-
section={type}
|
|
106
|
-
label={ASSET_LABELS[type]}
|
|
107
|
-
onEditDescription={onEditDescription}
|
|
108
|
-
validate={(v) => {
|
|
109
|
-
if (!isValidKebabCase(v)) return 'Must be kebab-case'
|
|
110
|
-
const editing = form.assets[type].editing
|
|
111
|
-
if (form.assets[type].items.some((item) => item === v && item !== editing)) return `"${v}" already exists`
|
|
112
|
-
return undefined
|
|
113
|
-
}}
|
|
114
|
-
/>
|
|
115
|
-
</Box>
|
|
116
|
-
))}
|
|
117
|
-
|
|
118
|
-
<Box marginTop={1}>
|
|
119
|
-
<Button
|
|
120
|
-
id="edit-confirm-btn"
|
|
121
|
-
label="[ Review & Confirm ]"
|
|
122
|
-
disabled={!canConfirm}
|
|
123
|
-
gradient={canConfirm}
|
|
124
|
-
animateGradient={canConfirm && focusedId === 'edit-confirm-btn'}
|
|
125
|
-
onPress={onSubmit}
|
|
126
|
-
/>
|
|
127
|
-
</Box>
|
|
128
|
-
|
|
129
|
-
{!canConfirm && (
|
|
130
|
-
<Box marginLeft={2}>
|
|
131
|
-
<Text dimColor>Add at least one skill, agent, or command</Text>
|
|
132
|
-
</Box>
|
|
133
|
-
)}
|
|
134
|
-
|
|
135
|
-
<Box>
|
|
136
|
-
<Text dimColor>↑ ↓ navigate · Enter edit · Esc Esc exit (no changes)</Text>
|
|
137
|
-
</Box>
|
|
138
|
-
</Box>
|
|
139
|
-
)
|
|
140
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { FacetManifest } from '@agent-facets/core'
|
|
2
|
-
import type { AssetSectionKey, FormState } from '../../context/form-state-context.ts'
|
|
3
|
-
|
|
4
|
-
/** Maps manifest asset keys to form section keys. */
|
|
5
|
-
const MANIFEST_TO_FORM: Record<string, AssetSectionKey> = {
|
|
6
|
-
skills: 'skill',
|
|
7
|
-
agents: 'agent',
|
|
8
|
-
commands: 'command',
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Builds initial form state from an existing manifest. */
|
|
12
|
-
export function manifestToFormState(manifest: FacetManifest): FormState {
|
|
13
|
-
const assets: FormState['assets'] = {
|
|
14
|
-
skill: { items: [], descriptions: {}, editing: undefined, adding: false },
|
|
15
|
-
agent: { items: [], descriptions: {}, editing: undefined, adding: false },
|
|
16
|
-
command: { items: [], descriptions: {}, editing: undefined, adding: false },
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
for (const [manifestKey, formKey] of Object.entries(MANIFEST_TO_FORM)) {
|
|
20
|
-
const section = manifest[manifestKey as keyof FacetManifest]
|
|
21
|
-
if (section && typeof section === 'object' && !Array.isArray(section)) {
|
|
22
|
-
const entries = section as Record<string, { description?: string }>
|
|
23
|
-
for (const [name, descriptor] of Object.entries(entries)) {
|
|
24
|
-
assets[formKey].items.push(name)
|
|
25
|
-
assets[formKey].descriptions[name] = descriptor.description ?? `A ${name} ${formKey}`
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
fields: {
|
|
32
|
-
name: { value: manifest.name, status: 'confirmed' },
|
|
33
|
-
description: { value: manifest.description ?? '', status: 'confirmed' },
|
|
34
|
-
version: { value: manifest.version, status: 'confirmed' },
|
|
35
|
-
},
|
|
36
|
-
assets,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
import { useCallback, useEffect, useMemo } from 'react'
|
|
3
|
-
import { Button } from '../../components/button.tsx'
|
|
4
|
-
import { ReconciliationItemRow } from '../../components/reconciliation-item.tsx'
|
|
5
|
-
import { useFocusOrder } from '../../context/focus-order-context.ts'
|
|
6
|
-
import { THEME } from '../../theme.ts'
|
|
7
|
-
import type { ReconciliationItem, ReconciliationResolution } from './edit-types.ts'
|
|
8
|
-
|
|
9
|
-
/** Maps a reconciliation item to a unique key. */
|
|
10
|
-
function itemKey(item: ReconciliationItem): string {
|
|
11
|
-
return `${item.kind}:${item.type}:${item.name}`
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Returns the two action options for a reconciliation item kind. */
|
|
15
|
-
function optionsForKind(kind: ReconciliationItem['kind']): [{ label: string }, { label: string }] {
|
|
16
|
-
switch (kind) {
|
|
17
|
-
case 'addition':
|
|
18
|
-
return [{ label: 'Add to manifest' }, { label: 'Ignore for now' }]
|
|
19
|
-
case 'missing':
|
|
20
|
-
return [{ label: 'Scaffold template' }, { label: 'Remove from manifest' }]
|
|
21
|
-
case 'front-matter':
|
|
22
|
-
return [{ label: 'Strip front matter' }, { label: 'Remove from manifest' }]
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Converts a selected option index to a resolution for the given item kind. */
|
|
27
|
-
function indexToResolution(kind: ReconciliationItem['kind'], index: number): ReconciliationResolution {
|
|
28
|
-
switch (kind) {
|
|
29
|
-
case 'addition':
|
|
30
|
-
return index === 0 ? { action: 'add-to-manifest' } : { action: 'ignore' }
|
|
31
|
-
case 'missing':
|
|
32
|
-
return index === 0 ? { action: 'scaffold-template' } : { action: 'remove-from-manifest' }
|
|
33
|
-
case 'front-matter':
|
|
34
|
-
return index === 0 ? { action: 'strip-front-matter' } : { action: 'remove-from-manifest' }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Returns the selected option index for a resolution, or null. */
|
|
39
|
-
function resolutionToIndex(
|
|
40
|
-
kind: ReconciliationItem['kind'],
|
|
41
|
-
resolution: ReconciliationResolution | undefined,
|
|
42
|
-
): number | null {
|
|
43
|
-
if (!resolution) return null
|
|
44
|
-
switch (kind) {
|
|
45
|
-
case 'addition':
|
|
46
|
-
return resolution.action === 'add-to-manifest' ? 0 : resolution.action === 'ignore' ? 1 : null
|
|
47
|
-
case 'missing':
|
|
48
|
-
return resolution.action === 'scaffold-template' ? 0 : resolution.action === 'remove-from-manifest' ? 1 : null
|
|
49
|
-
case 'front-matter':
|
|
50
|
-
return resolution.action === 'strip-front-matter' ? 0 : resolution.action === 'remove-from-manifest' ? 1 : null
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function ReconciliationView({
|
|
55
|
-
items,
|
|
56
|
-
resolutions,
|
|
57
|
-
onResolve,
|
|
58
|
-
onContinue,
|
|
59
|
-
}: {
|
|
60
|
-
items: ReconciliationItem[]
|
|
61
|
-
resolutions: Map<string, ReconciliationResolution>
|
|
62
|
-
onResolve: (key: string, resolution: ReconciliationResolution) => void
|
|
63
|
-
onContinue: () => void
|
|
64
|
-
}) {
|
|
65
|
-
const { setFocusIds, focusedId, focus } = useFocusOrder()
|
|
66
|
-
|
|
67
|
-
const allResolved = items.every((item) => resolutions.has(itemKey(item)))
|
|
68
|
-
|
|
69
|
-
// Group items by kind
|
|
70
|
-
const additions = items.filter((i) => i.kind === 'addition')
|
|
71
|
-
const missing = items.filter((i) => i.kind === 'missing')
|
|
72
|
-
const frontMatter = items.filter((i) => i.kind === 'front-matter')
|
|
73
|
-
|
|
74
|
-
// Build focus order: all items then continue button
|
|
75
|
-
const focusIds = useMemo(() => {
|
|
76
|
-
const ids = items.map((item) => `recon-${itemKey(item)}`)
|
|
77
|
-
ids.push('recon-continue')
|
|
78
|
-
return ids
|
|
79
|
-
}, [items])
|
|
80
|
-
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
setFocusIds(focusIds)
|
|
83
|
-
if (!focusedId) {
|
|
84
|
-
focus(focusIds[0] ?? '')
|
|
85
|
-
}
|
|
86
|
-
}, [focusIds, setFocusIds, focusedId, focus])
|
|
87
|
-
|
|
88
|
-
const handleSelect = useCallback(
|
|
89
|
-
(item: ReconciliationItem, optionIndex: number) => {
|
|
90
|
-
const key = itemKey(item)
|
|
91
|
-
const resolution = indexToResolution(item.kind, optionIndex)
|
|
92
|
-
onResolve(key, resolution)
|
|
93
|
-
|
|
94
|
-
// Auto-advance to next unresolved item
|
|
95
|
-
const currentIdx = items.findIndex((i) => itemKey(i) === key)
|
|
96
|
-
for (let i = currentIdx + 1; i < items.length; i++) {
|
|
97
|
-
const nextItem = items[i]
|
|
98
|
-
if (!nextItem) continue
|
|
99
|
-
const nextKey = itemKey(nextItem)
|
|
100
|
-
if (!resolutions.has(nextKey)) {
|
|
101
|
-
focus(`recon-${nextKey}`)
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// All resolved — focus continue button
|
|
106
|
-
focus('recon-continue')
|
|
107
|
-
},
|
|
108
|
-
[items, resolutions, onResolve, focus],
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
const renderGroup = (label: string, groupItems: ReconciliationItem[]) => {
|
|
112
|
-
if (groupItems.length === 0) return null
|
|
113
|
-
return (
|
|
114
|
-
<Box flexDirection="column" key={label}>
|
|
115
|
-
<Box marginBottom={0}>
|
|
116
|
-
<Text bold color={THEME.warning}>
|
|
117
|
-
{label}
|
|
118
|
-
</Text>
|
|
119
|
-
</Box>
|
|
120
|
-
{groupItems.map((item) => {
|
|
121
|
-
const key = itemKey(item)
|
|
122
|
-
const description =
|
|
123
|
-
item.kind === 'missing'
|
|
124
|
-
? `${item.name} (${item.type}) — ${item.expectedPath}`
|
|
125
|
-
: 'path' in item
|
|
126
|
-
? item.path
|
|
127
|
-
: ''
|
|
128
|
-
|
|
129
|
-
return (
|
|
130
|
-
<ReconciliationItemRow
|
|
131
|
-
key={key}
|
|
132
|
-
id={`recon-${key}`}
|
|
133
|
-
description={description}
|
|
134
|
-
options={optionsForKind(item.kind)}
|
|
135
|
-
selectedIndex={resolutionToIndex(item.kind, resolutions.get(key))}
|
|
136
|
-
onSelect={(index) => handleSelect(item, index)}
|
|
137
|
-
/>
|
|
138
|
-
)
|
|
139
|
-
})}
|
|
140
|
-
</Box>
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<Box flexDirection="column" padding={1} gap={1}>
|
|
146
|
-
<Text bold color={THEME.brand}>
|
|
147
|
-
Reconciliation — {items.length} item{items.length !== 1 ? 's' : ''} to resolve
|
|
148
|
-
</Text>
|
|
149
|
-
|
|
150
|
-
{renderGroup('New files on disk:', additions)}
|
|
151
|
-
{renderGroup('Missing from disk:', missing)}
|
|
152
|
-
{renderGroup('Front matter detected:', frontMatter)}
|
|
153
|
-
|
|
154
|
-
<Box marginTop={1}>
|
|
155
|
-
<Button
|
|
156
|
-
id="recon-continue"
|
|
157
|
-
label="[ Continue to edit ]"
|
|
158
|
-
disabled={!allResolved}
|
|
159
|
-
gradient={allResolved}
|
|
160
|
-
animateGradient={allResolved && focusedId === 'recon-continue'}
|
|
161
|
-
onPress={onContinue}
|
|
162
|
-
/>
|
|
163
|
-
</Box>
|
|
164
|
-
|
|
165
|
-
<Box>
|
|
166
|
-
<Text dimColor>↑ ↓ navigate · ← → switch option · Enter select · Esc Esc exit</Text>
|
|
167
|
-
</Box>
|
|
168
|
-
</Box>
|
|
169
|
-
)
|
|
170
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import type { FacetManifest } from '@agent-facets/core'
|
|
2
|
-
import { useCallback, useState } from 'react'
|
|
3
|
-
import type { AssetSectionKey, FormState } from '../../context/form-state-context.ts'
|
|
4
|
-
import type { EditContext, EditOperation, EditResult, ReconciliationResolution } from './edit-types.ts'
|
|
5
|
-
|
|
6
|
-
/** Maps form section keys to manifest asset keys. */
|
|
7
|
-
const FORM_TO_MANIFEST: Record<AssetSectionKey, 'skills' | 'agents' | 'commands'> = {
|
|
8
|
-
skill: 'skills',
|
|
9
|
-
agent: 'agents',
|
|
10
|
-
command: 'commands',
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Builds a manifest from form state, preserving non-asset fields from the original. */
|
|
14
|
-
function buildManifest(original: FacetManifest, form: FormState): FacetManifest {
|
|
15
|
-
const manifest: FacetManifest = {
|
|
16
|
-
...original,
|
|
17
|
-
name: form.fields.name.value,
|
|
18
|
-
version: form.fields.version.value,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (form.fields.description.value) {
|
|
22
|
-
manifest.description = form.fields.description.value
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
for (const [formKey, manifestKey] of Object.entries(FORM_TO_MANIFEST) as [
|
|
26
|
-
AssetSectionKey,
|
|
27
|
-
'skills' | 'agents' | 'commands',
|
|
28
|
-
][]) {
|
|
29
|
-
const items = form.assets[formKey].items
|
|
30
|
-
if (items.length > 0) {
|
|
31
|
-
const section: Record<string, { description: string }> = {}
|
|
32
|
-
for (const name of items) {
|
|
33
|
-
section[name] = { description: form.assets[formKey].descriptions[name] ?? '' }
|
|
34
|
-
}
|
|
35
|
-
manifest[manifestKey] = section
|
|
36
|
-
} else {
|
|
37
|
-
delete manifest[manifestKey]
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return manifest
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Builds the list of file operations from resolutions + form changes. */
|
|
45
|
-
function buildOperations(
|
|
46
|
-
context: EditContext,
|
|
47
|
-
form: FormState,
|
|
48
|
-
resolutions: Map<string, ReconciliationResolution>,
|
|
49
|
-
): EditOperation[] {
|
|
50
|
-
const operations: EditOperation[] = [{ op: 'write-manifest' }]
|
|
51
|
-
|
|
52
|
-
// Operations from reconciliation resolutions
|
|
53
|
-
for (const [key, resolution] of resolutions) {
|
|
54
|
-
const parts = key.split(':')
|
|
55
|
-
const kind = parts[0]
|
|
56
|
-
const assetType = parts[1] as 'skills' | 'agents' | 'commands'
|
|
57
|
-
const name = parts[2]
|
|
58
|
-
if (!kind || !assetType || !name) continue
|
|
59
|
-
|
|
60
|
-
if (resolution.action === 'scaffold-template') {
|
|
61
|
-
operations.push({ op: 'scaffold', type: assetType, name })
|
|
62
|
-
} else if (resolution.action === 'remove-from-manifest' && kind === 'front-matter') {
|
|
63
|
-
operations.push({ op: 'delete-file', type: assetType, name })
|
|
64
|
-
} else if (resolution.action === 'strip-front-matter') {
|
|
65
|
-
const item = context.reconciliationItems.find(
|
|
66
|
-
(i) => i.kind === 'front-matter' && i.type === assetType && i.name === name,
|
|
67
|
-
)
|
|
68
|
-
if (item && 'path' in item) {
|
|
69
|
-
operations.push({ op: 'strip-front-matter', type: assetType, name, path: item.path })
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// New assets added during editing (not from reconciliation)
|
|
75
|
-
for (const [formKey, manifestKey] of Object.entries(FORM_TO_MANIFEST) as [
|
|
76
|
-
AssetSectionKey,
|
|
77
|
-
'skills' | 'agents' | 'commands',
|
|
78
|
-
][]) {
|
|
79
|
-
const originalSection = context.manifest[manifestKey]
|
|
80
|
-
const originalNames =
|
|
81
|
-
originalSection && typeof originalSection === 'object' && !Array.isArray(originalSection)
|
|
82
|
-
? Object.keys(originalSection)
|
|
83
|
-
: []
|
|
84
|
-
|
|
85
|
-
for (const name of form.assets[formKey].items) {
|
|
86
|
-
const isFromReconciliation = resolutions.has(`addition:${manifestKey}:${name}`)
|
|
87
|
-
if (!originalNames.includes(name) && !isFromReconciliation) {
|
|
88
|
-
operations.push({ op: 'scaffold', type: manifestKey, name })
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Removed assets
|
|
93
|
-
for (const name of originalNames) {
|
|
94
|
-
if (!form.assets[formKey].items.includes(name)) {
|
|
95
|
-
operations.push({ op: 'delete-file', type: manifestKey, name })
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return operations
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function useEditSession(context: EditContext) {
|
|
104
|
-
const [resolutions, setResolutions] = useState<Map<string, ReconciliationResolution>>(new Map())
|
|
105
|
-
|
|
106
|
-
const resolve = useCallback((key: string, resolution: ReconciliationResolution) => {
|
|
107
|
-
setResolutions((prev) => {
|
|
108
|
-
const next = new Map(prev)
|
|
109
|
-
next.set(key, resolution)
|
|
110
|
-
return next
|
|
111
|
-
})
|
|
112
|
-
}, [])
|
|
113
|
-
|
|
114
|
-
/** Builds the final edit result from current form state and resolutions. */
|
|
115
|
-
const buildResult = useCallback(
|
|
116
|
-
(form: FormState): EditResult => {
|
|
117
|
-
const manifest = buildManifest(context.manifest, form)
|
|
118
|
-
const operations = buildOperations(context, form, resolutions)
|
|
119
|
-
return { outcome: 'applied', manifest, operations }
|
|
120
|
-
},
|
|
121
|
-
[context, resolutions],
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
return { resolutions, resolve, buildResult }
|
|
125
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { useApp } from 'ink'
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
3
|
-
import { FocusModeProvider, useFocusMode } from '../../context/focus-mode-context.ts'
|
|
4
|
-
import { FocusOrderProvider, useFocusOrder } from '../../context/focus-order-context.ts'
|
|
5
|
-
import type { AssetSectionKey, FormState } from '../../context/form-state-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 { EditConfirmView } from './edit-confirm-view.tsx'
|
|
10
|
-
import type { EditContext, EditResult, ReconciliationResolution } from './edit-types.ts'
|
|
11
|
-
import { EditView } from './edit-view.tsx'
|
|
12
|
-
import { manifestToFormState } from './manifest-to-form.ts'
|
|
13
|
-
import { ReconciliationView } from './reconciliation-view.tsx'
|
|
14
|
-
import { useEditSession } from './use-edit-session.ts'
|
|
15
|
-
|
|
16
|
-
type EditPhase = 'reconciliation' | 'editing' | 'confirmation'
|
|
17
|
-
|
|
18
|
-
export interface EditWizardSnapshot {
|
|
19
|
-
phase: EditPhase
|
|
20
|
-
formState?: FormState
|
|
21
|
-
focusedId?: string | null
|
|
22
|
-
resolutions: Map<string, ReconciliationResolution>
|
|
23
|
-
selectedItem?: {
|
|
24
|
-
section: AssetSectionKey
|
|
25
|
-
name: string
|
|
26
|
-
field: 'name' | 'description'
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface EditWizardProps {
|
|
31
|
-
context: EditContext
|
|
32
|
-
snapshot?: EditWizardSnapshot
|
|
33
|
-
onComplete: (result: EditResult) => void
|
|
34
|
-
onSnapshot?: (snapshot: EditWizardSnapshot) => void
|
|
35
|
-
onRequestEditor?: (section: AssetSectionKey, name: string, description: string) => void
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function EditWizardInner({ context, snapshot, onComplete, onSnapshot, onRequestEditor }: EditWizardProps) {
|
|
39
|
-
const { exit } = useApp()
|
|
40
|
-
const { setMode } = useFocusMode()
|
|
41
|
-
const { form } = useFormState()
|
|
42
|
-
const { focusedId, focus } = useFocusOrder()
|
|
43
|
-
const hasReconciliation = context.reconciliationItems.length > 0
|
|
44
|
-
|
|
45
|
-
const initialPhase = snapshot?.phase ?? (hasReconciliation ? 'reconciliation' : 'editing')
|
|
46
|
-
const [phase, setPhase] = useState<EditPhase>(initialPhase)
|
|
47
|
-
const { resolutions, resolve, buildResult } = useEditSession(context)
|
|
48
|
-
|
|
49
|
-
// Report snapshot to parent for editor round-trips
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
onSnapshot?.({ phase, formState: form, focusedId, resolutions })
|
|
52
|
-
}, [phase, form, focusedId, resolutions, onSnapshot])
|
|
53
|
-
|
|
54
|
-
const cancel = useCallback(() => {
|
|
55
|
-
onComplete({ outcome: 'cancelled' })
|
|
56
|
-
exit()
|
|
57
|
-
}, [onComplete, exit])
|
|
58
|
-
|
|
59
|
-
useExitKeys(cancel)
|
|
60
|
-
useNavigationKeys()
|
|
61
|
-
|
|
62
|
-
const handleEditDescription = useCallback(
|
|
63
|
-
(section: AssetSectionKey, name: string) => {
|
|
64
|
-
const description = form.assets[section].descriptions[name] ?? ''
|
|
65
|
-
onRequestEditor?.(section, name, description)
|
|
66
|
-
},
|
|
67
|
-
[form, onRequestEditor],
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
const handleConfirm = useCallback(() => {
|
|
71
|
-
onComplete(buildResult(form))
|
|
72
|
-
exit()
|
|
73
|
-
}, [form, buildResult, onComplete, exit])
|
|
74
|
-
|
|
75
|
-
if (phase === 'reconciliation') {
|
|
76
|
-
return (
|
|
77
|
-
<ReconciliationView
|
|
78
|
-
items={context.reconciliationItems}
|
|
79
|
-
resolutions={resolutions}
|
|
80
|
-
onResolve={resolve}
|
|
81
|
-
onContinue={() => setPhase('editing')}
|
|
82
|
-
/>
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (phase === 'editing') {
|
|
87
|
-
return (
|
|
88
|
-
<EditView
|
|
89
|
-
onSubmit={() => {
|
|
90
|
-
setPhase('confirmation')
|
|
91
|
-
setMode('form-confirmation')
|
|
92
|
-
}}
|
|
93
|
-
onEditDescription={handleEditDescription}
|
|
94
|
-
/>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (phase === 'confirmation') {
|
|
99
|
-
return (
|
|
100
|
-
<EditConfirmView
|
|
101
|
-
onConfirm={handleConfirm}
|
|
102
|
-
onBack={() => {
|
|
103
|
-
setPhase('editing')
|
|
104
|
-
setMode('form-navigation')
|
|
105
|
-
focus('edit-confirm-btn')
|
|
106
|
-
}}
|
|
107
|
-
/>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return null
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function EditWizard(props: EditWizardProps) {
|
|
115
|
-
const initialFormState = useMemo(
|
|
116
|
-
() => props.snapshot?.formState ?? manifestToFormState(props.context.manifest),
|
|
117
|
-
[props.snapshot?.formState, props.context.manifest],
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<FocusModeProvider>
|
|
122
|
-
<FocusOrderProvider initialFocusId={props.snapshot?.focusedId}>
|
|
123
|
-
<FormStateProvider initialState={initialFormState}>
|
|
124
|
-
<EditWizardInner {...props} />
|
|
125
|
-
</FormStateProvider>
|
|
126
|
-
</FocusOrderProvider>
|
|
127
|
-
</FocusModeProvider>
|
|
128
|
-
)
|
|
129
|
-
}
|
package/src/version.ts
DELETED
package/tsconfig.json
DELETED