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
package/src/commands/build.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { render } from 'ink'
|
|
2
|
-
import { createElement } from 'react'
|
|
3
|
-
import type { Command } from '../commands.ts'
|
|
4
|
-
import { BuildView } from '../tui/views/build/build-view.tsx'
|
|
5
|
-
import { resolveTargetDir } from './resolve-dir.ts'
|
|
6
|
-
|
|
7
|
-
export const buildCommand: Command = {
|
|
8
|
-
name: 'build',
|
|
9
|
-
description: 'Build a facet from the current directory',
|
|
10
|
-
usage: '[directory]',
|
|
11
|
-
run: async (args: string[], _flags: Record<string, unknown>): Promise<number> => {
|
|
12
|
-
const resolved = await resolveTargetDir(args[0], { mustExist: true, facetMustExist: true })
|
|
13
|
-
if (!resolved.ok) {
|
|
14
|
-
console.error(resolved.message)
|
|
15
|
-
return 1
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const rootDir = resolved.dir
|
|
19
|
-
const displayDir = resolved.display
|
|
20
|
-
|
|
21
|
-
// Track result for stdout summary after Ink exits
|
|
22
|
-
let buildName = ''
|
|
23
|
-
let buildVersion = ''
|
|
24
|
-
let artifactCount = 0
|
|
25
|
-
let integrity = ''
|
|
26
|
-
let errorCount = 0
|
|
27
|
-
|
|
28
|
-
const instance = render(
|
|
29
|
-
createElement(BuildView, {
|
|
30
|
-
rootDir,
|
|
31
|
-
onSuccess: (name: string, version: string, fileCount: number, hash: string) => {
|
|
32
|
-
buildName = name
|
|
33
|
-
buildVersion = version
|
|
34
|
-
artifactCount = fileCount
|
|
35
|
-
integrity = hash
|
|
36
|
-
},
|
|
37
|
-
onFailure: (count: number) => {
|
|
38
|
-
errorCount = count
|
|
39
|
-
},
|
|
40
|
-
}),
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
await instance.waitUntilExit()
|
|
45
|
-
// Ink has unmounted — print stdout summary for scroll-back
|
|
46
|
-
const shortHash = integrity.length > 20 ? `${integrity.slice(0, 20)}...` : integrity
|
|
47
|
-
process.stdout.write(
|
|
48
|
-
`✓ Built ${buildName} v${buildVersion} → ${displayDir}/dist/ (${artifactCount} assets, ${shortHash})\n`,
|
|
49
|
-
)
|
|
50
|
-
return 0
|
|
51
|
-
} catch {
|
|
52
|
-
process.stdout.write(
|
|
53
|
-
`✗ Build failed — ${errorCount} error${errorCount !== 1 ? 's' : ''}. Run \`facet edit${args[0] ? ` ${displayDir}` : ''}\` to fix.\n`,
|
|
54
|
-
)
|
|
55
|
-
return 1
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import { createInterface } from 'node:readline'
|
|
3
|
-
import { FACET_MANIFEST_FILE } from '@agent-facets/core'
|
|
4
|
-
import { type } from 'arktype'
|
|
5
|
-
import type { Command } from '../../commands.ts'
|
|
6
|
-
import { writeScaffold } from '../create-scaffold.ts'
|
|
7
|
-
import { resolveTargetDir } from '../resolve-dir.ts'
|
|
8
|
-
import { runCreateWizardInk } from './wizard.tsx'
|
|
9
|
-
|
|
10
|
-
export type { CreateOptions } from '../create-scaffold.ts'
|
|
11
|
-
export { writeScaffold } from '../create-scaffold.ts'
|
|
12
|
-
|
|
13
|
-
const CreateFlags = type({ 'force?': 'boolean' })
|
|
14
|
-
|
|
15
|
-
async function confirmOverwrite(display: string): Promise<boolean> {
|
|
16
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
17
|
-
|
|
18
|
-
return new Promise((resolve) => {
|
|
19
|
-
rl.question(`A facet already exists in ${display}. Overwrite? (y/N) `, (answer) => {
|
|
20
|
-
rl.close()
|
|
21
|
-
resolve(answer.toLowerCase() === 'y')
|
|
22
|
-
})
|
|
23
|
-
})
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const createCommand: Command = {
|
|
27
|
-
name: 'create',
|
|
28
|
-
description: 'Create a new facet project interactively',
|
|
29
|
-
usage: '[directory]',
|
|
30
|
-
flags: {
|
|
31
|
-
force: { type: 'boolean', description: 'Overwrite existing facet.json' },
|
|
32
|
-
},
|
|
33
|
-
run: async (args: string[], flags: Record<string, unknown>): Promise<number> => {
|
|
34
|
-
const resolved = await resolveTargetDir(args[0], { mustExist: false })
|
|
35
|
-
if (!resolved.ok) {
|
|
36
|
-
console.error(resolved.message)
|
|
37
|
-
return 1
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const targetDir = resolved.dir
|
|
41
|
-
const displayDir = resolved.display
|
|
42
|
-
|
|
43
|
-
// Validate flags via Arktype
|
|
44
|
-
const validatedFlags = CreateFlags(flags)
|
|
45
|
-
if (validatedFlags instanceof type.errors) {
|
|
46
|
-
console.error(`Invalid flags: ${validatedFlags.summary}`)
|
|
47
|
-
return 1
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Overwrite protection
|
|
51
|
-
const manifestExists = await Bun.file(join(targetDir, FACET_MANIFEST_FILE)).exists()
|
|
52
|
-
if (manifestExists && !validatedFlags.force) {
|
|
53
|
-
const confirmed = await confirmOverwrite(displayDir)
|
|
54
|
-
if (!confirmed) {
|
|
55
|
-
console.log('Cancelled.')
|
|
56
|
-
return 1
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const opts = await runCreateWizardInk()
|
|
61
|
-
if (!opts) {
|
|
62
|
-
console.log('\nCancelled.')
|
|
63
|
-
return 1
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const files = await writeScaffold(opts, targetDir)
|
|
67
|
-
|
|
68
|
-
console.log(`\nFacet created: ${opts.name} → ${displayDir}`)
|
|
69
|
-
for (const file of files) {
|
|
70
|
-
console.log(` ${displayDir}/${file}`)
|
|
71
|
-
}
|
|
72
|
-
console.log(`\nRun "facet build${args[0] ? ` ${displayDir}` : ''}" to validate your facet.`)
|
|
73
|
-
|
|
74
|
-
return 0
|
|
75
|
-
},
|
|
76
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { render } from 'ink'
|
|
2
|
-
import type { AssetSectionKey } from '../../tui/context/form-state-context.ts'
|
|
3
|
-
import { openInEditorSync } from '../../tui/editor.ts'
|
|
4
|
-
import type { WizardSnapshot } from '../../tui/views/create/wizard.tsx'
|
|
5
|
-
import { CreateWizard } from '../../tui/views/create/wizard.tsx'
|
|
6
|
-
import type { CreateOptions } from '../create-scaffold.ts'
|
|
7
|
-
|
|
8
|
-
interface EditorRequest {
|
|
9
|
-
section: AssetSectionKey
|
|
10
|
-
name: string
|
|
11
|
-
description: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function runCreateWizardInk(): Promise<CreateOptions | null> {
|
|
15
|
-
let result: CreateOptions | null = null
|
|
16
|
-
let snapshot: WizardSnapshot | undefined
|
|
17
|
-
let pendingEditor: EditorRequest | null = null
|
|
18
|
-
let done = false
|
|
19
|
-
|
|
20
|
-
while (!done) {
|
|
21
|
-
pendingEditor = null
|
|
22
|
-
|
|
23
|
-
await new Promise<void>((resolve) => {
|
|
24
|
-
const instance = render(
|
|
25
|
-
<CreateWizard
|
|
26
|
-
snapshot={snapshot}
|
|
27
|
-
onComplete={(opts) => {
|
|
28
|
-
result = opts
|
|
29
|
-
}}
|
|
30
|
-
onCancel={() => {
|
|
31
|
-
result = null
|
|
32
|
-
}}
|
|
33
|
-
onSnapshot={(s) => {
|
|
34
|
-
snapshot = s
|
|
35
|
-
}}
|
|
36
|
-
onRequestEditor={(section, name, description) => {
|
|
37
|
-
pendingEditor = { section, name, description }
|
|
38
|
-
instance.unmount()
|
|
39
|
-
}}
|
|
40
|
-
/>,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
instance.waitUntilExit().then(() => resolve())
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
if (pendingEditor) {
|
|
47
|
-
const req = pendingEditor as EditorRequest
|
|
48
|
-
const edited = openInEditorSync(req.description, `${req.name}.md`)
|
|
49
|
-
if (snapshot) {
|
|
50
|
-
const section = snapshot.form.assets[req.section]
|
|
51
|
-
snapshot = {
|
|
52
|
-
...snapshot,
|
|
53
|
-
selectedItem: undefined,
|
|
54
|
-
form: {
|
|
55
|
-
...snapshot.form,
|
|
56
|
-
assets: {
|
|
57
|
-
...snapshot.form.assets,
|
|
58
|
-
[req.section]: {
|
|
59
|
-
...section,
|
|
60
|
-
descriptions: {
|
|
61
|
-
...section.descriptions,
|
|
62
|
-
...(edited !== null ? { [req.name]: edited.trim() } : {}),
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
done = true
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return result
|
|
75
|
-
}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { mkdir } from 'node:fs/promises'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { FACET_MANIFEST_FILE, KEBAB_CASE } from '@agent-facets/core'
|
|
4
|
-
|
|
5
|
-
// --- Types ---
|
|
6
|
-
|
|
7
|
-
export interface CreateOptions {
|
|
8
|
-
name: string
|
|
9
|
-
version: string
|
|
10
|
-
description: string
|
|
11
|
-
skills: string[]
|
|
12
|
-
agents: string[]
|
|
13
|
-
commands: string[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// --- Defaults ---
|
|
17
|
-
|
|
18
|
-
export const DEFAULT_VERSION = '0.0.0'
|
|
19
|
-
|
|
20
|
-
// --- Validation ---
|
|
21
|
-
|
|
22
|
-
export { KEBAB_CASE }
|
|
23
|
-
export const SEMVER = /^\d+\.\d+\.\d+$/
|
|
24
|
-
|
|
25
|
-
export function isValidKebabCase(value: string): boolean {
|
|
26
|
-
return KEBAB_CASE.test(value)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function isValidSemVer(value: string): boolean {
|
|
30
|
-
return SEMVER.test(value)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// --- Template generation ---
|
|
34
|
-
|
|
35
|
-
function toTitleCase(kebab: string): string {
|
|
36
|
-
return kebab
|
|
37
|
-
.split('-')
|
|
38
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
39
|
-
.join(' ')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function skillTemplate(name: string): string {
|
|
43
|
-
return `# ${toTitleCase(name)}
|
|
44
|
-
|
|
45
|
-
<!-- This is a starter skill template. Replace this content with your skill's instructions. -->
|
|
46
|
-
<!-- Skills provide reusable knowledge and guidelines that agents and commands can reference. -->
|
|
47
|
-
<!-- A skill needs a description (required) — the description helps consumers decide -->
|
|
48
|
-
<!-- whether to use this skill. The prompt content is this file. -->
|
|
49
|
-
|
|
50
|
-
## Purpose
|
|
51
|
-
|
|
52
|
-
Describe what this skill teaches or what guidelines it provides.
|
|
53
|
-
|
|
54
|
-
## Guidelines
|
|
55
|
-
|
|
56
|
-
- Add your skill's guidelines here
|
|
57
|
-
- Each guideline should be clear and actionable
|
|
58
|
-
`
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function agentTemplate(name: string): string {
|
|
62
|
-
return `# ${toTitleCase(name)}
|
|
63
|
-
|
|
64
|
-
<!-- This is a starter agent template. Replace this content with your agent's prompt. -->
|
|
65
|
-
<!-- Agents are AI assistant personas with specific roles, behaviors, and tool access. -->
|
|
66
|
-
|
|
67
|
-
## Role
|
|
68
|
-
|
|
69
|
-
Describe this agent's role and responsibilities.
|
|
70
|
-
|
|
71
|
-
## Behavior
|
|
72
|
-
|
|
73
|
-
- Define how this agent should behave
|
|
74
|
-
- Specify what tools it should use
|
|
75
|
-
- Describe its communication style
|
|
76
|
-
`
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function commandTemplate(name: string): string {
|
|
80
|
-
return `# ${toTitleCase(name)}
|
|
81
|
-
|
|
82
|
-
<!-- This is a starter command template. Replace this content with your command's prompt. -->
|
|
83
|
-
<!-- Commands are user-invokable actions that perform specific tasks. -->
|
|
84
|
-
|
|
85
|
-
## Task
|
|
86
|
-
|
|
87
|
-
Describe what this command does when invoked.
|
|
88
|
-
|
|
89
|
-
## Steps
|
|
90
|
-
|
|
91
|
-
1. First step
|
|
92
|
-
2. Second step
|
|
93
|
-
3. Final step
|
|
94
|
-
`
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// --- Manifest generation ---
|
|
98
|
-
|
|
99
|
-
export function generateManifest(opts: CreateOptions): string {
|
|
100
|
-
const manifest: Record<string, unknown> = {
|
|
101
|
-
name: opts.name,
|
|
102
|
-
version: opts.version,
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (opts.description) {
|
|
106
|
-
manifest.description = opts.description
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (opts.skills.length > 0) {
|
|
110
|
-
const skills: Record<string, { description: string }> = {}
|
|
111
|
-
for (const skill of opts.skills) {
|
|
112
|
-
skills[skill] = { description: `A ${toTitleCase(skill)} skill` }
|
|
113
|
-
}
|
|
114
|
-
manifest.skills = skills
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (opts.agents.length > 0) {
|
|
118
|
-
const agents: Record<string, { description: string }> = {}
|
|
119
|
-
for (const agent of opts.agents) {
|
|
120
|
-
agents[agent] = { description: `A ${toTitleCase(agent)} agent` }
|
|
121
|
-
}
|
|
122
|
-
manifest.agents = agents
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (opts.commands.length > 0) {
|
|
126
|
-
const commands: Record<string, { description: string }> = {}
|
|
127
|
-
for (const command of opts.commands) {
|
|
128
|
-
commands[command] = { description: `A ${toTitleCase(command)} command` }
|
|
129
|
-
}
|
|
130
|
-
manifest.commands = commands
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return JSON.stringify(manifest, null, 2)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// --- File listing preview ---
|
|
137
|
-
|
|
138
|
-
export function previewFiles(opts: CreateOptions): string[] {
|
|
139
|
-
const files: string[] = [FACET_MANIFEST_FILE]
|
|
140
|
-
for (const skill of opts.skills) {
|
|
141
|
-
files.push(`skills/${skill}/SKILL.md`)
|
|
142
|
-
}
|
|
143
|
-
for (const agent of opts.agents) {
|
|
144
|
-
files.push(`agents/${agent}.md`)
|
|
145
|
-
}
|
|
146
|
-
for (const command of opts.commands) {
|
|
147
|
-
files.push(`commands/${command}.md`)
|
|
148
|
-
}
|
|
149
|
-
return files
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// --- Scaffold writing ---
|
|
153
|
-
|
|
154
|
-
export async function writeScaffold(opts: CreateOptions, targetDir: string): Promise<string[]> {
|
|
155
|
-
const files: string[] = []
|
|
156
|
-
|
|
157
|
-
// Write manifest
|
|
158
|
-
const manifestPath = join(targetDir, FACET_MANIFEST_FILE)
|
|
159
|
-
await Bun.write(manifestPath, generateManifest(opts))
|
|
160
|
-
files.push(FACET_MANIFEST_FILE)
|
|
161
|
-
|
|
162
|
-
// Write skill files (Agent Skills directory convention: skills/<name>/SKILL.md)
|
|
163
|
-
for (const skill of opts.skills) {
|
|
164
|
-
await mkdir(join(targetDir, 'skills', skill), { recursive: true })
|
|
165
|
-
await Bun.write(join(targetDir, `skills/${skill}/SKILL.md`), skillTemplate(skill))
|
|
166
|
-
files.push(`skills/${skill}/SKILL.md`)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Write agent files
|
|
170
|
-
for (const agent of opts.agents) {
|
|
171
|
-
await mkdir(join(targetDir, 'agents'), { recursive: true })
|
|
172
|
-
await Bun.write(join(targetDir, `agents/${agent}.md`), agentTemplate(agent))
|
|
173
|
-
files.push(`agents/${agent}.md`)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Write command files
|
|
177
|
-
for (const command of opts.commands) {
|
|
178
|
-
await mkdir(join(targetDir, 'commands'), { recursive: true })
|
|
179
|
-
await Bun.write(join(targetDir, `commands/${command}.md`), commandTemplate(command))
|
|
180
|
-
files.push(`commands/${command}.md`)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return files
|
|
184
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { mkdir } from 'node:fs/promises'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
type FacetManifest,
|
|
5
|
-
hasFrontMatter,
|
|
6
|
-
loadManifest,
|
|
7
|
-
reconcile,
|
|
8
|
-
scanAssets,
|
|
9
|
-
writeManifest,
|
|
10
|
-
} from '@agent-facets/core'
|
|
11
|
-
import type { Command } from '../../commands.ts'
|
|
12
|
-
import type { EditContext, EditOperation, EditResult, ReconciliationItem } from '../../tui/views/edit/edit-types.ts'
|
|
13
|
-
import { agentTemplate, commandTemplate, skillTemplate } from '../create-scaffold.ts'
|
|
14
|
-
import { resolveTargetDir } from '../resolve-dir.ts'
|
|
15
|
-
import { runEditWizardInk } from './wizard.tsx'
|
|
16
|
-
|
|
17
|
-
export async function buildEditContext(
|
|
18
|
-
rootDir: string,
|
|
19
|
-
): Promise<{ ok: true; context: EditContext } | { ok: false; exitCode: number }> {
|
|
20
|
-
// Load manifest
|
|
21
|
-
const loadResult = await loadManifest(rootDir)
|
|
22
|
-
if (!loadResult.ok) {
|
|
23
|
-
// Hard error — show errors and exit
|
|
24
|
-
console.error('Manifest is invalid:')
|
|
25
|
-
for (const err of loadResult.errors) {
|
|
26
|
-
console.error(` ${err.message}`)
|
|
27
|
-
}
|
|
28
|
-
console.error('\nFix facet.json and try again.')
|
|
29
|
-
return { ok: false, exitCode: 1 }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const manifest = loadResult.data
|
|
33
|
-
|
|
34
|
-
// Scan disk for assets
|
|
35
|
-
const discovered = await scanAssets(rootDir)
|
|
36
|
-
|
|
37
|
-
// Run reconciliation
|
|
38
|
-
const recon = reconcile(manifest, discovered)
|
|
39
|
-
|
|
40
|
-
// Build reconciliation items
|
|
41
|
-
const items: ReconciliationItem[] = []
|
|
42
|
-
|
|
43
|
-
for (const addition of recon.additions) {
|
|
44
|
-
items.push({ kind: 'addition', type: addition.type, name: addition.name, path: addition.path })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
for (const missing of recon.missing) {
|
|
48
|
-
items.push({ kind: 'missing', type: missing.type, name: missing.name, expectedPath: missing.expectedPath })
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check matched assets for front matter
|
|
52
|
-
for (const matched of recon.matched) {
|
|
53
|
-
const filePath = join(rootDir, matched.path)
|
|
54
|
-
const content = await Bun.file(filePath).text()
|
|
55
|
-
if (hasFrontMatter(content)) {
|
|
56
|
-
items.push({ kind: 'front-matter', type: matched.type, name: matched.name, path: matched.path })
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return { ok: true, context: { rootDir, manifest, reconciliationItems: items } }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function applyOperations(
|
|
64
|
-
manifest: FacetManifest,
|
|
65
|
-
operations: EditOperation[],
|
|
66
|
-
rootDir: string,
|
|
67
|
-
): Promise<void> {
|
|
68
|
-
for (const op of operations) {
|
|
69
|
-
switch (op.op) {
|
|
70
|
-
case 'write-manifest':
|
|
71
|
-
await writeManifest(manifest, rootDir)
|
|
72
|
-
break
|
|
73
|
-
|
|
74
|
-
case 'scaffold': {
|
|
75
|
-
if (op.type === 'skills') {
|
|
76
|
-
const dir = join(rootDir, 'skills', op.name)
|
|
77
|
-
await mkdir(dir, { recursive: true })
|
|
78
|
-
await Bun.write(join(dir, 'SKILL.md'), skillTemplate(op.name))
|
|
79
|
-
} else if (op.type === 'agents') {
|
|
80
|
-
await mkdir(join(rootDir, 'agents'), { recursive: true })
|
|
81
|
-
await Bun.write(join(rootDir, `agents/${op.name}.md`), agentTemplate(op.name))
|
|
82
|
-
} else if (op.type === 'commands') {
|
|
83
|
-
await mkdir(join(rootDir, 'commands'), { recursive: true })
|
|
84
|
-
await Bun.write(join(rootDir, `commands/${op.name}.md`), commandTemplate(op.name))
|
|
85
|
-
}
|
|
86
|
-
break
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
case 'delete-file': {
|
|
90
|
-
const path =
|
|
91
|
-
op.type === 'skills' ? join(rootDir, 'skills', op.name, 'SKILL.md') : join(rootDir, op.type, `${op.name}.md`)
|
|
92
|
-
try {
|
|
93
|
-
const { unlink } = await import('node:fs/promises')
|
|
94
|
-
await unlink(path)
|
|
95
|
-
} catch {
|
|
96
|
-
// File already gone — that's fine
|
|
97
|
-
}
|
|
98
|
-
break
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
case 'strip-front-matter': {
|
|
102
|
-
const { extractFrontMatter } = await import('@agent-facets/core')
|
|
103
|
-
const filePath = join(rootDir, op.path)
|
|
104
|
-
const raw = await Bun.file(filePath).text()
|
|
105
|
-
const { content } = extractFrontMatter(raw)
|
|
106
|
-
await Bun.write(filePath, content)
|
|
107
|
-
break
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export const editCommand: Command = {
|
|
114
|
-
name: 'edit',
|
|
115
|
-
description: 'Edit a facet project interactively',
|
|
116
|
-
usage: '[directory]',
|
|
117
|
-
run: async (args: string[], _flags: Record<string, unknown>): Promise<number> => {
|
|
118
|
-
const resolved = await resolveTargetDir(args[0], { mustExist: true, facetMustExist: true })
|
|
119
|
-
if (!resolved.ok) {
|
|
120
|
-
console.error(resolved.message)
|
|
121
|
-
return 1
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const rootDir = resolved.dir
|
|
125
|
-
const displayDir = resolved.display
|
|
126
|
-
|
|
127
|
-
const loaded = await buildEditContext(rootDir)
|
|
128
|
-
if (!loaded.ok) return loaded.exitCode
|
|
129
|
-
|
|
130
|
-
const result: EditResult = await runEditWizardInk(loaded.context)
|
|
131
|
-
|
|
132
|
-
if (result.outcome === 'cancelled') {
|
|
133
|
-
console.log('\nCancelled — no changes applied.')
|
|
134
|
-
return 1
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
await applyOperations(result.manifest, result.operations, rootDir)
|
|
138
|
-
|
|
139
|
-
console.log(`\nChanges applied to ${displayDir}`)
|
|
140
|
-
console.log(`Run "facet build${args[0] ? ` ${displayDir}` : ''}" to validate your facet.`)
|
|
141
|
-
|
|
142
|
-
return 0
|
|
143
|
-
},
|
|
144
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { render } from 'ink'
|
|
2
|
-
import type { AssetSectionKey } from '../../tui/context/form-state-context.ts'
|
|
3
|
-
import { openInEditorSync } from '../../tui/editor.ts'
|
|
4
|
-
import type { EditContext, EditResult } from '../../tui/views/edit/edit-types.ts'
|
|
5
|
-
import type { EditWizardSnapshot } from '../../tui/views/edit/wizard.tsx'
|
|
6
|
-
import { EditWizard } from '../../tui/views/edit/wizard.tsx'
|
|
7
|
-
|
|
8
|
-
interface EditorRequest {
|
|
9
|
-
section: AssetSectionKey
|
|
10
|
-
name: string
|
|
11
|
-
description: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function runEditWizardInk(context: EditContext): Promise<EditResult> {
|
|
15
|
-
let result: EditResult = { outcome: 'cancelled' }
|
|
16
|
-
let snapshot: EditWizardSnapshot | undefined
|
|
17
|
-
let pendingEditor: EditorRequest | null = null
|
|
18
|
-
let done = false
|
|
19
|
-
|
|
20
|
-
while (!done) {
|
|
21
|
-
pendingEditor = null
|
|
22
|
-
|
|
23
|
-
await new Promise<void>((resolve) => {
|
|
24
|
-
const instance = render(
|
|
25
|
-
<EditWizard
|
|
26
|
-
context={context}
|
|
27
|
-
snapshot={snapshot}
|
|
28
|
-
onComplete={(r) => {
|
|
29
|
-
result = r
|
|
30
|
-
}}
|
|
31
|
-
onSnapshot={(s) => {
|
|
32
|
-
snapshot = s
|
|
33
|
-
}}
|
|
34
|
-
onRequestEditor={(section, name, description) => {
|
|
35
|
-
pendingEditor = { section, name, description }
|
|
36
|
-
instance.unmount()
|
|
37
|
-
}}
|
|
38
|
-
/>,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
instance.waitUntilExit().then(() => resolve())
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
if (pendingEditor) {
|
|
45
|
-
const req = pendingEditor as EditorRequest
|
|
46
|
-
const edited = openInEditorSync(req.description, `${req.name}.md`)
|
|
47
|
-
if (snapshot) {
|
|
48
|
-
snapshot = {
|
|
49
|
-
...snapshot,
|
|
50
|
-
selectedItem: undefined,
|
|
51
|
-
formState: snapshot.formState
|
|
52
|
-
? {
|
|
53
|
-
...snapshot.formState,
|
|
54
|
-
assets: {
|
|
55
|
-
...snapshot.formState.assets,
|
|
56
|
-
[req.section]: {
|
|
57
|
-
...snapshot.formState.assets[req.section],
|
|
58
|
-
descriptions: {
|
|
59
|
-
...snapshot.formState.assets[req.section].descriptions,
|
|
60
|
-
...(edited !== null ? { [req.name]: edited.trim() } : {}),
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
: undefined,
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} else {
|
|
69
|
-
done = true
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return result
|
|
74
|
-
}
|