agent-facets 0.3.0 → 0.3.5

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 (60) hide show
  1. package/bin/facet +1 -1
  2. package/package.json +16 -37
  3. package/{scripts/postinstall.mjs → postinstall.mjs} +1 -1
  4. package/.package.json.bak +0 -45
  5. package/.turbo/turbo-build.log +0 -3
  6. package/CHANGELOG.md +0 -95
  7. package/bunfig.toml +0 -2
  8. package/dist/facet +0 -0
  9. package/src/__tests__/cli.test.ts +0 -195
  10. package/src/__tests__/create-build.test.ts +0 -227
  11. package/src/__tests__/edit-integration.test.ts +0 -171
  12. package/src/__tests__/launcher.test.ts +0 -106
  13. package/src/__tests__/postinstall.test.ts +0 -196
  14. package/src/__tests__/resolve-dir.test.ts +0 -95
  15. package/src/commands/build.ts +0 -58
  16. package/src/commands/create/index.ts +0 -76
  17. package/src/commands/create/types.ts +0 -9
  18. package/src/commands/create/wizard.tsx +0 -75
  19. package/src/commands/create-scaffold.ts +0 -184
  20. package/src/commands/edit/index.ts +0 -144
  21. package/src/commands/edit/wizard.tsx +0 -74
  22. package/src/commands/resolve-dir.ts +0 -98
  23. package/src/commands.ts +0 -40
  24. package/src/help.ts +0 -43
  25. package/src/index.ts +0 -10
  26. package/src/run.ts +0 -82
  27. package/src/suggest.ts +0 -35
  28. package/src/tui/components/asset-description.tsx +0 -17
  29. package/src/tui/components/asset-field-picker.tsx +0 -78
  30. package/src/tui/components/asset-inline-input.tsx +0 -91
  31. package/src/tui/components/asset-item.tsx +0 -44
  32. package/src/tui/components/asset-section.tsx +0 -191
  33. package/src/tui/components/button.tsx +0 -92
  34. package/src/tui/components/editable-field.tsx +0 -172
  35. package/src/tui/components/exit-toast.tsx +0 -20
  36. package/src/tui/components/reconciliation-item.tsx +0 -129
  37. package/src/tui/components/stage-row.tsx +0 -45
  38. package/src/tui/components/version-selector.tsx +0 -79
  39. package/src/tui/context/focus-mode-context.ts +0 -36
  40. package/src/tui/context/focus-order-context.ts +0 -68
  41. package/src/tui/context/form-state-context.ts +0 -260
  42. package/src/tui/editor.ts +0 -40
  43. package/src/tui/gradient.ts +0 -1
  44. package/src/tui/hooks/use-exit-keys.ts +0 -75
  45. package/src/tui/hooks/use-navigation-keys.ts +0 -34
  46. package/src/tui/layouts/wizard-layout.tsx +0 -41
  47. package/src/tui/theme.ts +0 -1
  48. package/src/tui/views/build/build-view.tsx +0 -152
  49. package/src/tui/views/create/confirm-view.tsx +0 -74
  50. package/src/tui/views/create/create-view.tsx +0 -158
  51. package/src/tui/views/create/wizard.tsx +0 -97
  52. package/src/tui/views/edit/edit-confirm-view.tsx +0 -93
  53. package/src/tui/views/edit/edit-types.ts +0 -34
  54. package/src/tui/views/edit/edit-view.tsx +0 -140
  55. package/src/tui/views/edit/manifest-to-form.ts +0 -38
  56. package/src/tui/views/edit/reconciliation-view.tsx +0 -170
  57. package/src/tui/views/edit/use-edit-session.ts +0 -125
  58. package/src/tui/views/edit/wizard.tsx +0 -129
  59. package/src/version.ts +0 -3
  60. package/tsconfig.json +0 -4
@@ -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,106 +0,0 @@
1
- import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
2
- import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'
3
- import { tmpdir } from 'node:os'
4
- import { join, resolve } from 'node:path'
5
-
6
- const LAUNCHER_PATH = resolve(import.meta.dir, '..', '..', 'bin', 'facet')
7
-
8
- interface ExecResult {
9
- stdout: string
10
- stderr: string
11
- exitCode: number
12
- }
13
-
14
- async function runLauncher(args: string[] = [], env: Record<string, string> = {}): Promise<ExecResult> {
15
- const proc = Bun.spawn(['node', LAUNCHER_PATH, ...args], {
16
- env: { ...process.env, ...env },
17
- stdout: 'pipe',
18
- stderr: 'pipe',
19
- })
20
-
21
- const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()])
22
- const exitCode = await proc.exited
23
-
24
- return { stdout, stderr, exitCode }
25
- }
26
-
27
- describe('launcher — FACET_BIN_PATH override', () => {
28
- let tmpDir: string
29
- let mockBinaryPath: string
30
-
31
- beforeAll(async () => {
32
- tmpDir = await mkdtemp(join(tmpdir(), 'launcher-test-'))
33
- mockBinaryPath = join(tmpDir, 'mock-facet')
34
- await writeFile(mockBinaryPath, '#!/bin/sh\necho "mock-facet: $@"\n')
35
- await chmod(mockBinaryPath, 0o755)
36
- })
37
-
38
- afterAll(async () => {
39
- await rm(tmpDir, { recursive: true, force: true })
40
- })
41
-
42
- test('runs the binary at FACET_BIN_PATH', async () => {
43
- const result = await runLauncher(['--version'], { FACET_BIN_PATH: mockBinaryPath })
44
- expect(result.stdout).toContain('mock-facet:')
45
- expect(result.exitCode).toBe(0)
46
- })
47
-
48
- test('forwards arguments to the target binary', async () => {
49
- const result = await runLauncher(['build', '--force', 'my-dir'], { FACET_BIN_PATH: mockBinaryPath })
50
- expect(result.stdout).toContain('build --force my-dir')
51
- })
52
- })
53
-
54
- describe('launcher — forwards exit code', () => {
55
- let tmpDir: string
56
- let exitingBinaryPath: string
57
-
58
- beforeAll(async () => {
59
- tmpDir = await mkdtemp(join(tmpdir(), 'launcher-exit-'))
60
- exitingBinaryPath = join(tmpDir, 'exit-42')
61
- await writeFile(exitingBinaryPath, '#!/bin/sh\nexit 42\n')
62
- await chmod(exitingBinaryPath, 0o755)
63
- })
64
-
65
- afterAll(async () => {
66
- await rm(tmpDir, { recursive: true, force: true })
67
- })
68
-
69
- test('exits with the same code as the target binary', async () => {
70
- const result = await runLauncher([], { FACET_BIN_PATH: exitingBinaryPath })
71
- expect(result.exitCode).toBe(42)
72
- })
73
- })
74
-
75
- describe('launcher — no binary found', () => {
76
- let tmpDir: string
77
- let isolatedLauncherPath: string
78
-
79
- beforeAll(async () => {
80
- // Create an isolated copy of the launcher in a directory with no node_modules
81
- // and no .facet, so resolution falls through to the error path.
82
- tmpDir = await mkdtemp(join(tmpdir(), 'launcher-nobin-'))
83
- isolatedLauncherPath = join(tmpDir, 'facet')
84
- await Bun.write(isolatedLauncherPath, await Bun.file(LAUNCHER_PATH).text())
85
- await chmod(isolatedLauncherPath, 0o755)
86
- })
87
-
88
- afterAll(async () => {
89
- await rm(tmpDir, { recursive: true, force: true })
90
- })
91
-
92
- test('prints error with candidate package names and exits 1', async () => {
93
- const proc = Bun.spawn(['node', isolatedLauncherPath], {
94
- env: { ...process.env, FACET_BIN_PATH: undefined },
95
- stdout: 'pipe',
96
- stderr: 'pipe',
97
- })
98
-
99
- const stderr = await new Response(proc.stderr).text()
100
- const exitCode = await proc.exited
101
-
102
- expect(exitCode).toBe(1)
103
- expect(stderr).toContain('agent-facets-')
104
- expect(stderr).toContain('package manager failed')
105
- })
106
- })
@@ -1,196 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { resolve } from 'node:path'
3
-
4
- // Import the pure functions from the postinstall script
5
- const postinstallPath = resolve(import.meta.dir, '..', '..', 'scripts', 'postinstall.mjs')
6
- const { buildCandidates, detectPlatform } = await import(postinstallPath)
7
-
8
- describe('detectPlatform', () => {
9
- test('returns an object with platform and arch strings', () => {
10
- const result = detectPlatform()
11
- expect(typeof result.platform).toBe('string')
12
- expect(typeof result.arch).toBe('string')
13
- expect(result.platform.length).toBeGreaterThan(0)
14
- expect(result.arch.length).toBeGreaterThan(0)
15
- })
16
-
17
- test('maps darwin correctly', () => {
18
- // We're running on macOS in this project
19
- const result = detectPlatform()
20
- if (process.platform === 'darwin') {
21
- expect(result.platform).toBe('darwin')
22
- }
23
- })
24
- })
25
-
26
- describe('buildCandidates', () => {
27
- // ---------------------------------------------------------------------------
28
- // Linux glibc
29
- // ---------------------------------------------------------------------------
30
-
31
- test('linux x64 avx2 glibc', () => {
32
- const result = buildCandidates('linux', 'x64', { avx2: true, musl: false })
33
- expect(result).toEqual([
34
- 'agent-facets-linux-x64',
35
- 'agent-facets-linux-x64-baseline',
36
- 'agent-facets-linux-x64-musl',
37
- 'agent-facets-linux-x64-baseline-musl',
38
- ])
39
- })
40
-
41
- test('linux x64 no-avx2 glibc', () => {
42
- const result = buildCandidates('linux', 'x64', { avx2: false, musl: false })
43
- expect(result).toEqual([
44
- 'agent-facets-linux-x64-baseline',
45
- 'agent-facets-linux-x64',
46
- 'agent-facets-linux-x64-baseline-musl',
47
- 'agent-facets-linux-x64-musl',
48
- ])
49
- })
50
-
51
- test('linux arm64 glibc', () => {
52
- const result = buildCandidates('linux', 'arm64', { avx2: false, musl: false })
53
- expect(result).toEqual(['agent-facets-linux-arm64', 'agent-facets-linux-arm64-musl'])
54
- })
55
-
56
- // ---------------------------------------------------------------------------
57
- // Linux musl
58
- // ---------------------------------------------------------------------------
59
-
60
- test('linux x64 avx2 musl', () => {
61
- const result = buildCandidates('linux', 'x64', { avx2: true, musl: true })
62
- expect(result).toEqual([
63
- 'agent-facets-linux-x64-musl',
64
- 'agent-facets-linux-x64-baseline-musl',
65
- 'agent-facets-linux-x64',
66
- 'agent-facets-linux-x64-baseline',
67
- ])
68
- })
69
-
70
- test('linux x64 no-avx2 musl', () => {
71
- const result = buildCandidates('linux', 'x64', { avx2: false, musl: true })
72
- expect(result).toEqual([
73
- 'agent-facets-linux-x64-baseline-musl',
74
- 'agent-facets-linux-x64-musl',
75
- 'agent-facets-linux-x64-baseline',
76
- 'agent-facets-linux-x64',
77
- ])
78
- })
79
-
80
- test('linux arm64 musl', () => {
81
- const result = buildCandidates('linux', 'arm64', { avx2: false, musl: true })
82
- expect(result).toEqual(['agent-facets-linux-arm64-musl', 'agent-facets-linux-arm64'])
83
- })
84
-
85
- // ---------------------------------------------------------------------------
86
- // Non-Linux
87
- // ---------------------------------------------------------------------------
88
-
89
- test('darwin arm64', () => {
90
- const result = buildCandidates('darwin', 'arm64', { avx2: false })
91
- expect(result).toEqual(['agent-facets-darwin-arm64'])
92
- })
93
-
94
- test('darwin x64 avx2', () => {
95
- const result = buildCandidates('darwin', 'x64', { avx2: true })
96
- expect(result).toEqual(['agent-facets-darwin-x64', 'agent-facets-darwin-x64-baseline'])
97
- })
98
-
99
- test('darwin x64 no-avx2', () => {
100
- const result = buildCandidates('darwin', 'x64', { avx2: false })
101
- expect(result).toEqual(['agent-facets-darwin-x64-baseline', 'agent-facets-darwin-x64'])
102
- })
103
-
104
- test('windows x64 avx2', () => {
105
- const result = buildCandidates('windows', 'x64', { avx2: true })
106
- expect(result).toEqual(['agent-facets-windows-x64', 'agent-facets-windows-x64-baseline'])
107
- })
108
-
109
- test('windows arm64', () => {
110
- const result = buildCandidates('windows', 'arm64', { avx2: false })
111
- expect(result).toEqual(['agent-facets-windows-arm64'])
112
- })
113
- })
114
-
115
- // ---------------------------------------------------------------------------
116
- // Consistency: launcher and postinstall must produce identical candidate lists
117
- // ---------------------------------------------------------------------------
118
-
119
- describe('consistency — launcher and postinstall candidate lists match', () => {
120
- /**
121
- * Reimplements the launcher's candidate-building logic as a pure function.
122
- * This is intentionally a direct translation of the CommonJS code in bin/facet
123
- * so the test catches any drift between the two implementations.
124
- */
125
- function launcherCandidates(platform: string, arch: string, opts: { avx2: boolean; musl?: boolean }): string[] {
126
- const base = `agent-facets-${platform}-${arch}`
127
- const baseline = arch === 'x64' && !opts.avx2
128
-
129
- if (platform === 'linux') {
130
- const musl = !!opts.musl
131
-
132
- if (musl) {
133
- if (arch === 'x64') {
134
- if (baseline) return [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
135
- return [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
136
- }
137
- return [`${base}-musl`, base]
138
- }
139
-
140
- if (arch === 'x64') {
141
- if (baseline) return [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
142
- return [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
143
- }
144
- return [base, `${base}-musl`]
145
- }
146
-
147
- if (arch === 'x64') {
148
- if (baseline) return [`${base}-baseline`, base]
149
- return [base, `${base}-baseline`]
150
- }
151
- return [base]
152
- }
153
-
154
- const cases: Array<{ platform: string; arch: string; avx2: boolean; musl?: boolean }> = [
155
- // Linux glibc
156
- { platform: 'linux', arch: 'x64', avx2: true, musl: false },
157
- { platform: 'linux', arch: 'x64', avx2: false, musl: false },
158
- { platform: 'linux', arch: 'arm64', avx2: false, musl: false },
159
- // Linux musl
160
- { platform: 'linux', arch: 'x64', avx2: true, musl: true },
161
- { platform: 'linux', arch: 'x64', avx2: false, musl: true },
162
- { platform: 'linux', arch: 'arm64', avx2: false, musl: true },
163
- // Darwin
164
- { platform: 'darwin', arch: 'arm64', avx2: false },
165
- { platform: 'darwin', arch: 'x64', avx2: true },
166
- { platform: 'darwin', arch: 'x64', avx2: false },
167
- // Windows
168
- { platform: 'windows', arch: 'arm64', avx2: false },
169
- { platform: 'windows', arch: 'x64', avx2: true },
170
- { platform: 'windows', arch: 'x64', avx2: false },
171
- ]
172
-
173
- for (const c of cases) {
174
- const label = `${c.platform}/${c.arch} avx2=${c.avx2}${c.musl !== undefined ? ` musl=${c.musl}` : ''}`
175
- test(label, () => {
176
- const fromPostinstall = buildCandidates(c.platform, c.arch, { avx2: c.avx2, musl: c.musl })
177
- const fromLauncher = launcherCandidates(c.platform, c.arch, { avx2: c.avx2, musl: c.musl })
178
- expect(fromPostinstall).toEqual(fromLauncher)
179
- })
180
- }
181
- })
182
-
183
- // ---------------------------------------------------------------------------
184
- // Silent failure
185
- // ---------------------------------------------------------------------------
186
-
187
- describe('postinstall — silent failure', () => {
188
- test('exits 0 when no platform packages are installed', async () => {
189
- const proc = Bun.spawn(['node', resolve(import.meta.dir, '..', '..', 'scripts', 'postinstall.mjs')], {
190
- stdout: 'pipe',
191
- stderr: 'pipe',
192
- })
193
- const exitCode = await proc.exited
194
- expect(exitCode).toBe(0)
195
- })
196
- })
@@ -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
- })
@@ -1,58 +0,0 @@
1
- import { render } from 'ink'
2
- import { createElement } from 'react'
3
- import type { Command } from '../commands.ts'
4
- import { BuildView } from '../tui/views/build/build-view.tsx'
5
- import { resolveTargetDir } from './resolve-dir.ts'
6
-
7
- export const buildCommand: Command = {
8
- name: 'build',
9
- description: 'Build a facet from the current directory',
10
- usage: '[directory]',
11
- run: async (args: string[], _flags: Record<string, unknown>): Promise<number> => {
12
- const resolved = await resolveTargetDir(args[0], { mustExist: true, facetMustExist: true })
13
- if (!resolved.ok) {
14
- console.error(resolved.message)
15
- return 1
16
- }
17
-
18
- const rootDir = resolved.dir
19
- const displayDir = resolved.display
20
-
21
- // Track result for stdout summary after Ink exits
22
- let buildName = ''
23
- let buildVersion = ''
24
- let artifactCount = 0
25
- let integrity = ''
26
- let errorCount = 0
27
-
28
- const instance = render(
29
- createElement(BuildView, {
30
- rootDir,
31
- onSuccess: (name: string, version: string, fileCount: number, hash: string) => {
32
- buildName = name
33
- buildVersion = version
34
- artifactCount = fileCount
35
- integrity = hash
36
- },
37
- onFailure: (count: number) => {
38
- errorCount = count
39
- },
40
- }),
41
- )
42
-
43
- try {
44
- await instance.waitUntilExit()
45
- // Ink has unmounted — print stdout summary for scroll-back
46
- const shortHash = integrity.length > 20 ? `${integrity.slice(0, 20)}...` : integrity
47
- process.stdout.write(
48
- `✓ Built ${buildName} v${buildVersion} → ${displayDir}/dist/ (${artifactCount} assets, ${shortHash})\n`,
49
- )
50
- return 0
51
- } catch {
52
- process.stdout.write(
53
- `✗ Build failed — ${errorCount} error${errorCount !== 1 ? 's' : ''}. Run \`facet edit${args[0] ? ` ${displayDir}` : ''}\` to fix.\n`,
54
- )
55
- return 1
56
- }
57
- },
58
- }