agent-facets 0.1.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/CHANGELOG.md +30 -0
- package/bunfig.toml +2 -0
- package/dist/facet +0 -0
- package/dist/facet-ink-test +0 -0
- package/package.json +41 -0
- package/src/__tests__/cli.test.ts +152 -0
- package/src/__tests__/create-build.test.ts +207 -0
- package/src/commands/build.ts +45 -0
- package/src/commands/create/index.ts +30 -0
- package/src/commands/create/types.ts +9 -0
- package/src/commands/create/wizard.tsx +24 -0
- package/src/commands/create-scaffold.ts +180 -0
- package/src/commands.ts +31 -0
- package/src/help.ts +36 -0
- package/src/index.ts +9 -0
- package/src/run.ts +55 -0
- package/src/suggest.ts +35 -0
- package/src/tui/components/asset-inline-input.tsx +79 -0
- package/src/tui/components/asset-item.tsx +48 -0
- package/src/tui/components/asset-section.tsx +145 -0
- package/src/tui/components/button.tsx +92 -0
- package/src/tui/components/editable-field.tsx +172 -0
- package/src/tui/components/exit-toast.tsx +20 -0
- package/src/tui/components/stage-row.tsx +33 -0
- package/src/tui/components/version-selector.tsx +79 -0
- package/src/tui/context/focus-mode-context.ts +36 -0
- package/src/tui/context/focus-order-context.ts +62 -0
- package/src/tui/context/form-state-context.ts +229 -0
- package/src/tui/gradient.ts +1 -0
- package/src/tui/hooks/use-exit-keys.ts +75 -0
- package/src/tui/hooks/use-navigation-keys.ts +34 -0
- package/src/tui/layouts/wizard-layout.tsx +41 -0
- package/src/tui/theme.ts +1 -0
- package/src/tui/views/build/build-view.tsx +153 -0
- package/src/tui/views/create/confirm-view.tsx +74 -0
- package/src/tui/views/create/create-view.tsx +154 -0
- package/src/tui/views/create/wizard.tsx +68 -0
- package/src/version.ts +3 -0
- package/tsconfig.json +4 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# agent-facets
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5813b90: Small test for change set management in CI
|
|
8
|
+
- Updated dependencies [5813b90]
|
|
9
|
+
- @agent-facets/core@0.1.1
|
|
10
|
+
|
|
11
|
+
## 0.1.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 2243bbf: Added basic create command to CLI
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [2243bbf]
|
|
20
|
+
- @agent-facets/core@0.1.0
|
|
21
|
+
|
|
22
|
+
## 0.0.1
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- 74e3d25: Should be 0.0.1 now
|
|
27
|
+
- 74e3d25: Initial publishing
|
|
28
|
+
- Updated dependencies [74e3d25]
|
|
29
|
+
- Updated dependencies [74e3d25]
|
|
30
|
+
- @agent-facets/core@0.0.1
|
package/bunfig.toml
ADDED
package/dist/facet
ADDED
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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.1.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
|
+
"types": "tsc --noEmit",
|
|
16
|
+
"test": "bun test --timeout 15000"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@agent-facets/brand": "workspace:*",
|
|
20
|
+
"@bomb.sh/args": "0.3.1",
|
|
21
|
+
"@agent-facets/core": "workspace:*",
|
|
22
|
+
"@types/react": "19.2.14",
|
|
23
|
+
"ink": "6.8.0",
|
|
24
|
+
"ink-gradient": "4.0.0",
|
|
25
|
+
"ink-spinner": "5.0.0",
|
|
26
|
+
"ink-text-input": "6.0.0",
|
|
27
|
+
"react": "19.2.4",
|
|
28
|
+
"react-devtools-core": "7.0.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/bun": "1.3.10",
|
|
32
|
+
"ink-testing-library": "4.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"typescript": "^5"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"provenance": false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const CLI_PATH = resolve(import.meta.dir, '../../dist/facet')
|
|
5
|
+
const COMMAND_NAMES = ['add', 'build', 'create', 'info', 'install', 'list', 'publish', 'remove', 'upgrade']
|
|
6
|
+
const STUB_COMMAND_NAMES = ['add', 'info', 'install', 'list', 'publish', 'remove', 'upgrade']
|
|
7
|
+
|
|
8
|
+
type ExecResult = {
|
|
9
|
+
stdout: string
|
|
10
|
+
stderr: string
|
|
11
|
+
exitCode: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function runCli(...args: string[]): Promise<ExecResult> {
|
|
15
|
+
const proc = Bun.spawn([CLI_PATH, ...args], {
|
|
16
|
+
stdout: 'pipe',
|
|
17
|
+
stderr: 'pipe',
|
|
18
|
+
})
|
|
19
|
+
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()])
|
|
20
|
+
const exitCode = await proc.exited
|
|
21
|
+
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- Help ---
|
|
25
|
+
|
|
26
|
+
describe('CLI — help', () => {
|
|
27
|
+
test('--help prints command list to stdout and exits 0', async () => {
|
|
28
|
+
const result = await runCli('--help')
|
|
29
|
+
expect(result.exitCode).toBe(0)
|
|
30
|
+
expect(result.stdout).toContain('Usage: facet <command>')
|
|
31
|
+
for (const cmd of COMMAND_NAMES) {
|
|
32
|
+
expect(result.stdout).toContain(cmd)
|
|
33
|
+
}
|
|
34
|
+
expect(result.stderr).toBe('')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('help command produces same output as --help', async () => {
|
|
38
|
+
const helpFlag = await runCli('--help')
|
|
39
|
+
const helpCommand = await runCli('help')
|
|
40
|
+
expect(helpCommand.exitCode).toBe(0)
|
|
41
|
+
expect(helpCommand.stdout).toBe(helpFlag.stdout)
|
|
42
|
+
expect(helpCommand.stderr).toBe('')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// --- Version ---
|
|
47
|
+
|
|
48
|
+
describe('CLI — version', () => {
|
|
49
|
+
test('--version prints version matching package.json and exits 0', async () => {
|
|
50
|
+
const pkg = await Bun.file(resolve(import.meta.dir, '../../package.json')).json()
|
|
51
|
+
const result = await runCli('--version')
|
|
52
|
+
expect(result.exitCode).toBe(0)
|
|
53
|
+
expect(result.stdout).toBe(pkg.version)
|
|
54
|
+
expect(result.stderr).toBe('')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// --- Bare invocation ---
|
|
59
|
+
|
|
60
|
+
describe('CLI — bare invocation', () => {
|
|
61
|
+
test('no arguments prints help and exits 0', async () => {
|
|
62
|
+
const helpResult = await runCli('--help')
|
|
63
|
+
const bareResult = await runCli()
|
|
64
|
+
expect(bareResult.exitCode).toBe(0)
|
|
65
|
+
expect(bareResult.stdout).toBe(helpResult.stdout)
|
|
66
|
+
expect(bareResult.stderr).toBe('')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// --- Stub commands ---
|
|
71
|
+
|
|
72
|
+
describe('CLI — stub commands', () => {
|
|
73
|
+
test.each(STUB_COMMAND_NAMES)('"%s" prints not yet implemented with command name and exits 0', async (cmd) => {
|
|
74
|
+
const result = await runCli(cmd)
|
|
75
|
+
expect(result.exitCode).toBe(0)
|
|
76
|
+
expect(result.stdout).toContain(cmd)
|
|
77
|
+
expect(result.stdout).toContain('not yet implemented')
|
|
78
|
+
expect(result.stderr).toBe('')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// --- Unknown commands ---
|
|
83
|
+
|
|
84
|
+
describe('CLI — unknown commands', () => {
|
|
85
|
+
test('unknown command prints error to stderr and exits 1', async () => {
|
|
86
|
+
const result = await runCli('xyzzy')
|
|
87
|
+
expect(result.exitCode).toBe(1)
|
|
88
|
+
expect(result.stderr).toContain('Unknown command "xyzzy"')
|
|
89
|
+
expect(result.stdout).toBe('')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('unknown command with close match includes "did you mean?" suggestion', async () => {
|
|
93
|
+
const result = await runCli('bild')
|
|
94
|
+
expect(result.exitCode).toBe(1)
|
|
95
|
+
expect(result.stderr).toContain('Unknown command "bild"')
|
|
96
|
+
expect(result.stderr).toContain('Did you mean "build"')
|
|
97
|
+
expect(result.stdout).toBe('')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('unknown command with no close match does not include suggestion', async () => {
|
|
101
|
+
const result = await runCli('xyzzy')
|
|
102
|
+
expect(result.exitCode).toBe(1)
|
|
103
|
+
expect(result.stderr).toContain('Unknown command "xyzzy"')
|
|
104
|
+
expect(result.stderr).not.toContain('Did you mean')
|
|
105
|
+
expect(result.stdout).toBe('')
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// --- Per-command help ---
|
|
110
|
+
|
|
111
|
+
describe('CLI — per-command help', () => {
|
|
112
|
+
test('<command> --help prints command-specific help and exits 0', async () => {
|
|
113
|
+
const result = await runCli('build', '--help')
|
|
114
|
+
expect(result.exitCode).toBe(0)
|
|
115
|
+
expect(result.stdout).toContain('facet build')
|
|
116
|
+
expect(result.stdout).toContain('Build a facet from the current directory')
|
|
117
|
+
expect(result.stderr).toBe('')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// --- Unexpected error ---
|
|
122
|
+
|
|
123
|
+
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'] = {
|
|
131
|
+
name: 'crash',
|
|
132
|
+
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')
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
import { writeScaffold } from '../commands/create/index.ts'
|
|
6
|
+
|
|
7
|
+
let testDir: string
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
testDir = await mkdtemp(join(tmpdir(), 'cli-create-build-test-'))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await rm(testDir, { recursive: true, force: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
async function createFixtureDir(name: string): Promise<string> {
|
|
18
|
+
const dir = join(testDir, name)
|
|
19
|
+
await Bun.write(join(dir, '.keep'), '')
|
|
20
|
+
return dir
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CLI_PATH = resolve(import.meta.dir, '../../dist/facet')
|
|
24
|
+
|
|
25
|
+
async function runCli(...args: string[]) {
|
|
26
|
+
const proc = Bun.spawn([CLI_PATH, ...args], {
|
|
27
|
+
stdout: 'pipe',
|
|
28
|
+
stderr: 'pipe',
|
|
29
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
30
|
+
})
|
|
31
|
+
const stdout = await new Response(proc.stdout).text()
|
|
32
|
+
const stderr = await new Response(proc.stderr).text()
|
|
33
|
+
const exitCode = await proc.exited
|
|
34
|
+
|
|
35
|
+
// Don't let build errors flood test output — capture but don't dump
|
|
36
|
+
if (exitCode !== 0 && stderr.trim()) {
|
|
37
|
+
const lines = stderr.trim().split('\n')
|
|
38
|
+
const summary =
|
|
39
|
+
lines.length > 3 ? [...lines.slice(0, 3), `... (${lines.length - 3} more lines)`].join('\n') : stderr.trim()
|
|
40
|
+
return { stdout: stdout.trim(), stderr: summary, exitCode }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Scaffold generation (unit) ---
|
|
47
|
+
|
|
48
|
+
describe('writeScaffold', () => {
|
|
49
|
+
test('scaffolds with named assets across all types', async () => {
|
|
50
|
+
const dir = await createFixtureDir('scaffold-all')
|
|
51
|
+
const files = await writeScaffold(
|
|
52
|
+
{
|
|
53
|
+
name: 'my-facet',
|
|
54
|
+
version: '0.1.0',
|
|
55
|
+
description: 'A test facet',
|
|
56
|
+
skills: ['code-review', 'testing-guide'],
|
|
57
|
+
agents: ['reviewer'],
|
|
58
|
+
commands: ['deploy'],
|
|
59
|
+
},
|
|
60
|
+
dir,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
expect(files).toContain('facet.json')
|
|
64
|
+
expect(files).toContain('skills/code-review.md')
|
|
65
|
+
expect(files).toContain('skills/testing-guide.md')
|
|
66
|
+
expect(files).toContain('agents/reviewer.md')
|
|
67
|
+
expect(files).toContain('commands/deploy.md')
|
|
68
|
+
|
|
69
|
+
// Verify manifest content (JSON)
|
|
70
|
+
const manifestText = await Bun.file(join(dir, 'facet.json')).text()
|
|
71
|
+
const manifest = JSON.parse(manifestText)
|
|
72
|
+
expect(manifest.name).toBe('my-facet')
|
|
73
|
+
expect(manifest.version).toBe('0.1.0')
|
|
74
|
+
expect(manifest.description).toBe('A test facet')
|
|
75
|
+
expect(manifest.skills).toBeDefined()
|
|
76
|
+
expect(manifest.skills['code-review']).toBeDefined()
|
|
77
|
+
expect(manifest.skills['testing-guide']).toBeDefined()
|
|
78
|
+
expect(manifest.agents).toBeDefined()
|
|
79
|
+
expect(manifest.agents.reviewer).toBeDefined()
|
|
80
|
+
expect(manifest.commands).toBeDefined()
|
|
81
|
+
expect(manifest.commands.deploy).toBeDefined()
|
|
82
|
+
|
|
83
|
+
// Verify starter files exist and have named template content
|
|
84
|
+
const skill = await Bun.file(join(dir, 'skills/code-review.md')).text()
|
|
85
|
+
expect(skill).toContain('# Code Review')
|
|
86
|
+
|
|
87
|
+
const skill2 = await Bun.file(join(dir, 'skills/testing-guide.md')).text()
|
|
88
|
+
expect(skill2).toContain('# Testing Guide')
|
|
89
|
+
|
|
90
|
+
const agent = await Bun.file(join(dir, 'agents/reviewer.md')).text()
|
|
91
|
+
expect(agent).toContain('# Reviewer')
|
|
92
|
+
|
|
93
|
+
const command = await Bun.file(join(dir, 'commands/deploy.md')).text()
|
|
94
|
+
expect(command).toContain('# Deploy')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('scaffolds with only one skill', async () => {
|
|
98
|
+
const dir = await createFixtureDir('scaffold-skills-only')
|
|
99
|
+
const files = await writeScaffold(
|
|
100
|
+
{
|
|
101
|
+
name: 'minimal',
|
|
102
|
+
version: '0.1.0',
|
|
103
|
+
description: '',
|
|
104
|
+
skills: ['minimal'],
|
|
105
|
+
agents: [],
|
|
106
|
+
commands: [],
|
|
107
|
+
},
|
|
108
|
+
dir,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
expect(files).toContain('facet.json')
|
|
112
|
+
expect(files).toContain('skills/minimal.md')
|
|
113
|
+
expect(files).not.toContain('agents/')
|
|
114
|
+
expect(files).not.toContain('commands/')
|
|
115
|
+
|
|
116
|
+
const manifestText = await Bun.file(join(dir, 'facet.json')).text()
|
|
117
|
+
const manifest = JSON.parse(manifestText)
|
|
118
|
+
expect(manifest.skills).toBeDefined()
|
|
119
|
+
expect(manifest.agents).toBeUndefined()
|
|
120
|
+
expect(manifest.commands).toBeUndefined()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('scaffolded project passes build', async () => {
|
|
124
|
+
const dir = await createFixtureDir('scaffold-buildable')
|
|
125
|
+
await writeScaffold(
|
|
126
|
+
{
|
|
127
|
+
name: 'buildable',
|
|
128
|
+
version: '0.1.0',
|
|
129
|
+
description: 'A buildable facet',
|
|
130
|
+
skills: ['helper'],
|
|
131
|
+
agents: ['assistant'],
|
|
132
|
+
commands: [],
|
|
133
|
+
},
|
|
134
|
+
dir,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
// Run facet build against the scaffolded project
|
|
138
|
+
const result = await runCli('build', dir)
|
|
139
|
+
expect(result.exitCode).toBe(0)
|
|
140
|
+
expect(result.stdout).toContain('Built buildable')
|
|
141
|
+
|
|
142
|
+
// Verify dist/ output exists — archive + build manifest
|
|
143
|
+
const distArchive = await Bun.file(join(dir, 'dist/buildable-0.1.0.facet')).exists()
|
|
144
|
+
expect(distArchive).toBe(true)
|
|
145
|
+
|
|
146
|
+
const distManifest = await Bun.file(join(dir, 'dist/build-manifest.json')).exists()
|
|
147
|
+
expect(distManifest).toBe(true)
|
|
148
|
+
|
|
149
|
+
// No loose files
|
|
150
|
+
const looseManifest = await Bun.file(join(dir, 'dist/facet.json')).exists()
|
|
151
|
+
expect(looseManifest).toBe(false)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// --- Build command (e2e) ---
|
|
156
|
+
|
|
157
|
+
describe('facet build', () => {
|
|
158
|
+
test('build succeeds on valid project', async () => {
|
|
159
|
+
const dir = await createFixtureDir('build-valid')
|
|
160
|
+
await Bun.write(join(dir, 'skills/review.md'), '# Review skill content')
|
|
161
|
+
await Bun.write(
|
|
162
|
+
join(dir, 'facet.json'),
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
name: 'test-facet',
|
|
165
|
+
version: '1.0.0',
|
|
166
|
+
skills: {
|
|
167
|
+
review: {
|
|
168
|
+
description: 'Code review skill',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const result = await runCli('build', dir)
|
|
175
|
+
expect(result.exitCode).toBe(0)
|
|
176
|
+
expect(result.stdout).toContain('Built test-facet')
|
|
177
|
+
expect(result.stdout).toContain('sha256:')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('build fails on missing manifest', async () => {
|
|
181
|
+
const dir = await createFixtureDir('build-no-manifest')
|
|
182
|
+
|
|
183
|
+
const result = await runCli('build', dir)
|
|
184
|
+
expect(result.exitCode).toBe(1)
|
|
185
|
+
expect(result.stdout).toContain('Build failed')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('build fails on missing asset file', async () => {
|
|
189
|
+
const dir = await createFixtureDir('build-missing-file')
|
|
190
|
+
await Bun.write(
|
|
191
|
+
join(dir, 'facet.json'),
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
name: 'test-facet',
|
|
194
|
+
version: '1.0.0',
|
|
195
|
+
skills: {
|
|
196
|
+
review: {
|
|
197
|
+
description: 'Code review skill',
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const result = await runCli('build', dir)
|
|
204
|
+
expect(result.exitCode).toBe(1)
|
|
205
|
+
expect(result.stdout).toContain('Build failed')
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
|
|
6
|
+
export const buildCommand: Command = {
|
|
7
|
+
name: 'build',
|
|
8
|
+
description: 'Build a facet from the current directory',
|
|
9
|
+
run: async (args: string[]): Promise<number> => {
|
|
10
|
+
const rootDir = args[0] || process.cwd()
|
|
11
|
+
|
|
12
|
+
// Track result for stdout summary after Ink exits
|
|
13
|
+
let buildName = ''
|
|
14
|
+
let buildVersion = ''
|
|
15
|
+
let artifactCount = 0
|
|
16
|
+
let integrity = ''
|
|
17
|
+
let errorCount = 0
|
|
18
|
+
|
|
19
|
+
const instance = render(
|
|
20
|
+
createElement(BuildView, {
|
|
21
|
+
rootDir,
|
|
22
|
+
onSuccess: (name: string, version: string, fileCount: number, hash: string) => {
|
|
23
|
+
buildName = name
|
|
24
|
+
buildVersion = version
|
|
25
|
+
artifactCount = fileCount
|
|
26
|
+
integrity = hash
|
|
27
|
+
},
|
|
28
|
+
onFailure: (count: number) => {
|
|
29
|
+
errorCount = count
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await instance.waitUntilExit()
|
|
36
|
+
// Ink has unmounted — print stdout summary for scroll-back
|
|
37
|
+
const shortHash = integrity.length > 20 ? `${integrity.slice(0, 20)}...` : integrity
|
|
38
|
+
process.stdout.write(`✓ Built ${buildName} v${buildVersion} → dist/ (${artifactCount} assets, ${shortHash})\n`)
|
|
39
|
+
return 0
|
|
40
|
+
} catch {
|
|
41
|
+
process.stdout.write(`✗ Build failed — ${errorCount} error${errorCount !== 1 ? 's' : ''}\n`)
|
|
42
|
+
return 1
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Command } from '../../commands.ts'
|
|
2
|
+
import { writeScaffold } from '../create-scaffold.ts'
|
|
3
|
+
import { runCreateWizardInk } from './wizard.tsx'
|
|
4
|
+
|
|
5
|
+
export type { CreateOptions } from '../create-scaffold.ts'
|
|
6
|
+
export { writeScaffold } from '../create-scaffold.ts'
|
|
7
|
+
|
|
8
|
+
export const createCommand: Command = {
|
|
9
|
+
name: 'create',
|
|
10
|
+
description: 'Create a new facet project interactively',
|
|
11
|
+
run: async (args: string[]): Promise<number> => {
|
|
12
|
+
const targetDir = args[0] || process.cwd()
|
|
13
|
+
|
|
14
|
+
const opts = await runCreateWizardInk()
|
|
15
|
+
if (!opts) {
|
|
16
|
+
console.log('\nCancelled.')
|
|
17
|
+
return 1
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const files = await writeScaffold(opts, targetDir)
|
|
21
|
+
|
|
22
|
+
console.log(`\nFacet created: ${opts.name}`)
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
console.log(` ${file}`)
|
|
25
|
+
}
|
|
26
|
+
console.log('\nRun "facet build" to validate your facet.')
|
|
27
|
+
|
|
28
|
+
return 0
|
|
29
|
+
},
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { render } from 'ink'
|
|
2
|
+
import { CreateWizard } from '../../tui/views/create/wizard.tsx'
|
|
3
|
+
import type { CreateOptions } from '../create-scaffold.ts'
|
|
4
|
+
|
|
5
|
+
export async function runCreateWizardInk(): Promise<CreateOptions | null> {
|
|
6
|
+
return new Promise<CreateOptions | null>((resolve) => {
|
|
7
|
+
let result: CreateOptions | null = null
|
|
8
|
+
|
|
9
|
+
const instance = render(
|
|
10
|
+
<CreateWizard
|
|
11
|
+
onComplete={(opts) => {
|
|
12
|
+
result = opts
|
|
13
|
+
}}
|
|
14
|
+
onCancel={() => {
|
|
15
|
+
result = null
|
|
16
|
+
}}
|
|
17
|
+
/>,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
instance.waitUntilExit().then(() => {
|
|
21
|
+
resolve(result)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
}
|