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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-facets",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "https://github.com/agent-facets/facets",
|
|
6
|
+
"directory": "packages/cli"
|
|
7
|
+
},
|
|
8
|
+
"version": "0.2.1",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"facet": "./dist/facet"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "bun build src/index.ts --compile --outfile dist/facet",
|
|
15
|
+
"prepack": "bun ../../scripts/prepack.ts",
|
|
16
|
+
"postpack": "bun ../../scripts/postpack.ts",
|
|
17
|
+
"types": "tsc --noEmit",
|
|
18
|
+
"test": "bun test --timeout 15000"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@bomb.sh/args": "0.3.1",
|
|
22
|
+
"@types/react": "19.2.14",
|
|
23
|
+
"arktype": "2.1.29",
|
|
24
|
+
"ink": "6.8.0",
|
|
25
|
+
"ink-gradient": "4.0.0",
|
|
26
|
+
"ink-spinner": "5.0.0",
|
|
27
|
+
"ink-text-input": "6.0.0",
|
|
28
|
+
"react": "19.2.4",
|
|
29
|
+
"react-devtools-core": "7.0.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@agent-facets/brand": "workspace:*",
|
|
33
|
+
"@agent-facets/core": "workspace:*",
|
|
34
|
+
"@types/bun": "1.3.10",
|
|
35
|
+
"ink-testing-library": "4.0.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"typescript": "^5 || ^6"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": false
|
|
43
|
+
}
|
|
44
|
+
}
|
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
$ bun build src/index.ts --compile --outfile dist/facet
|
|
2
|
-
[
|
|
3
|
-
[
|
|
2
|
+
[202ms] bundle 388 modules
|
|
3
|
+
[3ms] compile dist/facet
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# agent-facets
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#39](https://github.com/agent-facets/facets/pull/39) [`f380b7b`](https://github.com/agent-facets/facets/commit/f380b7bc5115acec1f974ef1401eba199a2f90fb) Thanks [@eXamadeus](https://github.com/eXamadeus)! - Ensure release CI works in isolation
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#35](https://github.com/agent-facets/facets/pull/35) [`6350718`](https://github.com/agent-facets/facets/commit/63507188f1bb3a7276cd4812f69f7d16d1778fd6) Thanks [@eXamadeus](https://github.com/eXamadeus)! - Ensure proper release isolation
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#37](https://github.com/agent-facets/facets/pull/37) [`1c48260`](https://github.com/agent-facets/facets/commit/1c48260ab77fd27e64be6c5884aa6c447e3639e0) Thanks [@eXamadeus](https://github.com/eXamadeus)! - Better dev & ci dependency management via mise
|
|
18
|
+
- [#33](https://github.com/agent-facets/facets/pull/33) [`540e126`](https://github.com/agent-facets/facets/commit/540e126e677de98a9b3d4e39542df37de8756b73) Thanks [@eXamadeus](https://github.com/eXamadeus)! - Ensure CI runs tests before release and notify Slack when failures occur.
|
|
19
|
+
|
|
20
|
+
## 0.1.4
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- [`098fd08`](https://github.com/agent-facets/facets/commit/098fd08bf5d9970babc5c57bee6a155bffcecd97) Thanks [@eXamadeus](https://github.com/eXamadeus)! - Better CLI parameter validation
|
|
25
|
+
|
|
26
|
+
- [`5262cbe`](https://github.com/agent-facets/facets/commit/5262cbe66df02c625430309878e6061ccde183de) Thanks [@eXamadeus](https://github.com/eXamadeus)! - Fix publishing by properly categorizing dev dependencies
|
|
27
|
+
|
|
28
|
+
- [`d3b9439`](https://github.com/agent-facets/facets/commit/d3b9439466e0eb65687901426e2ebd6c5a333c60) Thanks [@eXamadeus](https://github.com/eXamadeus)! - Use better github attribution for changesets
|
|
29
|
+
|
|
30
|
+
## 0.1.3
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 66b179f: Wire up the facet edit command
|
|
35
|
+
|
|
3
36
|
## 0.1.2
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
package/dist/facet
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -5,21 +5,22 @@
|
|
|
5
5
|
"url": "https://github.com/agent-facets/facets",
|
|
6
6
|
"directory": "packages/cli"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.1
|
|
8
|
+
"version": "0.2.1",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"bin": {
|
|
11
11
|
"facet": "./dist/facet"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "bun build src/index.ts --compile --outfile dist/facet",
|
|
15
|
+
"prepack": "bun ../../scripts/prepack.ts",
|
|
16
|
+
"postpack": "bun ../../scripts/postpack.ts",
|
|
15
17
|
"types": "tsc --noEmit",
|
|
16
18
|
"test": "bun test --timeout 15000"
|
|
17
19
|
},
|
|
18
20
|
"dependencies": {
|
|
19
|
-
"@agent-facets/brand": "workspace:*",
|
|
20
21
|
"@bomb.sh/args": "0.3.1",
|
|
21
|
-
"@agent-facets/core": "workspace:*",
|
|
22
22
|
"@types/react": "19.2.14",
|
|
23
|
+
"arktype": "2.1.29",
|
|
23
24
|
"ink": "6.8.0",
|
|
24
25
|
"ink-gradient": "4.0.0",
|
|
25
26
|
"ink-spinner": "5.0.0",
|
|
@@ -28,11 +29,13 @@
|
|
|
28
29
|
"react-devtools-core": "7.0.1"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
32
|
+
"@agent-facets/brand": "0.2.0",
|
|
33
|
+
"@agent-facets/core": "0.2.0",
|
|
31
34
|
"@types/bun": "1.3.10",
|
|
32
35
|
"ink-testing-library": "4.0.0"
|
|
33
36
|
},
|
|
34
37
|
"peerDependencies": {
|
|
35
|
-
"typescript": "^5"
|
|
38
|
+
"typescript": "^5 || ^6"
|
|
36
39
|
},
|
|
37
40
|
"publishConfig": {
|
|
38
41
|
"access": "public",
|
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
|
|
4
4
|
const CLI_PATH = resolve(import.meta.dir, '../../dist/facet')
|
|
5
|
-
const COMMAND_NAMES = ['add', 'build', 'create', 'info', 'install', 'list', 'publish', 'remove', 'upgrade']
|
|
5
|
+
const COMMAND_NAMES = ['add', 'build', 'create', 'edit', 'info', 'install', 'list', 'publish', 'remove', 'upgrade']
|
|
6
6
|
const STUB_COMMAND_NAMES = ['add', 'info', 'install', 'list', 'publish', 'remove', 'upgrade']
|
|
7
7
|
|
|
8
8
|
type ExecResult = {
|
|
@@ -79,6 +79,16 @@ describe('CLI — stub commands', () => {
|
|
|
79
79
|
})
|
|
80
80
|
})
|
|
81
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
|
+
|
|
82
92
|
// --- Unknown commands ---
|
|
83
93
|
|
|
84
94
|
describe('CLI — unknown commands', () => {
|
|
@@ -121,32 +131,65 @@ describe('CLI — per-command help', () => {
|
|
|
121
131
|
// --- Unexpected error ---
|
|
122
132
|
|
|
123
133
|
describe('CLI — unexpected error', () => {
|
|
124
|
-
test('unexpected error
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
import { run } from '${resolve(import.meta.dir, '../run.ts')}'
|
|
130
|
-
commands['crash'] = {
|
|
134
|
+
test('unexpected error is thrown by run', async () => {
|
|
135
|
+
const { run } = await import('../run.ts')
|
|
136
|
+
|
|
137
|
+
const crashRegistry = {
|
|
138
|
+
crash: {
|
|
131
139
|
name: 'crash',
|
|
132
140
|
description: 'Throws an error',
|
|
133
|
-
run: async (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
expect(
|
|
150
|
-
expect(
|
|
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')
|
|
151
194
|
})
|
|
152
195
|
})
|
|
@@ -3,6 +3,7 @@ import { mkdtemp, rm } from 'node:fs/promises'
|
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join, resolve } from 'node:path'
|
|
5
5
|
import { writeScaffold } from '../commands/create/index.ts'
|
|
6
|
+
import { DEFAULT_VERSION } from '../commands/create-scaffold.ts'
|
|
6
7
|
|
|
7
8
|
let testDir: string
|
|
8
9
|
|
|
@@ -51,7 +52,7 @@ describe('writeScaffold', () => {
|
|
|
51
52
|
const files = await writeScaffold(
|
|
52
53
|
{
|
|
53
54
|
name: 'my-facet',
|
|
54
|
-
version:
|
|
55
|
+
version: DEFAULT_VERSION,
|
|
55
56
|
description: 'A test facet',
|
|
56
57
|
skills: ['code-review', 'testing-guide'],
|
|
57
58
|
agents: ['reviewer'],
|
|
@@ -61,8 +62,8 @@ describe('writeScaffold', () => {
|
|
|
61
62
|
)
|
|
62
63
|
|
|
63
64
|
expect(files).toContain('facet.json')
|
|
64
|
-
expect(files).toContain('skills/code-review.md')
|
|
65
|
-
expect(files).toContain('skills/testing-guide.md')
|
|
65
|
+
expect(files).toContain('skills/code-review/SKILL.md')
|
|
66
|
+
expect(files).toContain('skills/testing-guide/SKILL.md')
|
|
66
67
|
expect(files).toContain('agents/reviewer.md')
|
|
67
68
|
expect(files).toContain('commands/deploy.md')
|
|
68
69
|
|
|
@@ -70,7 +71,7 @@ describe('writeScaffold', () => {
|
|
|
70
71
|
const manifestText = await Bun.file(join(dir, 'facet.json')).text()
|
|
71
72
|
const manifest = JSON.parse(manifestText)
|
|
72
73
|
expect(manifest.name).toBe('my-facet')
|
|
73
|
-
expect(manifest.version).toBe(
|
|
74
|
+
expect(manifest.version).toBe(DEFAULT_VERSION)
|
|
74
75
|
expect(manifest.description).toBe('A test facet')
|
|
75
76
|
expect(manifest.skills).toBeDefined()
|
|
76
77
|
expect(manifest.skills['code-review']).toBeDefined()
|
|
@@ -81,10 +82,10 @@ describe('writeScaffold', () => {
|
|
|
81
82
|
expect(manifest.commands.deploy).toBeDefined()
|
|
82
83
|
|
|
83
84
|
// Verify starter files exist and have named template content
|
|
84
|
-
const skill = await Bun.file(join(dir, 'skills/code-review.md')).text()
|
|
85
|
+
const skill = await Bun.file(join(dir, 'skills/code-review/SKILL.md')).text()
|
|
85
86
|
expect(skill).toContain('# Code Review')
|
|
86
87
|
|
|
87
|
-
const skill2 = await Bun.file(join(dir, 'skills/testing-guide.md')).text()
|
|
88
|
+
const skill2 = await Bun.file(join(dir, 'skills/testing-guide/SKILL.md')).text()
|
|
88
89
|
expect(skill2).toContain('# Testing Guide')
|
|
89
90
|
|
|
90
91
|
const agent = await Bun.file(join(dir, 'agents/reviewer.md')).text()
|
|
@@ -99,7 +100,7 @@ describe('writeScaffold', () => {
|
|
|
99
100
|
const files = await writeScaffold(
|
|
100
101
|
{
|
|
101
102
|
name: 'minimal',
|
|
102
|
-
version:
|
|
103
|
+
version: DEFAULT_VERSION,
|
|
103
104
|
description: '',
|
|
104
105
|
skills: ['minimal'],
|
|
105
106
|
agents: [],
|
|
@@ -109,7 +110,7 @@ describe('writeScaffold', () => {
|
|
|
109
110
|
)
|
|
110
111
|
|
|
111
112
|
expect(files).toContain('facet.json')
|
|
112
|
-
expect(files).toContain('skills/minimal.md')
|
|
113
|
+
expect(files).toContain('skills/minimal/SKILL.md')
|
|
113
114
|
expect(files).not.toContain('agents/')
|
|
114
115
|
expect(files).not.toContain('commands/')
|
|
115
116
|
|
|
@@ -120,12 +121,31 @@ describe('writeScaffold', () => {
|
|
|
120
121
|
expect(manifest.commands).toBeUndefined()
|
|
121
122
|
})
|
|
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
|
+
|
|
123
143
|
test('scaffolded project passes build', async () => {
|
|
124
144
|
const dir = await createFixtureDir('scaffold-buildable')
|
|
125
145
|
await writeScaffold(
|
|
126
146
|
{
|
|
127
147
|
name: 'buildable',
|
|
128
|
-
version:
|
|
148
|
+
version: DEFAULT_VERSION,
|
|
129
149
|
description: 'A buildable facet',
|
|
130
150
|
skills: ['helper'],
|
|
131
151
|
agents: ['assistant'],
|
|
@@ -140,7 +160,7 @@ describe('writeScaffold', () => {
|
|
|
140
160
|
expect(result.stdout).toContain('Built buildable')
|
|
141
161
|
|
|
142
162
|
// Verify dist/ output exists — archive + build manifest
|
|
143
|
-
const distArchive = await Bun.file(join(dir,
|
|
163
|
+
const distArchive = await Bun.file(join(dir, `dist/buildable-${DEFAULT_VERSION}.facet`)).exists()
|
|
144
164
|
expect(distArchive).toBe(true)
|
|
145
165
|
|
|
146
166
|
const distManifest = await Bun.file(join(dir, 'dist/build-manifest.json')).exists()
|
|
@@ -157,7 +177,7 @@ describe('writeScaffold', () => {
|
|
|
157
177
|
describe('facet build', () => {
|
|
158
178
|
test('build succeeds on valid project', async () => {
|
|
159
179
|
const dir = await createFixtureDir('build-valid')
|
|
160
|
-
await Bun.write(join(dir, 'skills/review.md'), '# Review skill content')
|
|
180
|
+
await Bun.write(join(dir, 'skills/review/SKILL.md'), '# Review skill content')
|
|
161
181
|
await Bun.write(
|
|
162
182
|
join(dir, 'facet.json'),
|
|
163
183
|
JSON.stringify({
|
|
@@ -182,7 +202,7 @@ describe('facet build', () => {
|
|
|
182
202
|
|
|
183
203
|
const result = await runCli('build', dir)
|
|
184
204
|
expect(result.exitCode).toBe(1)
|
|
185
|
-
expect(result.
|
|
205
|
+
expect(result.stderr).toContain('facet.json')
|
|
186
206
|
})
|
|
187
207
|
|
|
188
208
|
test('build fails on missing asset file', async () => {
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
})
|