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.
- package/bin/facet +1 -1
- package/package.json +16 -37
- package/{scripts/postinstall.mjs → postinstall.mjs} +1 -1
- package/.package.json.bak +0 -45
- package/.turbo/turbo-build.log +0 -3
- package/CHANGELOG.md +0 -95
- 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__/launcher.test.ts +0 -106
- package/src/__tests__/postinstall.test.ts +0 -196
- 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,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
|
-
})
|
package/src/commands/build.ts
DELETED
|
@@ -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
|
-
}
|