@soleri/cli 1.8.0 → 1.10.0

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 (57) hide show
  1. package/README.md +4 -0
  2. package/dist/commands/agent.d.ts +8 -0
  3. package/dist/commands/agent.js +150 -0
  4. package/dist/commands/agent.js.map +1 -0
  5. package/dist/commands/create.js +30 -4
  6. package/dist/commands/create.js.map +1 -1
  7. package/dist/commands/install-knowledge.js +65 -3
  8. package/dist/commands/install-knowledge.js.map +1 -1
  9. package/dist/commands/install.d.ts +2 -0
  10. package/dist/commands/install.js +80 -0
  11. package/dist/commands/install.js.map +1 -0
  12. package/dist/commands/pack.d.ts +10 -0
  13. package/dist/commands/pack.js +512 -0
  14. package/dist/commands/pack.js.map +1 -0
  15. package/dist/commands/skills.d.ts +8 -0
  16. package/dist/commands/skills.js +167 -0
  17. package/dist/commands/skills.js.map +1 -0
  18. package/dist/commands/uninstall.d.ts +2 -0
  19. package/dist/commands/uninstall.js +74 -0
  20. package/dist/commands/uninstall.js.map +1 -0
  21. package/dist/hook-packs/installer.d.ts +0 -7
  22. package/dist/hook-packs/installer.js +1 -14
  23. package/dist/hook-packs/installer.js.map +1 -1
  24. package/dist/hook-packs/installer.ts +1 -18
  25. package/dist/hook-packs/registry.d.ts +2 -1
  26. package/dist/hook-packs/registry.ts +1 -1
  27. package/dist/main.js +40 -1
  28. package/dist/main.js.map +1 -1
  29. package/dist/prompts/archetypes.d.ts +1 -0
  30. package/dist/prompts/archetypes.js +177 -32
  31. package/dist/prompts/archetypes.js.map +1 -1
  32. package/dist/prompts/create-wizard.js +105 -60
  33. package/dist/prompts/create-wizard.js.map +1 -1
  34. package/dist/prompts/playbook.d.ts +8 -7
  35. package/dist/prompts/playbook.js +312 -30
  36. package/dist/prompts/playbook.js.map +1 -1
  37. package/dist/utils/checks.d.ts +0 -1
  38. package/dist/utils/checks.js +1 -1
  39. package/dist/utils/checks.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/archetypes.test.ts +84 -0
  42. package/src/__tests__/doctor.test.ts +2 -2
  43. package/src/__tests__/wizard-e2e.mjs +508 -0
  44. package/src/commands/agent.ts +181 -0
  45. package/src/commands/create.ts +146 -104
  46. package/src/commands/install-knowledge.ts +75 -4
  47. package/src/commands/install.ts +101 -0
  48. package/src/commands/pack.ts +585 -0
  49. package/src/commands/skills.ts +191 -0
  50. package/src/commands/uninstall.ts +93 -0
  51. package/src/hook-packs/installer.ts +1 -18
  52. package/src/hook-packs/registry.ts +1 -1
  53. package/src/main.ts +42 -1
  54. package/src/prompts/archetypes.ts +193 -62
  55. package/src/prompts/create-wizard.ts +114 -58
  56. package/src/prompts/playbook.ts +207 -21
  57. package/src/utils/checks.ts +1 -1
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ARCHETYPES } from '../prompts/archetypes.js';
3
+ import {
4
+ CORE_SKILLS,
5
+ SKILL_CATEGORIES,
6
+ DOMAIN_OPTIONS,
7
+ PRINCIPLE_CATEGORIES,
8
+ } from '../prompts/playbook.js';
9
+
10
+ const allDomainValues = DOMAIN_OPTIONS.map((d) => d.value);
11
+ const allPrincipleValues = PRINCIPLE_CATEGORIES.flatMap((c) => c.options.map((o) => o.value));
12
+ const allOptionalSkillValues = SKILL_CATEGORIES.flatMap((c) => c.options.map((o) => o.value));
13
+
14
+ describe('Archetypes', () => {
15
+ it('should have unique values', () => {
16
+ const values = ARCHETYPES.map((a) => a.value);
17
+ expect(new Set(values).size).toBe(values.length);
18
+ });
19
+
20
+ it('should all have tier field', () => {
21
+ for (const a of ARCHETYPES) {
22
+ expect(a.tier).toMatch(/^(free|premium)$/);
23
+ }
24
+ });
25
+
26
+ it('should have at least 9 archetypes', () => {
27
+ expect(ARCHETYPES.length).toBeGreaterThanOrEqual(9);
28
+ });
29
+
30
+ it('should reference only valid domains', () => {
31
+ for (const a of ARCHETYPES) {
32
+ for (const d of a.defaults.domains) {
33
+ expect(allDomainValues).toContain(d);
34
+ }
35
+ }
36
+ });
37
+
38
+ it('should reference only valid principles', () => {
39
+ for (const a of ARCHETYPES) {
40
+ for (const pr of a.defaults.principles) {
41
+ expect(allPrincipleValues).toContain(pr);
42
+ }
43
+ }
44
+ });
45
+
46
+ it('should not include core skills in archetype skills', () => {
47
+ const coreSet = new Set<string>(CORE_SKILLS);
48
+ for (const a of ARCHETYPES) {
49
+ for (const s of a.defaults.skills) {
50
+ expect(coreSet.has(s)).toBe(false);
51
+ }
52
+ }
53
+ });
54
+
55
+ it('should reference only valid optional skills', () => {
56
+ for (const a of ARCHETYPES) {
57
+ for (const s of a.defaults.skills) {
58
+ expect(allOptionalSkillValues).toContain(s);
59
+ }
60
+ }
61
+ });
62
+
63
+ it('should include Accessibility Guardian', () => {
64
+ expect(ARCHETYPES.find((a) => a.value === 'accessibility-guardian')).toBeDefined();
65
+ });
66
+
67
+ it('should include Documentation Writer', () => {
68
+ expect(ARCHETYPES.find((a) => a.value === 'documentation-writer')).toBeDefined();
69
+ });
70
+ });
71
+
72
+ describe('Core Skills', () => {
73
+ it('should include writing-plans and executing-plans', () => {
74
+ expect(CORE_SKILLS).toContain('writing-plans');
75
+ expect(CORE_SKILLS).toContain('executing-plans');
76
+ });
77
+
78
+ it('should not appear in optional skill categories', () => {
79
+ const coreSet = new Set<string>(CORE_SKILLS);
80
+ for (const s of allOptionalSkillValues) {
81
+ expect(coreSet.has(s)).toBe(false);
82
+ }
83
+ });
84
+ });
@@ -102,7 +102,7 @@ describe('doctor command', () => {
102
102
  });
103
103
 
104
104
  describe('runAllChecks', () => {
105
- it('should return array of check results', () => {
105
+ it('should return array of check results', { timeout: 20_000 }, () => {
106
106
  const results = runAllChecks(tempDir);
107
107
  expect(results.length).toBeGreaterThan(0);
108
108
  for (const r of results) {
@@ -111,7 +111,7 @@ describe('doctor command', () => {
111
111
  }
112
112
  });
113
113
 
114
- it('should include Node and npm checks regardless of directory', () => {
114
+ it('should include Node and npm checks regardless of directory', { timeout: 20_000 }, () => {
115
115
  const results = runAllChecks(tempDir);
116
116
  const labels = results.map((r) => r.label);
117
117
  expect(labels).toContain('Node.js');
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Comprehensive E2E tests for the interactive create wizard.
4
+ *
5
+ * Tests all 7 archetypes, custom path, custom greeting, custom domains/principles,
6
+ * hook pack selection, cancel flows, and decline flows.
7
+ *
8
+ * Run: node packages/cli/src/__tests__/wizard-e2e.mjs
9
+ */
10
+ import { spawn } from 'node:child_process';
11
+ import { existsSync, readdirSync, readFileSync, rmSync, mkdirSync } from 'node:fs';
12
+ import { join, dirname } from 'node:path';
13
+ import { tmpdir } from 'node:os';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const CLI = join(__dirname, '..', '..', 'dist', 'main.js');
18
+ const TEST_ROOT = join(tmpdir(), `soleri-e2e-${Date.now()}`);
19
+ mkdirSync(TEST_ROOT, { recursive: true });
20
+
21
+ // ─── Test harness ────────────────────────────────────────
22
+
23
+ let totalPass = 0;
24
+ let totalFail = 0;
25
+ const failures = [];
26
+
27
+ function assert(cond, msg, ctx = '') {
28
+ if (cond) {
29
+ totalPass++;
30
+ } else {
31
+ totalFail++;
32
+ failures.push(ctx ? `${ctx}: ${msg}` : msg);
33
+ console.error(` FAIL: ${msg}`);
34
+ }
35
+ }
36
+
37
+ function stripAnsi(s) {
38
+ return s
39
+ .replace(/\x1B\[[0-9;]*[A-Za-z]/g, '')
40
+ .replace(/\x1B\].*?\x07/g, '')
41
+ .replace(/\r/g, '');
42
+ }
43
+
44
+ function sleep(ms) {
45
+ return new Promise((r) => setTimeout(r, ms));
46
+ }
47
+
48
+ const ENTER = '\r';
49
+ const CTRL_C = '\x03';
50
+ const CTRL_U = '\x15';
51
+ const DOWN = '\x1B[B';
52
+ const LEFT = '\x1B[D';
53
+ const SPACE = ' ';
54
+
55
+ /**
56
+ * Drive the CLI wizard by waiting for patterns and sending keystrokes.
57
+ */
58
+ function runWizard(name, actions, opts = {}) {
59
+ const timeout = opts.timeout || 180000;
60
+ return new Promise((resolve) => {
61
+ let buffer = '';
62
+ let completed = false;
63
+ let actionIndex = 0;
64
+
65
+ const proc = spawn('node', [CLI, 'create'], {
66
+ env: { ...process.env, TERM: 'xterm-256color', COLUMNS: '120', LINES: '40' },
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ });
69
+
70
+ proc.stdout.on('data', (d) => { buffer += d.toString(); });
71
+ proc.stderr.on('data', (d) => { buffer += d.toString(); });
72
+
73
+ async function drive() {
74
+ while (actionIndex < actions.length && !completed) {
75
+ const a = actions[actionIndex];
76
+ const clean = stripAnsi(buffer);
77
+ const matched =
78
+ typeof a.waitFor === 'string'
79
+ ? clean.toLowerCase().includes(a.waitFor.toLowerCase())
80
+ : a.waitFor.test(clean);
81
+
82
+ if (matched) {
83
+ actionIndex++;
84
+ await sleep(a.delay || 150);
85
+ if (!completed) {
86
+ try {
87
+ proc.stdin.write(a.send);
88
+ } catch {}
89
+ }
90
+ } else {
91
+ await sleep(100);
92
+ }
93
+ }
94
+ }
95
+
96
+ drive();
97
+ const poller = setInterval(() => {
98
+ if (!completed) drive();
99
+ }, 300);
100
+
101
+ proc.on('close', (code) => {
102
+ completed = true;
103
+ clearInterval(poller);
104
+ clearTimeout(timer);
105
+ resolve({
106
+ exitCode: code,
107
+ output: stripAnsi(buffer),
108
+ actionsCompleted: actionIndex,
109
+ });
110
+ });
111
+
112
+ const timer = setTimeout(() => {
113
+ if (!completed) {
114
+ completed = true;
115
+ clearInterval(poller);
116
+ proc.kill('SIGTERM');
117
+ setTimeout(() => {
118
+ try { proc.kill('SIGKILL'); } catch {}
119
+ resolve({
120
+ exitCode: -1,
121
+ output: stripAnsi(buffer) + '\n[TIMEOUT]',
122
+ actionsCompleted: actionIndex,
123
+ });
124
+ }, 2000);
125
+ }
126
+ }, timeout);
127
+ });
128
+ }
129
+
130
+ // ─── Helper: standard archetype actions ──────────────────
131
+
132
+ function archetypeActions(outDir, { downCount = 0 } = {}) {
133
+ const arrows = DOWN.repeat(downCount);
134
+ return [
135
+ { waitFor: 'kind of agent', send: arrows + SPACE + ENTER },
136
+ { waitFor: 'Display name', send: ENTER },
137
+ { waitFor: 'Role', send: ENTER },
138
+ { waitFor: 'Description', send: ENTER },
139
+ { waitFor: /domain|expertise/i, send: ENTER },
140
+ { waitFor: /principle|guiding/i, send: ENTER },
141
+ { waitFor: /tone/i, send: ENTER },
142
+ { waitFor: /skill/i, send: ENTER },
143
+ { waitFor: /greeting/i, send: ENTER },
144
+ { waitFor: /output|directory/i, send: CTRL_U + outDir + ENTER, delay: 300 },
145
+ { waitFor: /hook|pack/i, send: ENTER },
146
+ { waitFor: /create agent/i, send: ENTER },
147
+ ];
148
+ }
149
+
150
+ // ─── Archetype definitions for validation ────────────────
151
+
152
+ // Note: agentId is slugify(label), not the archetype value.
153
+ // e.g., "Full-Stack Assistant" → "full-stack-assistant"
154
+ const ARCHETYPES = [
155
+ { value: 'code-reviewer', agentId: 'code-reviewer', label: 'Code Reviewer', tone: 'mentor', totalSkills: 10, downCount: 0 },
156
+ { value: 'security-auditor', agentId: 'security-auditor', label: 'Security Auditor', tone: 'precise', totalSkills: 10, downCount: 1 },
157
+ { value: 'api-architect', agentId: 'api-architect', label: 'API Architect', tone: 'pragmatic', totalSkills: 10, downCount: 2 },
158
+ { value: 'test-engineer', agentId: 'test-engineer', label: 'Test Engineer', tone: 'mentor', totalSkills: 10, downCount: 3 },
159
+ { value: 'devops-pilot', agentId: 'devops-pilot', label: 'DevOps Pilot', tone: 'pragmatic', totalSkills: 10, downCount: 4 },
160
+ { value: 'database-architect', agentId: 'database-architect', label: 'Database Architect', tone: 'precise', totalSkills: 10, downCount: 5 },
161
+ { value: 'full-stack', agentId: 'full-stack-assistant', label: 'Full-Stack Assistant', tone: 'mentor', totalSkills: 11, downCount: 6 },
162
+ ];
163
+
164
+ // ══════════════════════════════════════════════════════════
165
+ // CANCEL TESTS
166
+ // ══════════════════════════════════════════════════════════
167
+
168
+ async function testCancelArchetype() {
169
+ console.log('\n [1/14] Cancel at archetype (Ctrl+C)');
170
+ const r = await runWizard('cancel-arch', [
171
+ { waitFor: 'kind of agent', send: CTRL_C },
172
+ ], { timeout: 15000 });
173
+ assert(r.actionsCompleted >= 1, 'prompt reached', 'cancel-archetype');
174
+ }
175
+
176
+ async function testCancelName() {
177
+ console.log('\n [2/14] Cancel at display name');
178
+ const r = await runWizard('cancel-name', [
179
+ { waitFor: 'kind of agent', send: SPACE + ENTER },
180
+ { waitFor: 'Display name', send: CTRL_C },
181
+ ], { timeout: 15000 });
182
+ assert(r.actionsCompleted >= 2, 'reached name prompt', 'cancel-name');
183
+ }
184
+
185
+ async function testCancelRole() {
186
+ console.log('\n [3/14] Cancel at role');
187
+ const r = await runWizard('cancel-role', [
188
+ { waitFor: 'kind of agent', send: SPACE + ENTER },
189
+ { waitFor: 'Display name', send: ENTER },
190
+ { waitFor: 'Role', send: CTRL_C },
191
+ ], { timeout: 15000 });
192
+ assert(r.actionsCompleted >= 3, 'reached role prompt', 'cancel-role');
193
+ }
194
+
195
+ async function testCancelSkills() {
196
+ console.log('\n [4/14] Cancel at skills');
197
+ const r = await runWizard('cancel-skills', [
198
+ { waitFor: 'kind of agent', send: SPACE + ENTER },
199
+ { waitFor: 'Display name', send: ENTER },
200
+ { waitFor: 'Role', send: ENTER },
201
+ { waitFor: 'Description', send: ENTER },
202
+ { waitFor: /domain|expertise/i, send: ENTER },
203
+ { waitFor: /principle|guiding/i, send: ENTER },
204
+ { waitFor: /tone/i, send: ENTER },
205
+ { waitFor: /skill/i, send: CTRL_C },
206
+ ], { timeout: 15000 });
207
+ assert(r.actionsCompleted >= 8, 'reached skills prompt', 'cancel-skills');
208
+ }
209
+
210
+ // ══════════════════════════════════════════════════════════
211
+ // DECLINE AT CONFIRMATION
212
+ // ══════════════════════════════════════════════════════════
213
+
214
+ async function testDeclineConfirm() {
215
+ console.log('\n [5/14] Decline at confirmation');
216
+ const outDir = join(TEST_ROOT, 'decline');
217
+ mkdirSync(outDir, { recursive: true });
218
+
219
+ const r = await runWizard('decline', [
220
+ { waitFor: 'kind of agent', send: SPACE + ENTER },
221
+ { waitFor: 'Display name', send: ENTER },
222
+ { waitFor: 'Role', send: ENTER },
223
+ { waitFor: 'Description', send: ENTER },
224
+ { waitFor: /domain|expertise/i, send: ENTER },
225
+ { waitFor: /principle|guiding/i, send: ENTER },
226
+ { waitFor: /tone/i, send: ENTER },
227
+ { waitFor: /skill/i, send: ENTER },
228
+ { waitFor: /greeting/i, send: ENTER },
229
+ { waitFor: /output|directory/i, send: CTRL_U + outDir + ENTER, delay: 300 },
230
+ { waitFor: /hook|pack/i, send: ENTER },
231
+ { waitFor: /create agent/i, send: LEFT + ENTER },
232
+ ], { timeout: 15000 });
233
+
234
+ assert(r.actionsCompleted >= 12, `all prompts reached (${r.actionsCompleted}/12)`, 'decline');
235
+ assert(!existsSync(join(outDir, 'code-reviewer', 'package.json')), 'no agent created', 'decline');
236
+ }
237
+
238
+ // ══════════════════════════════════════════════════════════
239
+ // ALL 7 ARCHETYPES
240
+ // ══════════════════════════════════════════════════════════
241
+
242
+ async function testArchetype(arch, idx) {
243
+ const testNum = 6 + idx;
244
+ console.log(`\n [${testNum}/14] Archetype: ${arch.label} (down×${arch.downCount})`);
245
+ const outDir = join(TEST_ROOT, arch.agentId);
246
+ mkdirSync(outDir, { recursive: true });
247
+
248
+ const r = await runWizard(arch.agentId, archetypeActions(outDir, { downCount: arch.downCount }));
249
+
250
+ const ad = join(outDir, arch.agentId);
251
+ const ctx = `archetype-${arch.agentId}`;
252
+
253
+ assert(r.actionsCompleted >= 11, `prompts reached (${r.actionsCompleted}/12)`, ctx);
254
+ assert(r.exitCode === 0, `exit 0 (got ${r.exitCode})`, ctx);
255
+ assert(existsSync(join(ad, 'package.json')), 'package.json exists', ctx);
256
+ assert(existsSync(join(ad, 'dist', 'index.js')), 'dist/index.js built', ctx);
257
+
258
+ // Validate persona
259
+ const personaPath = join(ad, 'src', 'identity', 'persona.ts');
260
+ if (existsSync(personaPath)) {
261
+ const persona = readFileSync(personaPath, 'utf-8');
262
+ assert(persona.includes(`'${arch.label}'`) || persona.includes(`"${arch.label}"`),
263
+ `name = ${arch.label}`, ctx);
264
+ assert(persona.includes(`tone: '${arch.tone}'`), `tone = ${arch.tone}`, ctx);
265
+ } else {
266
+ assert(false, 'persona.ts exists', ctx);
267
+ }
268
+
269
+ // Validate skills
270
+ const skillsDir = join(ad, 'skills');
271
+ if (existsSync(skillsDir)) {
272
+ const skills = readdirSync(skillsDir);
273
+ assert(skills.length === arch.totalSkills, `${arch.totalSkills} skills (got ${skills.length})`, ctx);
274
+ // Core skills always present
275
+ for (const core of ['brainstorming', 'systematic-debugging', 'verification-before-completion', 'health-check', 'context-resume', 'writing-plans', 'executing-plans']) {
276
+ assert(skills.includes(core), `core skill: ${core}`, ctx);
277
+ }
278
+ } else {
279
+ assert(false, 'skills/ dir exists', ctx);
280
+ }
281
+
282
+ // Validate package.json has correct name
283
+ if (existsSync(join(ad, 'package.json'))) {
284
+ const pkg = JSON.parse(readFileSync(join(ad, 'package.json'), 'utf-8'));
285
+ assert(pkg.name === `${arch.agentId}-mcp`, `package name = ${arch.agentId}-mcp`, ctx);
286
+ assert(pkg.dependencies?.['@soleri/core'], '@soleri/core dependency exists', ctx);
287
+ }
288
+
289
+ // Validate domains directory
290
+ const docsDir = join(ad, 'docs', 'vault', 'knowledge');
291
+ if (existsSync(docsDir)) {
292
+ const domains = readdirSync(docsDir);
293
+ assert(domains.length >= 1, `has domain dirs (got ${domains.length})`, ctx);
294
+ }
295
+
296
+ console.log(` exit=${r.exitCode}, agent=${existsSync(ad)}`);
297
+ }
298
+
299
+ // ══════════════════════════════════════════════════════════
300
+ // CUSTOM ARCHETYPE PATH
301
+ // ══════════════════════════════════════════════════════════
302
+
303
+ async function testCustomArchetype() {
304
+ console.log('\n [13/14] Custom archetype — full custom flow');
305
+ const outDir = join(TEST_ROOT, 'custom');
306
+ mkdirSync(outDir, { recursive: true });
307
+
308
+ // Navigate to "✦ Create Custom" (8th option = 7 downs)
309
+ const customName = 'GraphQL Guardian';
310
+ const customId = 'graphql-guardian';
311
+ const customRole = 'Validates GraphQL schemas against federation rules';
312
+ const customDesc = 'This agent checks GraphQL schemas for breaking changes, naming conventions, and federation compatibility across subgraphs.';
313
+ const customGreeting = "Hey! Drop your GraphQL schema and I will check it for issues.";
314
+
315
+ const r = await runWizard('custom', [
316
+ // Step 1: Select "✦ Create Custom" (9 downs — 9 archetypes before _custom)
317
+ { waitFor: 'kind of agent', send: DOWN.repeat(9) + SPACE + ENTER },
318
+ // Step 2: Type custom name
319
+ { waitFor: 'Display name', send: customName + ENTER },
320
+ // Step 3: Custom role (has playbook note first, then text prompt)
321
+ { waitFor: 'What does your agent do', send: customRole + ENTER },
322
+ // Step 4: Custom description
323
+ { waitFor: 'Describe your agent', send: customDesc + ENTER },
324
+ // Step 5: Domains — select security + api-design (space to toggle, then enter)
325
+ // Options: security(1st), code-review(2nd), testing(3rd), api-design(4th)
326
+ // None pre-selected for custom. Select security(space) + down×3 + api-design(space) + enter
327
+ { waitFor: /domain|expertise/i, send: SPACE + DOWN + DOWN + DOWN + SPACE + ENTER },
328
+ // Step 6: Principles — select first two (space, down, space, enter)
329
+ { waitFor: /principle|guiding/i, send: SPACE + DOWN + SPACE + ENTER },
330
+ // Step 7: Tone — select Precise (first option)
331
+ { waitFor: /tone/i, send: ENTER },
332
+ // Step 8: Skills — select vault-navigator + knowledge-harvest (space, down×2, space, enter)
333
+ { waitFor: /skill/i, send: SPACE + DOWN + DOWN + SPACE + ENTER },
334
+ // Step 9: Greeting — select Custom (down + enter)
335
+ { waitFor: /greeting/i, send: DOWN + ENTER },
336
+ // Custom greeting text prompt
337
+ { waitFor: 'Your greeting', send: customGreeting + ENTER },
338
+ // Step 10: Output directory
339
+ { waitFor: /output|directory/i, send: CTRL_U + outDir + ENTER, delay: 300 },
340
+ // Hook packs — skip
341
+ { waitFor: /hook|pack/i, send: ENTER },
342
+ // Confirm
343
+ { waitFor: /create agent/i, send: ENTER },
344
+ ]);
345
+
346
+ const ad = join(outDir, customId);
347
+ const ctx = 'custom-archetype';
348
+
349
+ assert(r.actionsCompleted >= 13, `prompts reached (${r.actionsCompleted}/14)`, ctx);
350
+ assert(r.exitCode === 0, `exit 0 (got ${r.exitCode})`, ctx);
351
+ assert(existsSync(join(ad, 'package.json')), 'package.json exists', ctx);
352
+
353
+ // Validate persona has custom values
354
+ const personaPath = join(ad, 'src', 'identity', 'persona.ts');
355
+ if (existsSync(personaPath)) {
356
+ const persona = readFileSync(personaPath, 'utf-8');
357
+ assert(persona.includes(customName), `name = ${customName}`, ctx);
358
+ assert(persona.includes("tone: 'precise'"), 'tone = precise', ctx);
359
+ assert(persona.includes(customRole), 'custom role present', ctx);
360
+ } else {
361
+ assert(false, 'persona.ts exists', ctx);
362
+ }
363
+
364
+ // Validate custom greeting
365
+ const greetingPath = join(ad, 'src', 'identity', 'persona.ts');
366
+ if (existsSync(greetingPath)) {
367
+ const content = readFileSync(greetingPath, 'utf-8');
368
+ assert(content.includes(customGreeting), 'custom greeting present', ctx);
369
+ }
370
+
371
+ // Validate skills: 7 core + 2 optional (vault-navigator, knowledge-harvest)
372
+ const skillsDir = join(ad, 'skills');
373
+ if (existsSync(skillsDir)) {
374
+ const skills = readdirSync(skillsDir);
375
+ assert(skills.length === 9, `9 skills (got ${skills.length})`, ctx);
376
+ assert(skills.includes('writing-plans'), 'has writing-plans (core)', ctx);
377
+ assert(skills.includes('vault-navigator'), 'has vault-navigator', ctx);
378
+ assert(!skills.includes('code-patrol'), 'no code-patrol (not selected)', ctx);
379
+ }
380
+
381
+ // Validate build
382
+ assert(existsSync(join(ad, 'dist', 'index.js')), 'dist/index.js built', ctx);
383
+
384
+ console.log(` exit=${r.exitCode}, agent=${existsSync(ad)}`);
385
+ }
386
+
387
+ // ══════════════════════════════════════════════════════════
388
+ // HOOK PACKS
389
+ // ══════════════════════════════════════════════════════════
390
+
391
+ async function testHookPacks() {
392
+ console.log('\n [14/14] Hook packs — select a11y + typescript-safety');
393
+ const outDir = join(TEST_ROOT, 'hooks');
394
+ mkdirSync(outDir, { recursive: true });
395
+
396
+ // Hook pack options order: a11y(1st), clean-commits(2nd), css-discipline(3rd),
397
+ // full(4th), typescript-safety(5th)
398
+ // Select a11y(space) + down×4 + typescript-safety(space) + enter
399
+ const r = await runWizard('hooks', [
400
+ { waitFor: 'kind of agent', send: SPACE + ENTER },
401
+ { waitFor: 'Display name', send: ENTER },
402
+ { waitFor: 'Role', send: ENTER },
403
+ { waitFor: 'Description', send: ENTER },
404
+ { waitFor: /domain|expertise/i, send: ENTER },
405
+ { waitFor: /principle|guiding/i, send: ENTER },
406
+ { waitFor: /tone/i, send: ENTER },
407
+ { waitFor: /skill/i, send: ENTER },
408
+ { waitFor: /greeting/i, send: ENTER },
409
+ { waitFor: /output|directory/i, send: CTRL_U + outDir + ENTER, delay: 300 },
410
+ // Hook packs: select a11y + typescript-safety
411
+ { waitFor: /hook|pack/i, send: SPACE + DOWN + DOWN + DOWN + DOWN + SPACE + ENTER },
412
+ { waitFor: /create agent/i, send: ENTER },
413
+ ]);
414
+
415
+ const ad = join(outDir, 'code-reviewer');
416
+ const ctx = 'hook-packs';
417
+
418
+ assert(r.actionsCompleted >= 11, `prompts reached (${r.actionsCompleted}/12)`, ctx);
419
+ assert(r.exitCode === 0, `exit 0 (got ${r.exitCode})`, ctx);
420
+
421
+ // Validate hooks were installed
422
+ const output = r.output;
423
+ assert(output.includes('a11y') && output.includes('installed'), 'a11y pack installed', ctx);
424
+ assert(output.includes('typescript-safety') && output.includes('installed'), 'typescript-safety pack installed', ctx);
425
+
426
+ // Check .claude directory has hooks
427
+ const claudeDir = join(ad, '.claude');
428
+ if (existsSync(claudeDir)) {
429
+ const files = readdirSync(claudeDir, { recursive: true }).map(String);
430
+ assert(files.length > 0, `.claude/ has hook files (${files.length})`, ctx);
431
+ }
432
+
433
+ console.log(` exit=${r.exitCode}, agent=${existsSync(ad)}, hooks=${r.output.includes('installed')}`);
434
+ }
435
+
436
+ // ══════════════════════════════════════════════════════════
437
+ // RUN ALL TESTS
438
+ // ══════════════════════════════════════════════════════════
439
+
440
+ console.log('═══════════════════════════════════════════════');
441
+ console.log(' SOLERI CLI WIZARD — COMPREHENSIVE E2E TESTS');
442
+ console.log('═══════════════════════════════════════════════');
443
+
444
+ const start = Date.now();
445
+
446
+ // Cancel flows (fast)
447
+ await testCancelArchetype();
448
+ await testCancelName();
449
+ await testCancelRole();
450
+ await testCancelSkills();
451
+
452
+ // Decline
453
+ await testDeclineConfirm();
454
+
455
+ // All 7 archetypes (each scaffolds + builds, slower)
456
+ for (let i = 0; i < ARCHETYPES.length; i++) {
457
+ await testArchetype(ARCHETYPES[i], i);
458
+ }
459
+
460
+ // Custom path
461
+ await testCustomArchetype();
462
+
463
+ // Hook packs
464
+ await testHookPacks();
465
+
466
+ // ─── Cleanup ─────────────────────────────────────────────
467
+ rmSync(TEST_ROOT, { recursive: true, force: true });
468
+
469
+ // Clean up any MCP registrations
470
+ try {
471
+ const claudeJson = join(
472
+ process.env.HOME || process.env.USERPROFILE || '',
473
+ '.claude.json',
474
+ );
475
+ if (existsSync(claudeJson)) {
476
+ const c = JSON.parse(readFileSync(claudeJson, 'utf-8'));
477
+ let changed = false;
478
+ for (const arch of ARCHETYPES) {
479
+ if (c.mcpServers?.[arch.agentId]) {
480
+ delete c.mcpServers[arch.agentId];
481
+ changed = true;
482
+ }
483
+ }
484
+ if (c.mcpServers?.['graphql-guardian']) {
485
+ delete c.mcpServers['graphql-guardian'];
486
+ changed = true;
487
+ }
488
+ if (changed) {
489
+ const { writeFileSync } = await import('node:fs');
490
+ writeFileSync(claudeJson, JSON.stringify(c, null, 2) + '\n');
491
+ }
492
+ }
493
+ } catch {}
494
+
495
+ // ─── Summary ─────────────────────────────────────────────
496
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
497
+
498
+ console.log(`\n${'═'.repeat(50)}`);
499
+ console.log(` RESULTS: ${totalPass} passed, ${totalFail} failed (${elapsed}s)`);
500
+ if (failures.length > 0) {
501
+ console.log('\n FAILURES:');
502
+ for (const f of failures) {
503
+ console.log(` • ${f}`);
504
+ }
505
+ }
506
+ console.log('═'.repeat(50));
507
+
508
+ process.exit(totalFail > 0 ? 1 : 0);