agent-facets 0.1.2 → 0.2.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/.package.json.bak +44 -0
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +33 -0
- package/dist/facet +0 -0
- package/package.json +7 -4
- package/src/__tests__/cli.test.ts +69 -26
- package/src/__tests__/create-build.test.ts +32 -12
- package/src/__tests__/edit-integration.test.ts +171 -0
- package/src/__tests__/resolve-dir.test.ts +95 -0
- package/src/commands/build.ts +17 -4
- package/src/commands/create/index.ts +51 -5
- package/src/commands/create/wizard.tsx +66 -15
- package/src/commands/create-scaffold.ts +14 -10
- package/src/commands/edit/index.ts +144 -0
- package/src/commands/edit/wizard.tsx +74 -0
- package/src/commands/resolve-dir.ts +98 -0
- package/src/commands.ts +11 -2
- package/src/help.ts +17 -10
- package/src/index.ts +2 -1
- package/src/run.ts +32 -5
- package/src/tui/components/asset-description.tsx +17 -0
- package/src/tui/components/asset-field-picker.tsx +78 -0
- package/src/tui/components/asset-inline-input.tsx +13 -1
- package/src/tui/components/asset-item.tsx +3 -7
- package/src/tui/components/asset-section.tsx +72 -26
- package/src/tui/components/reconciliation-item.tsx +129 -0
- package/src/tui/components/stage-row.tsx +16 -4
- package/src/tui/context/focus-order-context.ts +8 -2
- package/src/tui/context/form-state-context.ts +34 -3
- package/src/tui/editor.ts +40 -0
- package/src/tui/views/build/build-view.tsx +43 -44
- package/src/tui/views/create/create-view.tsx +17 -13
- package/src/tui/views/create/wizard.tsx +35 -6
- package/src/tui/views/edit/edit-confirm-view.tsx +93 -0
- package/src/tui/views/edit/edit-types.ts +34 -0
- package/src/tui/views/edit/edit-view.tsx +140 -0
- package/src/tui/views/edit/manifest-to-form.ts +38 -0
- package/src/tui/views/edit/reconciliation-view.tsx +170 -0
- package/src/tui/views/edit/use-edit-session.ts +125 -0
- package/src/tui/views/edit/wizard.tsx +129 -0
package/src/commands/build.ts
CHANGED
|
@@ -2,12 +2,21 @@ import { render } from 'ink'
|
|
|
2
2
|
import { createElement } from 'react'
|
|
3
3
|
import type { Command } from '../commands.ts'
|
|
4
4
|
import { BuildView } from '../tui/views/build/build-view.tsx'
|
|
5
|
+
import { resolveTargetDir } from './resolve-dir.ts'
|
|
5
6
|
|
|
6
7
|
export const buildCommand: Command = {
|
|
7
8
|
name: 'build',
|
|
8
9
|
description: 'Build a facet from the current directory',
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
11
20
|
|
|
12
21
|
// Track result for stdout summary after Ink exits
|
|
13
22
|
let buildName = ''
|
|
@@ -35,10 +44,14 @@ export const buildCommand: Command = {
|
|
|
35
44
|
await instance.waitUntilExit()
|
|
36
45
|
// Ink has unmounted — print stdout summary for scroll-back
|
|
37
46
|
const shortHash = integrity.length > 20 ? `${integrity.slice(0, 20)}...` : integrity
|
|
38
|
-
process.stdout.write(
|
|
47
|
+
process.stdout.write(
|
|
48
|
+
`✓ Built ${buildName} v${buildVersion} → ${displayDir}/dist/ (${artifactCount} assets, ${shortHash})\n`,
|
|
49
|
+
)
|
|
39
50
|
return 0
|
|
40
51
|
} catch {
|
|
41
|
-
process.stdout.write(
|
|
52
|
+
process.stdout.write(
|
|
53
|
+
`✗ Build failed — ${errorCount} error${errorCount !== 1 ? 's' : ''}. Run \`facet edit${args[0] ? ` ${displayDir}` : ''}\` to fix.\n`,
|
|
54
|
+
)
|
|
42
55
|
return 1
|
|
43
56
|
}
|
|
44
57
|
},
|
|
@@ -1,15 +1,61 @@
|
|
|
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'
|
|
1
5
|
import type { Command } from '../../commands.ts'
|
|
2
6
|
import { writeScaffold } from '../create-scaffold.ts'
|
|
7
|
+
import { resolveTargetDir } from '../resolve-dir.ts'
|
|
3
8
|
import { runCreateWizardInk } from './wizard.tsx'
|
|
4
9
|
|
|
5
10
|
export type { CreateOptions } from '../create-scaffold.ts'
|
|
6
11
|
export { writeScaffold } from '../create-scaffold.ts'
|
|
7
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
|
+
|
|
8
26
|
export const createCommand: Command = {
|
|
9
27
|
name: 'create',
|
|
10
28
|
description: 'Create a new facet project interactively',
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
|
13
59
|
|
|
14
60
|
const opts = await runCreateWizardInk()
|
|
15
61
|
if (!opts) {
|
|
@@ -19,11 +65,11 @@ export const createCommand: Command = {
|
|
|
19
65
|
|
|
20
66
|
const files = await writeScaffold(opts, targetDir)
|
|
21
67
|
|
|
22
|
-
console.log(`\nFacet created: ${opts.name}`)
|
|
68
|
+
console.log(`\nFacet created: ${opts.name} → ${displayDir}`)
|
|
23
69
|
for (const file of files) {
|
|
24
|
-
console.log(` ${file}`)
|
|
70
|
+
console.log(` ${displayDir}/${file}`)
|
|
25
71
|
}
|
|
26
|
-
console.log(
|
|
72
|
+
console.log(`\nRun "facet build${args[0] ? ` ${displayDir}` : ''}" to validate your facet.`)
|
|
27
73
|
|
|
28
74
|
return 0
|
|
29
75
|
},
|
|
@@ -1,24 +1,75 @@
|
|
|
1
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'
|
|
2
5
|
import { CreateWizard } from '../../tui/views/create/wizard.tsx'
|
|
3
6
|
import type { CreateOptions } from '../create-scaffold.ts'
|
|
4
7
|
|
|
8
|
+
interface EditorRequest {
|
|
9
|
+
section: AssetSectionKey
|
|
10
|
+
name: string
|
|
11
|
+
description: string
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
export async function runCreateWizardInk(): Promise<CreateOptions | null> {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
8
22
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
)
|
|
19
42
|
|
|
20
|
-
|
|
21
|
-
resolve(result)
|
|
43
|
+
instance.waitUntilExit().then(() => resolve())
|
|
22
44
|
})
|
|
23
|
-
|
|
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
|
|
24
75
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { FACET_MANIFEST_FILE } from '@agent-facets/core'
|
|
3
|
+
import { FACET_MANIFEST_FILE, KEBAB_CASE } from '@agent-facets/core'
|
|
4
4
|
|
|
5
5
|
// --- Types ---
|
|
6
6
|
|
|
@@ -13,9 +13,13 @@ export interface CreateOptions {
|
|
|
13
13
|
commands: string[]
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// --- Defaults ---
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_VERSION = '0.0.0'
|
|
19
|
+
|
|
16
20
|
// --- Validation ---
|
|
17
21
|
|
|
18
|
-
export
|
|
22
|
+
export { KEBAB_CASE }
|
|
19
23
|
export const SEMVER = /^\d+\.\d+\.\d+$/
|
|
20
24
|
|
|
21
25
|
export function isValidKebabCase(value: string): boolean {
|
|
@@ -35,7 +39,7 @@ function toTitleCase(kebab: string): string {
|
|
|
35
39
|
.join(' ')
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function skillTemplate(name: string): string {
|
|
42
|
+
export function skillTemplate(name: string): string {
|
|
39
43
|
return `# ${toTitleCase(name)}
|
|
40
44
|
|
|
41
45
|
<!-- This is a starter skill template. Replace this content with your skill's instructions. -->
|
|
@@ -54,7 +58,7 @@ Describe what this skill teaches or what guidelines it provides.
|
|
|
54
58
|
`
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
function agentTemplate(name: string): string {
|
|
61
|
+
export function agentTemplate(name: string): string {
|
|
58
62
|
return `# ${toTitleCase(name)}
|
|
59
63
|
|
|
60
64
|
<!-- This is a starter agent template. Replace this content with your agent's prompt. -->
|
|
@@ -72,7 +76,7 @@ Describe this agent's role and responsibilities.
|
|
|
72
76
|
`
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
function commandTemplate(name: string): string {
|
|
79
|
+
export function commandTemplate(name: string): string {
|
|
76
80
|
return `# ${toTitleCase(name)}
|
|
77
81
|
|
|
78
82
|
<!-- This is a starter command template. Replace this content with your command's prompt. -->
|
|
@@ -134,7 +138,7 @@ export function generateManifest(opts: CreateOptions): string {
|
|
|
134
138
|
export function previewFiles(opts: CreateOptions): string[] {
|
|
135
139
|
const files: string[] = [FACET_MANIFEST_FILE]
|
|
136
140
|
for (const skill of opts.skills) {
|
|
137
|
-
files.push(`skills/${skill}.md`)
|
|
141
|
+
files.push(`skills/${skill}/SKILL.md`)
|
|
138
142
|
}
|
|
139
143
|
for (const agent of opts.agents) {
|
|
140
144
|
files.push(`agents/${agent}.md`)
|
|
@@ -155,11 +159,11 @@ export async function writeScaffold(opts: CreateOptions, targetDir: string): Pro
|
|
|
155
159
|
await Bun.write(manifestPath, generateManifest(opts))
|
|
156
160
|
files.push(FACET_MANIFEST_FILE)
|
|
157
161
|
|
|
158
|
-
// Write skill files
|
|
162
|
+
// Write skill files (Agent Skills directory convention: skills/<name>/SKILL.md)
|
|
159
163
|
for (const skill of opts.skills) {
|
|
160
|
-
await mkdir(join(targetDir, 'skills'), { recursive: true })
|
|
161
|
-
await Bun.write(join(targetDir, `skills/${skill}.md`), skillTemplate(skill))
|
|
162
|
-
files.push(`skills/${skill}.md`)
|
|
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`)
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
// Write agent files
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdir, stat } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
import { FACET_MANIFEST_FILE } from '@agent-facets/core'
|
|
4
|
+
|
|
5
|
+
export interface ResolvedDir {
|
|
6
|
+
ok: true
|
|
7
|
+
dir: string
|
|
8
|
+
display: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ResolvedDirError {
|
|
12
|
+
ok: false
|
|
13
|
+
message: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ResolveResult = ResolvedDir | ResolvedDirError
|
|
17
|
+
|
|
18
|
+
export interface ResolveOptions {
|
|
19
|
+
mustExist: boolean
|
|
20
|
+
facetMustExist?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates and resolves a directory argument for CLI commands.
|
|
25
|
+
*
|
|
26
|
+
* Handles:
|
|
27
|
+
* - No argument → uses process.cwd(), display as '.'
|
|
28
|
+
* - Argument ending with facet.json → silently uses parent directory
|
|
29
|
+
* - Argument is a non-directory file → error
|
|
30
|
+
* - Directory doesn't exist + mustExist false → auto-creates it
|
|
31
|
+
* - Directory doesn't exist + mustExist true → error
|
|
32
|
+
* - facetMustExist true but no facet.json → error
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveTargetDir(arg: string | undefined, opts: ResolveOptions): Promise<ResolveResult> {
|
|
35
|
+
const display = arg || '.'
|
|
36
|
+
|
|
37
|
+
// No argument → current directory
|
|
38
|
+
if (!arg) {
|
|
39
|
+
const dir = process.cwd()
|
|
40
|
+
|
|
41
|
+
if (opts.facetMustExist) {
|
|
42
|
+
const manifestExists = await Bun.file(join(dir, FACET_MANIFEST_FILE)).exists()
|
|
43
|
+
if (!manifestExists) {
|
|
44
|
+
return { ok: false, message: `No ${FACET_MANIFEST_FILE} found in ${display}` }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { ok: true, dir, display }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Pointing to facet.json directly → use parent directory
|
|
52
|
+
if (arg === FACET_MANIFEST_FILE || arg.endsWith(`/${FACET_MANIFEST_FILE}`)) {
|
|
53
|
+
const dir = resolve(dirname(arg))
|
|
54
|
+
|
|
55
|
+
const dirStat = await stat(dir).catch(() => null)
|
|
56
|
+
if (!dirStat?.isDirectory()) {
|
|
57
|
+
return { ok: false, message: `Directory does not exist: ${dirname(arg)}` }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (opts.facetMustExist) {
|
|
61
|
+
const manifestExists = await Bun.file(join(dir, FACET_MANIFEST_FILE)).exists()
|
|
62
|
+
if (!manifestExists) {
|
|
63
|
+
return { ok: false, message: `No ${FACET_MANIFEST_FILE} found in ${display}` }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { ok: true, dir, display: dirname(arg) || '.' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if the path exists
|
|
71
|
+
const pathStat = await stat(arg).catch(() => null)
|
|
72
|
+
|
|
73
|
+
// Path exists but is a file, not a directory
|
|
74
|
+
if (pathStat && !pathStat.isDirectory()) {
|
|
75
|
+
return { ok: false, message: `Expected a directory, not a file: ${arg}` }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Path doesn't exist
|
|
79
|
+
if (!pathStat) {
|
|
80
|
+
if (opts.mustExist) {
|
|
81
|
+
return { ok: false, message: `Directory does not exist: ${arg}` }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Auto-create for commands that allow it (e.g., create)
|
|
85
|
+
await mkdir(arg, { recursive: true })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const dir = resolve(arg)
|
|
89
|
+
|
|
90
|
+
if (opts.facetMustExist) {
|
|
91
|
+
const manifestExists = await Bun.file(join(dir, FACET_MANIFEST_FILE)).exists()
|
|
92
|
+
if (!manifestExists) {
|
|
93
|
+
return { ok: false, message: `No ${FACET_MANIFEST_FILE} found in ${display}` }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { ok: true, dir, display }
|
|
98
|
+
}
|
package/src/commands.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { buildCommand } from './commands/build.ts'
|
|
2
2
|
import { createCommand } from './commands/create/index.ts'
|
|
3
|
+
import { editCommand } from './commands/edit/index.ts'
|
|
4
|
+
|
|
5
|
+
export type FlagDef = {
|
|
6
|
+
type: 'boolean' | 'string'
|
|
7
|
+
description: string
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
export type Command = {
|
|
5
11
|
name: string
|
|
6
12
|
description: string
|
|
7
|
-
|
|
13
|
+
usage?: string
|
|
14
|
+
flags?: Record<string, FlagDef>
|
|
15
|
+
run: (args: string[], flags: Record<string, unknown>) => Promise<number>
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
function stubCommand(name: string, description: string): Command {
|
|
11
19
|
return {
|
|
12
20
|
name,
|
|
13
21
|
description,
|
|
14
|
-
run: async () => {
|
|
22
|
+
run: async (_args, _flags) => {
|
|
15
23
|
console.log(`"${name}" is not yet implemented.`)
|
|
16
24
|
return 0
|
|
17
25
|
},
|
|
@@ -22,6 +30,7 @@ export const commands: Record<string, Command> = {
|
|
|
22
30
|
add: stubCommand('add', 'Add a facet to the project'),
|
|
23
31
|
build: buildCommand,
|
|
24
32
|
create: createCommand,
|
|
33
|
+
edit: editCommand,
|
|
25
34
|
info: stubCommand('info', 'Show information about a facet'),
|
|
26
35
|
install: stubCommand('install', 'Install all facets from the lockfile'),
|
|
27
36
|
list: stubCommand('list', 'List installed facets'),
|
package/src/help.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { Command } from './commands.ts'
|
|
2
|
-
import { commands } from './commands.ts'
|
|
3
2
|
import { version } from './version.ts'
|
|
4
3
|
|
|
5
|
-
export function printGlobalHelp(): void {
|
|
4
|
+
export function printGlobalHelp(commands: Record<string, Command>): void {
|
|
6
5
|
const entries = Object.values(commands)
|
|
7
6
|
const maxNameLength = Math.max(...entries.map((c) => c.name.length))
|
|
8
7
|
|
|
@@ -23,14 +22,22 @@ export function printGlobalHelp(): void {
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
export function printCommandHelp(command: Command): void {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
|
|
25
|
+
const usage = command.usage ? ` ${command.usage}` : ''
|
|
26
|
+
|
|
27
|
+
const lines = [`Usage: facet ${command.name}${usage} [options]`, '', ` ${command.description}`, '', 'Options:']
|
|
28
|
+
|
|
29
|
+
if (command.flags) {
|
|
30
|
+
const flagEntries = Object.entries(command.flags)
|
|
31
|
+
const maxFlagLength = Math.max(...flagEntries.map(([name]) => `--${name}`.length), '--help'.length)
|
|
32
|
+
|
|
33
|
+
for (const [name, def] of flagEntries) {
|
|
34
|
+
lines.push(` ${`--${name}`.padEnd(maxFlagLength + 4)}${def.description}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
lines.push(` ${'--help'.padEnd(maxFlagLength + 4)}Show help`)
|
|
38
|
+
} else {
|
|
39
|
+
lines.push(' --help Show help')
|
|
40
|
+
}
|
|
34
41
|
|
|
35
42
|
console.log(lines.join('\n'))
|
|
36
43
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { commands } from './commands.ts'
|
|
1
2
|
import { run } from './run.ts'
|
|
2
3
|
|
|
3
4
|
try {
|
|
4
|
-
const code = await run(process.argv.slice(2))
|
|
5
|
+
const code = await run(process.argv.slice(2), commands)
|
|
5
6
|
process.exit(code)
|
|
6
7
|
} catch (error) {
|
|
7
8
|
console.error(error instanceof Error ? error.message : 'An unexpected error occurred.')
|