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,195 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
|
|
4
|
-
const CLI_PATH = resolve(import.meta.dir, '../../dist/facet')
|
|
5
|
-
const COMMAND_NAMES = ['add', 'build', 'create', 'edit', 'info', 'install', 'list', 'publish', 'remove', 'upgrade']
|
|
6
|
-
const STUB_COMMAND_NAMES = ['add', 'info', 'install', 'list', 'publish', 'remove', 'upgrade']
|
|
7
|
-
|
|
8
|
-
type ExecResult = {
|
|
9
|
-
stdout: string
|
|
10
|
-
stderr: string
|
|
11
|
-
exitCode: number
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function runCli(...args: string[]): Promise<ExecResult> {
|
|
15
|
-
const proc = Bun.spawn([CLI_PATH, ...args], {
|
|
16
|
-
stdout: 'pipe',
|
|
17
|
-
stderr: 'pipe',
|
|
18
|
-
})
|
|
19
|
-
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()])
|
|
20
|
-
const exitCode = await proc.exited
|
|
21
|
-
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// --- Help ---
|
|
25
|
-
|
|
26
|
-
describe('CLI — help', () => {
|
|
27
|
-
test('--help prints command list to stdout and exits 0', async () => {
|
|
28
|
-
const result = await runCli('--help')
|
|
29
|
-
expect(result.exitCode).toBe(0)
|
|
30
|
-
expect(result.stdout).toContain('Usage: facet <command>')
|
|
31
|
-
for (const cmd of COMMAND_NAMES) {
|
|
32
|
-
expect(result.stdout).toContain(cmd)
|
|
33
|
-
}
|
|
34
|
-
expect(result.stderr).toBe('')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('help command produces same output as --help', async () => {
|
|
38
|
-
const helpFlag = await runCli('--help')
|
|
39
|
-
const helpCommand = await runCli('help')
|
|
40
|
-
expect(helpCommand.exitCode).toBe(0)
|
|
41
|
-
expect(helpCommand.stdout).toBe(helpFlag.stdout)
|
|
42
|
-
expect(helpCommand.stderr).toBe('')
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
// --- Version ---
|
|
47
|
-
|
|
48
|
-
describe('CLI — version', () => {
|
|
49
|
-
test('--version prints version matching package.json and exits 0', async () => {
|
|
50
|
-
const pkg = await Bun.file(resolve(import.meta.dir, '../../package.json')).json()
|
|
51
|
-
const result = await runCli('--version')
|
|
52
|
-
expect(result.exitCode).toBe(0)
|
|
53
|
-
expect(result.stdout).toBe(pkg.version)
|
|
54
|
-
expect(result.stderr).toBe('')
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// --- Bare invocation ---
|
|
59
|
-
|
|
60
|
-
describe('CLI — bare invocation', () => {
|
|
61
|
-
test('no arguments prints help and exits 0', async () => {
|
|
62
|
-
const helpResult = await runCli('--help')
|
|
63
|
-
const bareResult = await runCli()
|
|
64
|
-
expect(bareResult.exitCode).toBe(0)
|
|
65
|
-
expect(bareResult.stdout).toBe(helpResult.stdout)
|
|
66
|
-
expect(bareResult.stderr).toBe('')
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
// --- Stub commands ---
|
|
71
|
-
|
|
72
|
-
describe('CLI — stub commands', () => {
|
|
73
|
-
test.each(STUB_COMMAND_NAMES)('"%s" prints not yet implemented with command name and exits 0', async (cmd) => {
|
|
74
|
-
const result = await runCli(cmd)
|
|
75
|
-
expect(result.exitCode).toBe(0)
|
|
76
|
-
expect(result.stdout).toContain(cmd)
|
|
77
|
-
expect(result.stdout).toContain('not yet implemented')
|
|
78
|
-
expect(result.stderr).toBe('')
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// --- Edit command dispatch ---
|
|
83
|
-
|
|
84
|
-
describe('CLI — edit command', () => {
|
|
85
|
-
test('edit with no manifest prints error and exits 1', async () => {
|
|
86
|
-
const result = await runCli('edit', import.meta.dir)
|
|
87
|
-
expect(result.exitCode).toBe(1)
|
|
88
|
-
expect(result.stderr).toContain('facet.json')
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// --- Unknown commands ---
|
|
93
|
-
|
|
94
|
-
describe('CLI — unknown commands', () => {
|
|
95
|
-
test('unknown command prints error to stderr and exits 1', async () => {
|
|
96
|
-
const result = await runCli('xyzzy')
|
|
97
|
-
expect(result.exitCode).toBe(1)
|
|
98
|
-
expect(result.stderr).toContain('Unknown command "xyzzy"')
|
|
99
|
-
expect(result.stdout).toBe('')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test('unknown command with close match includes "did you mean?" suggestion', async () => {
|
|
103
|
-
const result = await runCli('bild')
|
|
104
|
-
expect(result.exitCode).toBe(1)
|
|
105
|
-
expect(result.stderr).toContain('Unknown command "bild"')
|
|
106
|
-
expect(result.stderr).toContain('Did you mean "build"')
|
|
107
|
-
expect(result.stdout).toBe('')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('unknown command with no close match does not include suggestion', async () => {
|
|
111
|
-
const result = await runCli('xyzzy')
|
|
112
|
-
expect(result.exitCode).toBe(1)
|
|
113
|
-
expect(result.stderr).toContain('Unknown command "xyzzy"')
|
|
114
|
-
expect(result.stderr).not.toContain('Did you mean')
|
|
115
|
-
expect(result.stdout).toBe('')
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
// --- Per-command help ---
|
|
120
|
-
|
|
121
|
-
describe('CLI — per-command help', () => {
|
|
122
|
-
test('<command> --help prints command-specific help and exits 0', async () => {
|
|
123
|
-
const result = await runCli('build', '--help')
|
|
124
|
-
expect(result.exitCode).toBe(0)
|
|
125
|
-
expect(result.stdout).toContain('facet build')
|
|
126
|
-
expect(result.stdout).toContain('Build a facet from the current directory')
|
|
127
|
-
expect(result.stderr).toBe('')
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// --- Unexpected error ---
|
|
132
|
-
|
|
133
|
-
describe('CLI — unexpected error', () => {
|
|
134
|
-
test('unexpected error is thrown by run', async () => {
|
|
135
|
-
const { run } = await import('../run.ts')
|
|
136
|
-
|
|
137
|
-
const crashRegistry = {
|
|
138
|
-
crash: {
|
|
139
|
-
name: 'crash',
|
|
140
|
-
description: 'Throws an error',
|
|
141
|
-
run: async (_args: string[], _flags: Record<string, unknown>) => {
|
|
142
|
-
throw new Error('boom')
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
await expect(run(['crash'], crashRegistry)).rejects.toThrow('boom')
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
// --- Per-command flags ---
|
|
152
|
-
|
|
153
|
-
describe('CLI — per-command flags', () => {
|
|
154
|
-
test('create --help shows --force flag and usage', async () => {
|
|
155
|
-
const result = await runCli('create', '--help')
|
|
156
|
-
expect(result.exitCode).toBe(0)
|
|
157
|
-
expect(result.stdout).toContain('[directory]')
|
|
158
|
-
expect(result.stdout).toContain('--force')
|
|
159
|
-
expect(result.stdout).toContain('Overwrite existing facet.json')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
test('build --help shows directory usage', async () => {
|
|
163
|
-
const result = await runCli('build', '--help')
|
|
164
|
-
expect(result.exitCode).toBe(0)
|
|
165
|
-
expect(result.stdout).toContain('[directory]')
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
test('edit --help shows directory usage', async () => {
|
|
169
|
-
const result = await runCli('edit', '--help')
|
|
170
|
-
expect(result.exitCode).toBe(0)
|
|
171
|
-
expect(result.stdout).toContain('[directory]')
|
|
172
|
-
})
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// --- Directory validation ---
|
|
176
|
-
|
|
177
|
-
describe('CLI — directory validation', () => {
|
|
178
|
-
test('build with non-existent directory errors', async () => {
|
|
179
|
-
const result = await runCli('build', `/tmp/does-not-exist-${Date.now()}`)
|
|
180
|
-
expect(result.exitCode).toBe(1)
|
|
181
|
-
expect(result.stderr).toContain('does not exist')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('edit with non-existent directory errors', async () => {
|
|
185
|
-
const result = await runCli('edit', `/tmp/does-not-exist-${Date.now()}`)
|
|
186
|
-
expect(result.exitCode).toBe(1)
|
|
187
|
-
expect(result.stderr).toContain('does not exist')
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
test('build with file instead of directory errors', async () => {
|
|
191
|
-
const result = await runCli('build', import.meta.path)
|
|
192
|
-
expect(result.exitCode).toBe(1)
|
|
193
|
-
expect(result.stderr).toContain('Expected a directory')
|
|
194
|
-
})
|
|
195
|
-
})
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join, resolve } from 'node:path'
|
|
5
|
-
import { writeScaffold } from '../commands/create/index.ts'
|
|
6
|
-
import { DEFAULT_VERSION } from '../commands/create-scaffold.ts'
|
|
7
|
-
|
|
8
|
-
let testDir: string
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
testDir = await mkdtemp(join(tmpdir(), 'cli-create-build-test-'))
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
afterAll(async () => {
|
|
15
|
-
await rm(testDir, { recursive: true, force: true })
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
async function createFixtureDir(name: string): Promise<string> {
|
|
19
|
-
const dir = join(testDir, name)
|
|
20
|
-
await Bun.write(join(dir, '.keep'), '')
|
|
21
|
-
return dir
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const CLI_PATH = resolve(import.meta.dir, '../../dist/facet')
|
|
25
|
-
|
|
26
|
-
async function runCli(...args: string[]) {
|
|
27
|
-
const proc = Bun.spawn([CLI_PATH, ...args], {
|
|
28
|
-
stdout: 'pipe',
|
|
29
|
-
stderr: 'pipe',
|
|
30
|
-
env: { ...process.env, NO_COLOR: '1' },
|
|
31
|
-
})
|
|
32
|
-
const stdout = await new Response(proc.stdout).text()
|
|
33
|
-
const stderr = await new Response(proc.stderr).text()
|
|
34
|
-
const exitCode = await proc.exited
|
|
35
|
-
|
|
36
|
-
// Don't let build errors flood test output — capture but don't dump
|
|
37
|
-
if (exitCode !== 0 && stderr.trim()) {
|
|
38
|
-
const lines = stderr.trim().split('\n')
|
|
39
|
-
const summary =
|
|
40
|
-
lines.length > 3 ? [...lines.slice(0, 3), `... (${lines.length - 3} more lines)`].join('\n') : stderr.trim()
|
|
41
|
-
return { stdout: stdout.trim(), stderr: summary, exitCode }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// --- Scaffold generation (unit) ---
|
|
48
|
-
|
|
49
|
-
describe('writeScaffold', () => {
|
|
50
|
-
test('scaffolds with named assets across all types', async () => {
|
|
51
|
-
const dir = await createFixtureDir('scaffold-all')
|
|
52
|
-
const files = await writeScaffold(
|
|
53
|
-
{
|
|
54
|
-
name: 'my-facet',
|
|
55
|
-
version: DEFAULT_VERSION,
|
|
56
|
-
description: 'A test facet',
|
|
57
|
-
skills: ['code-review', 'testing-guide'],
|
|
58
|
-
agents: ['reviewer'],
|
|
59
|
-
commands: ['deploy'],
|
|
60
|
-
},
|
|
61
|
-
dir,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
expect(files).toContain('facet.json')
|
|
65
|
-
expect(files).toContain('skills/code-review/SKILL.md')
|
|
66
|
-
expect(files).toContain('skills/testing-guide/SKILL.md')
|
|
67
|
-
expect(files).toContain('agents/reviewer.md')
|
|
68
|
-
expect(files).toContain('commands/deploy.md')
|
|
69
|
-
|
|
70
|
-
// Verify manifest content (JSON)
|
|
71
|
-
const manifestText = await Bun.file(join(dir, 'facet.json')).text()
|
|
72
|
-
const manifest = JSON.parse(manifestText)
|
|
73
|
-
expect(manifest.name).toBe('my-facet')
|
|
74
|
-
expect(manifest.version).toBe(DEFAULT_VERSION)
|
|
75
|
-
expect(manifest.description).toBe('A test facet')
|
|
76
|
-
expect(manifest.skills).toBeDefined()
|
|
77
|
-
expect(manifest.skills['code-review']).toBeDefined()
|
|
78
|
-
expect(manifest.skills['testing-guide']).toBeDefined()
|
|
79
|
-
expect(manifest.agents).toBeDefined()
|
|
80
|
-
expect(manifest.agents.reviewer).toBeDefined()
|
|
81
|
-
expect(manifest.commands).toBeDefined()
|
|
82
|
-
expect(manifest.commands.deploy).toBeDefined()
|
|
83
|
-
|
|
84
|
-
// Verify starter files exist and have named template content
|
|
85
|
-
const skill = await Bun.file(join(dir, 'skills/code-review/SKILL.md')).text()
|
|
86
|
-
expect(skill).toContain('# Code Review')
|
|
87
|
-
|
|
88
|
-
const skill2 = await Bun.file(join(dir, 'skills/testing-guide/SKILL.md')).text()
|
|
89
|
-
expect(skill2).toContain('# Testing Guide')
|
|
90
|
-
|
|
91
|
-
const agent = await Bun.file(join(dir, 'agents/reviewer.md')).text()
|
|
92
|
-
expect(agent).toContain('# Reviewer')
|
|
93
|
-
|
|
94
|
-
const command = await Bun.file(join(dir, 'commands/deploy.md')).text()
|
|
95
|
-
expect(command).toContain('# Deploy')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test('scaffolds with only one skill', async () => {
|
|
99
|
-
const dir = await createFixtureDir('scaffold-skills-only')
|
|
100
|
-
const files = await writeScaffold(
|
|
101
|
-
{
|
|
102
|
-
name: 'minimal',
|
|
103
|
-
version: DEFAULT_VERSION,
|
|
104
|
-
description: '',
|
|
105
|
-
skills: ['minimal'],
|
|
106
|
-
agents: [],
|
|
107
|
-
commands: [],
|
|
108
|
-
},
|
|
109
|
-
dir,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
expect(files).toContain('facet.json')
|
|
113
|
-
expect(files).toContain('skills/minimal/SKILL.md')
|
|
114
|
-
expect(files).not.toContain('agents/')
|
|
115
|
-
expect(files).not.toContain('commands/')
|
|
116
|
-
|
|
117
|
-
const manifestText = await Bun.file(join(dir, 'facet.json')).text()
|
|
118
|
-
const manifest = JSON.parse(manifestText)
|
|
119
|
-
expect(manifest.skills).toBeDefined()
|
|
120
|
-
expect(manifest.agents).toBeUndefined()
|
|
121
|
-
expect(manifest.commands).toBeUndefined()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
test('version defaults to DEFAULT_VERSION (0.0.0)', async () => {
|
|
125
|
-
const dir = await createFixtureDir('scaffold-default-version')
|
|
126
|
-
await writeScaffold(
|
|
127
|
-
{
|
|
128
|
-
name: 'default-ver',
|
|
129
|
-
version: DEFAULT_VERSION,
|
|
130
|
-
description: 'Testing default version',
|
|
131
|
-
skills: ['example'],
|
|
132
|
-
agents: [],
|
|
133
|
-
commands: [],
|
|
134
|
-
},
|
|
135
|
-
dir,
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
const manifestText = await Bun.file(join(dir, 'facet.json')).text()
|
|
139
|
-
const manifest = JSON.parse(manifestText)
|
|
140
|
-
expect(manifest.version).toBe('0.0.0')
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
test('scaffolded project passes build', async () => {
|
|
144
|
-
const dir = await createFixtureDir('scaffold-buildable')
|
|
145
|
-
await writeScaffold(
|
|
146
|
-
{
|
|
147
|
-
name: 'buildable',
|
|
148
|
-
version: DEFAULT_VERSION,
|
|
149
|
-
description: 'A buildable facet',
|
|
150
|
-
skills: ['helper'],
|
|
151
|
-
agents: ['assistant'],
|
|
152
|
-
commands: [],
|
|
153
|
-
},
|
|
154
|
-
dir,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
// Run facet build against the scaffolded project
|
|
158
|
-
const result = await runCli('build', dir)
|
|
159
|
-
expect(result.exitCode).toBe(0)
|
|
160
|
-
expect(result.stdout).toContain('Built buildable')
|
|
161
|
-
|
|
162
|
-
// Verify dist/ output exists — archive + build manifest
|
|
163
|
-
const distArchive = await Bun.file(join(dir, `dist/buildable-${DEFAULT_VERSION}.facet`)).exists()
|
|
164
|
-
expect(distArchive).toBe(true)
|
|
165
|
-
|
|
166
|
-
const distManifest = await Bun.file(join(dir, 'dist/build-manifest.json')).exists()
|
|
167
|
-
expect(distManifest).toBe(true)
|
|
168
|
-
|
|
169
|
-
// No loose files
|
|
170
|
-
const looseManifest = await Bun.file(join(dir, 'dist/facet.json')).exists()
|
|
171
|
-
expect(looseManifest).toBe(false)
|
|
172
|
-
})
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// --- Build command (e2e) ---
|
|
176
|
-
|
|
177
|
-
describe('facet build', () => {
|
|
178
|
-
test('build succeeds on valid project', async () => {
|
|
179
|
-
const dir = await createFixtureDir('build-valid')
|
|
180
|
-
await Bun.write(join(dir, 'skills/review/SKILL.md'), '# Review skill content')
|
|
181
|
-
await Bun.write(
|
|
182
|
-
join(dir, 'facet.json'),
|
|
183
|
-
JSON.stringify({
|
|
184
|
-
name: 'test-facet',
|
|
185
|
-
version: '1.0.0',
|
|
186
|
-
skills: {
|
|
187
|
-
review: {
|
|
188
|
-
description: 'Code review skill',
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
}),
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
const result = await runCli('build', dir)
|
|
195
|
-
expect(result.exitCode).toBe(0)
|
|
196
|
-
expect(result.stdout).toContain('Built test-facet')
|
|
197
|
-
expect(result.stdout).toContain('sha256:')
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
test('build fails on missing manifest', async () => {
|
|
201
|
-
const dir = await createFixtureDir('build-no-manifest')
|
|
202
|
-
|
|
203
|
-
const result = await runCli('build', dir)
|
|
204
|
-
expect(result.exitCode).toBe(1)
|
|
205
|
-
expect(result.stderr).toContain('facet.json')
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('build fails on missing asset file', async () => {
|
|
209
|
-
const dir = await createFixtureDir('build-missing-file')
|
|
210
|
-
await Bun.write(
|
|
211
|
-
join(dir, 'facet.json'),
|
|
212
|
-
JSON.stringify({
|
|
213
|
-
name: 'test-facet',
|
|
214
|
-
version: '1.0.0',
|
|
215
|
-
skills: {
|
|
216
|
-
review: {
|
|
217
|
-
description: 'Code review skill',
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
}),
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
const result = await runCli('build', dir)
|
|
224
|
-
expect(result.exitCode).toBe(1)
|
|
225
|
-
expect(result.stdout).toContain('Build failed')
|
|
226
|
-
})
|
|
227
|
-
})
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdir } from 'node:fs/promises'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { runBuildPipeline } from '@agent-facets/core'
|
|
6
|
-
import dedent from 'dedent'
|
|
7
|
-
import { applyOperations, buildEditContext } from '../commands/edit/index.ts'
|
|
8
|
-
import type { EditOperation } from '../tui/views/edit/edit-types.ts'
|
|
9
|
-
|
|
10
|
-
async function createFixtureDir(name: string): Promise<string> {
|
|
11
|
-
const dir = join(tmpdir(), `facets-edit-integ-${name}-${Date.now()}`)
|
|
12
|
-
await mkdir(dir, { recursive: true })
|
|
13
|
-
return dir
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function writeManifest(dir: string, manifest: Record<string, unknown>): Promise<void> {
|
|
17
|
-
await Bun.write(join(dir, 'facet.json'), JSON.stringify(manifest, null, 2))
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('edit integration', () => {
|
|
21
|
-
test('buildEditContext detects new files on disk not in manifest', async () => {
|
|
22
|
-
const dir = await createFixtureDir('detect-additions')
|
|
23
|
-
await writeManifest(dir, {
|
|
24
|
-
name: 'test',
|
|
25
|
-
version: '1.0.0',
|
|
26
|
-
skills: { existing: { description: 'Existing skill' } },
|
|
27
|
-
})
|
|
28
|
-
await mkdir(join(dir, 'skills/existing'), { recursive: true })
|
|
29
|
-
await Bun.write(join(dir, 'skills/existing/SKILL.md'), '# Existing')
|
|
30
|
-
// Add a file not in manifest
|
|
31
|
-
await mkdir(join(dir, 'skills/new-one'), { recursive: true })
|
|
32
|
-
await Bun.write(join(dir, 'skills/new-one/SKILL.md'), '# New')
|
|
33
|
-
|
|
34
|
-
const result = await buildEditContext(dir)
|
|
35
|
-
expect(result.ok).toBe(true)
|
|
36
|
-
if (!result.ok) return
|
|
37
|
-
|
|
38
|
-
const additions = result.context.reconciliationItems.filter((i) => i.kind === 'addition')
|
|
39
|
-
expect(additions).toHaveLength(1)
|
|
40
|
-
expect(additions[0]?.name).toBe('new-one')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('buildEditContext detects missing files in manifest', async () => {
|
|
44
|
-
const dir = await createFixtureDir('detect-missing')
|
|
45
|
-
await writeManifest(dir, {
|
|
46
|
-
name: 'test',
|
|
47
|
-
version: '1.0.0',
|
|
48
|
-
skills: {
|
|
49
|
-
present: { description: 'Present' },
|
|
50
|
-
gone: { description: 'Gone' },
|
|
51
|
-
},
|
|
52
|
-
})
|
|
53
|
-
await mkdir(join(dir, 'skills/present'), { recursive: true })
|
|
54
|
-
await Bun.write(join(dir, 'skills/present/SKILL.md'), '# Present')
|
|
55
|
-
// 'gone' has no file on disk
|
|
56
|
-
|
|
57
|
-
const result = await buildEditContext(dir)
|
|
58
|
-
expect(result.ok).toBe(true)
|
|
59
|
-
if (!result.ok) return
|
|
60
|
-
|
|
61
|
-
const missing = result.context.reconciliationItems.filter((i) => i.kind === 'missing')
|
|
62
|
-
expect(missing).toHaveLength(1)
|
|
63
|
-
expect(missing[0]?.name).toBe('gone')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('buildEditContext detects front matter in matched files', async () => {
|
|
67
|
-
const dir = await createFixtureDir('detect-frontmatter')
|
|
68
|
-
await writeManifest(dir, {
|
|
69
|
-
name: 'test',
|
|
70
|
-
version: '1.0.0',
|
|
71
|
-
skills: { review: { description: 'Review' } },
|
|
72
|
-
})
|
|
73
|
-
await mkdir(join(dir, 'skills/review'), { recursive: true })
|
|
74
|
-
await Bun.write(
|
|
75
|
-
join(dir, 'skills/review/SKILL.md'),
|
|
76
|
-
dedent`
|
|
77
|
-
---
|
|
78
|
-
name: Review
|
|
79
|
-
---
|
|
80
|
-
# Review skill
|
|
81
|
-
`,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
const result = await buildEditContext(dir)
|
|
85
|
-
expect(result.ok).toBe(true)
|
|
86
|
-
if (!result.ok) return
|
|
87
|
-
|
|
88
|
-
const fm = result.context.reconciliationItems.filter((i) => i.kind === 'front-matter')
|
|
89
|
-
expect(fm).toHaveLength(1)
|
|
90
|
-
expect(fm[0]?.name).toBe('review')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
test('applyOperations scaffolds new skill files', async () => {
|
|
94
|
-
const dir = await createFixtureDir('scaffold-skill')
|
|
95
|
-
const manifest = {
|
|
96
|
-
name: 'test',
|
|
97
|
-
version: '1.0.0',
|
|
98
|
-
skills: { helper: { description: 'A helper skill' } },
|
|
99
|
-
}
|
|
100
|
-
const operations: EditOperation[] = [{ op: 'write-manifest' }, { op: 'scaffold', type: 'skills', name: 'helper' }]
|
|
101
|
-
|
|
102
|
-
await applyOperations(manifest, operations, dir)
|
|
103
|
-
|
|
104
|
-
const manifestExists = await Bun.file(join(dir, 'facet.json')).exists()
|
|
105
|
-
expect(manifestExists).toBe(true)
|
|
106
|
-
|
|
107
|
-
const skillExists = await Bun.file(join(dir, 'skills/helper/SKILL.md')).exists()
|
|
108
|
-
expect(skillExists).toBe(true)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('applyOperations strips front matter from files', async () => {
|
|
112
|
-
const dir = await createFixtureDir('strip-fm')
|
|
113
|
-
await mkdir(join(dir, 'skills/review'), { recursive: true })
|
|
114
|
-
await Bun.write(
|
|
115
|
-
join(dir, 'skills/review/SKILL.md'),
|
|
116
|
-
dedent`
|
|
117
|
-
---
|
|
118
|
-
name: Review
|
|
119
|
-
description: A review skill
|
|
120
|
-
---
|
|
121
|
-
# Review
|
|
122
|
-
Review all code.
|
|
123
|
-
`,
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
const manifest = {
|
|
127
|
-
name: 'test',
|
|
128
|
-
version: '1.0.0',
|
|
129
|
-
skills: { review: { description: 'A review skill' } },
|
|
130
|
-
}
|
|
131
|
-
const operations: EditOperation[] = [
|
|
132
|
-
{ op: 'write-manifest' },
|
|
133
|
-
{ op: 'strip-front-matter', type: 'skills', name: 'review', path: 'skills/review/SKILL.md' },
|
|
134
|
-
]
|
|
135
|
-
|
|
136
|
-
await applyOperations(manifest, operations, dir)
|
|
137
|
-
|
|
138
|
-
const content = await Bun.file(join(dir, 'skills/review/SKILL.md')).text()
|
|
139
|
-
expect(content).not.toContain('---')
|
|
140
|
-
expect(content).toContain('# Review')
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
test('applyOperations deletes removed asset files', async () => {
|
|
144
|
-
const dir = await createFixtureDir('delete-asset')
|
|
145
|
-
await mkdir(join(dir, 'skills/old'), { recursive: true })
|
|
146
|
-
await Bun.write(join(dir, 'skills/old/SKILL.md'), '# Old skill')
|
|
147
|
-
|
|
148
|
-
const manifest = { name: 'test', version: '1.0.0', skills: { remaining: { description: 'Remaining' } } }
|
|
149
|
-
const operations: EditOperation[] = [{ op: 'write-manifest' }, { op: 'delete-file', type: 'skills', name: 'old' }]
|
|
150
|
-
|
|
151
|
-
await applyOperations(manifest, operations, dir)
|
|
152
|
-
|
|
153
|
-
const deleted = await Bun.file(join(dir, 'skills/old/SKILL.md')).exists()
|
|
154
|
-
expect(deleted).toBe(false)
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test('scaffold then build succeeds end-to-end', async () => {
|
|
158
|
-
const dir = await createFixtureDir('scaffold-then-build')
|
|
159
|
-
const manifest = {
|
|
160
|
-
name: 'test-facet',
|
|
161
|
-
version: '1.0.0',
|
|
162
|
-
skills: { example: { description: 'An example skill' } },
|
|
163
|
-
}
|
|
164
|
-
const operations: EditOperation[] = [{ op: 'write-manifest' }, { op: 'scaffold', type: 'skills', name: 'example' }]
|
|
165
|
-
|
|
166
|
-
await applyOperations(manifest, operations, dir)
|
|
167
|
-
|
|
168
|
-
const buildResult = await runBuildPipeline(dir)
|
|
169
|
-
expect(buildResult.ok).toBe(true)
|
|
170
|
-
})
|
|
171
|
-
})
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdir } from 'node:fs/promises'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { resolveTargetDir } from '../commands/resolve-dir.ts'
|
|
6
|
-
|
|
7
|
-
async function createFixtureDir(name: string): Promise<string> {
|
|
8
|
-
const dir = join(tmpdir(), `facets-resolve-test-${name}-${Date.now()}`)
|
|
9
|
-
await mkdir(dir, { recursive: true })
|
|
10
|
-
|
|
11
|
-
return dir
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('resolveTargetDir', () => {
|
|
15
|
-
test('no arg defaults to cwd', async () => {
|
|
16
|
-
const result = await resolveTargetDir(undefined, { mustExist: true })
|
|
17
|
-
expect(result.ok).toBe(true)
|
|
18
|
-
if (!result.ok) return
|
|
19
|
-
|
|
20
|
-
expect(result.dir).toBe(process.cwd())
|
|
21
|
-
expect(result.display).toBe('.')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('facet.json path uses parent directory', async () => {
|
|
25
|
-
const dir = await createFixtureDir('facet-json')
|
|
26
|
-
await Bun.write(join(dir, 'facet.json'), '{}')
|
|
27
|
-
|
|
28
|
-
const result = await resolveTargetDir(join(dir, 'facet.json'), { mustExist: true })
|
|
29
|
-
expect(result.ok).toBe(true)
|
|
30
|
-
if (!result.ok) return
|
|
31
|
-
|
|
32
|
-
expect(result.dir).toBe(dir)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
test('non-directory file returns error', async () => {
|
|
36
|
-
const dir = await createFixtureDir('non-dir')
|
|
37
|
-
const filePath = join(dir, 'not-a-dir.txt')
|
|
38
|
-
await Bun.write(filePath, 'hello')
|
|
39
|
-
|
|
40
|
-
const result = await resolveTargetDir(filePath, { mustExist: true })
|
|
41
|
-
expect(result.ok).toBe(false)
|
|
42
|
-
if (result.ok) return
|
|
43
|
-
|
|
44
|
-
expect(result.message).toContain('Expected a directory')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('non-existent dir with mustExist true returns error', async () => {
|
|
48
|
-
const result = await resolveTargetDir(`/tmp/does-not-exist-${Date.now()}`, { mustExist: true })
|
|
49
|
-
expect(result.ok).toBe(false)
|
|
50
|
-
if (result.ok) return
|
|
51
|
-
|
|
52
|
-
expect(result.message).toContain('does not exist')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test('non-existent dir with mustExist false auto-creates it', async () => {
|
|
56
|
-
const dir = join(tmpdir(), `facets-resolve-autocreate-${Date.now()}`)
|
|
57
|
-
|
|
58
|
-
const result = await resolveTargetDir(dir, { mustExist: false })
|
|
59
|
-
expect(result.ok).toBe(true)
|
|
60
|
-
if (!result.ok) return
|
|
61
|
-
|
|
62
|
-
const { stat } = await import('node:fs/promises')
|
|
63
|
-
const dirStat = await stat(result.dir)
|
|
64
|
-
expect(dirStat.isDirectory()).toBe(true)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('valid existing directory passes', async () => {
|
|
68
|
-
const dir = await createFixtureDir('valid-dir')
|
|
69
|
-
|
|
70
|
-
const result = await resolveTargetDir(dir, { mustExist: true })
|
|
71
|
-
expect(result.ok).toBe(true)
|
|
72
|
-
if (!result.ok) return
|
|
73
|
-
|
|
74
|
-
expect(result.dir).toBe(dir)
|
|
75
|
-
expect(result.display).toBe(dir)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('facetMustExist true without facet.json returns error', async () => {
|
|
79
|
-
const dir = await createFixtureDir('no-manifest')
|
|
80
|
-
|
|
81
|
-
const result = await resolveTargetDir(dir, { mustExist: true, facetMustExist: true })
|
|
82
|
-
expect(result.ok).toBe(false)
|
|
83
|
-
if (result.ok) return
|
|
84
|
-
|
|
85
|
-
expect(result.message).toContain('facet.json')
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test('facetMustExist true with facet.json passes', async () => {
|
|
89
|
-
const dir = await createFixtureDir('has-manifest')
|
|
90
|
-
await Bun.write(join(dir, 'facet.json'), '{}')
|
|
91
|
-
|
|
92
|
-
const result = await resolveTargetDir(dir, { mustExist: true, facetMustExist: true })
|
|
93
|
-
expect(result.ok).toBe(true)
|
|
94
|
-
})
|
|
95
|
-
})
|