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.
Files changed (40) hide show
  1. package/.package.json.bak +44 -0
  2. package/.turbo/turbo-build.log +2 -2
  3. package/CHANGELOG.md +33 -0
  4. package/dist/facet +0 -0
  5. package/package.json +7 -4
  6. package/src/__tests__/cli.test.ts +69 -26
  7. package/src/__tests__/create-build.test.ts +32 -12
  8. package/src/__tests__/edit-integration.test.ts +171 -0
  9. package/src/__tests__/resolve-dir.test.ts +95 -0
  10. package/src/commands/build.ts +17 -4
  11. package/src/commands/create/index.ts +51 -5
  12. package/src/commands/create/wizard.tsx +66 -15
  13. package/src/commands/create-scaffold.ts +14 -10
  14. package/src/commands/edit/index.ts +144 -0
  15. package/src/commands/edit/wizard.tsx +74 -0
  16. package/src/commands/resolve-dir.ts +98 -0
  17. package/src/commands.ts +11 -2
  18. package/src/help.ts +17 -10
  19. package/src/index.ts +2 -1
  20. package/src/run.ts +32 -5
  21. package/src/tui/components/asset-description.tsx +17 -0
  22. package/src/tui/components/asset-field-picker.tsx +78 -0
  23. package/src/tui/components/asset-inline-input.tsx +13 -1
  24. package/src/tui/components/asset-item.tsx +3 -7
  25. package/src/tui/components/asset-section.tsx +72 -26
  26. package/src/tui/components/reconciliation-item.tsx +129 -0
  27. package/src/tui/components/stage-row.tsx +16 -4
  28. package/src/tui/context/focus-order-context.ts +8 -2
  29. package/src/tui/context/form-state-context.ts +34 -3
  30. package/src/tui/editor.ts +40 -0
  31. package/src/tui/views/build/build-view.tsx +43 -44
  32. package/src/tui/views/create/create-view.tsx +17 -13
  33. package/src/tui/views/create/wizard.tsx +35 -6
  34. package/src/tui/views/edit/edit-confirm-view.tsx +93 -0
  35. package/src/tui/views/edit/edit-types.ts +34 -0
  36. package/src/tui/views/edit/edit-view.tsx +140 -0
  37. package/src/tui/views/edit/manifest-to-form.ts +38 -0
  38. package/src/tui/views/edit/reconciliation-view.tsx +170 -0
  39. package/src/tui/views/edit/use-edit-session.ts +125 -0
  40. 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
+ }
@@ -1,3 +1,3 @@
1
1
  $ bun build src/index.ts --compile --outfile dist/facet
2
- [169ms] bundle 298 modules
3
- [2ms] compile dist/facet
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.2",
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 exits with code 2', async () => {
125
- // This test runs against source (not compiled binary) because it needs to
126
- // monkey-patch the command registry to inject a crashing command.
127
- const script = `
128
- import { commands } from '${resolve(import.meta.dir, '../commands.ts')}'
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 () => { throw new Error('boom') },
134
- }
135
- try {
136
- const code = await run(['crash'])
137
- process.exit(code)
138
- } catch (error) {
139
- console.error(error instanceof Error ? error.message : 'An unexpected error occurred.')
140
- process.exit(2)
141
- }
142
- `
143
- const proc = Bun.spawn(['bun', '--eval', script], {
144
- stdout: 'pipe',
145
- stderr: 'pipe',
146
- })
147
- const stderr = await new Response(proc.stderr).text()
148
- const exitCode = await proc.exited
149
- expect(exitCode).toBe(2)
150
- expect(stderr.trim()).toContain('boom')
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: '0.1.0',
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('0.1.0')
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: '0.1.0',
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: '0.1.0',
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, 'dist/buildable-0.1.0.facet')).exists()
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.stdout).toContain('Build failed')
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
+ })