@soleri/cli 1.9.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 (93) 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 -62
  31. package/dist/prompts/archetypes.js.map +1 -1
  32. package/dist/prompts/create-wizard.js +98 -49
  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 +201 -15
  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
  58. package/code-reviewer/.claude/hookify.focus-ring-required.local.md +0 -21
  59. package/code-reviewer/.claude/hookify.no-ai-attribution.local.md +0 -18
  60. package/code-reviewer/.claude/hookify.no-any-types.local.md +0 -18
  61. package/code-reviewer/.claude/hookify.no-console-log.local.md +0 -21
  62. package/code-reviewer/.claude/hookify.no-important.local.md +0 -18
  63. package/code-reviewer/.claude/hookify.no-inline-styles.local.md +0 -21
  64. package/code-reviewer/.claude/hookify.semantic-html.local.md +0 -18
  65. package/code-reviewer/.claude/hookify.ux-touch-targets.local.md +0 -18
  66. package/code-reviewer/.mcp.json +0 -11
  67. package/code-reviewer/README.md +0 -346
  68. package/code-reviewer/package-lock.json +0 -4484
  69. package/code-reviewer/package.json +0 -45
  70. package/code-reviewer/scripts/copy-assets.js +0 -15
  71. package/code-reviewer/scripts/setup.sh +0 -130
  72. package/code-reviewer/skills/brainstorming/SKILL.md +0 -170
  73. package/code-reviewer/skills/code-patrol/SKILL.md +0 -176
  74. package/code-reviewer/skills/context-resume/SKILL.md +0 -143
  75. package/code-reviewer/skills/executing-plans/SKILL.md +0 -201
  76. package/code-reviewer/skills/fix-and-learn/SKILL.md +0 -164
  77. package/code-reviewer/skills/health-check/SKILL.md +0 -225
  78. package/code-reviewer/skills/second-opinion/SKILL.md +0 -142
  79. package/code-reviewer/skills/systematic-debugging/SKILL.md +0 -230
  80. package/code-reviewer/skills/verification-before-completion/SKILL.md +0 -170
  81. package/code-reviewer/skills/writing-plans/SKILL.md +0 -207
  82. package/code-reviewer/src/__tests__/facades.test.ts +0 -598
  83. package/code-reviewer/src/activation/activate.ts +0 -125
  84. package/code-reviewer/src/activation/claude-md-content.ts +0 -217
  85. package/code-reviewer/src/activation/inject-claude-md.ts +0 -113
  86. package/code-reviewer/src/extensions/index.ts +0 -47
  87. package/code-reviewer/src/extensions/ops/example.ts +0 -28
  88. package/code-reviewer/src/identity/persona.ts +0 -62
  89. package/code-reviewer/src/index.ts +0 -278
  90. package/code-reviewer/src/intelligence/data/architecture.json +0 -5
  91. package/code-reviewer/src/intelligence/data/code-review.json +0 -5
  92. package/code-reviewer/tsconfig.json +0 -30
  93. package/code-reviewer/vitest.config.ts +0 -23
@@ -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);
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Agent lifecycle CLI — status, update, diff.
3
+ *
4
+ * `soleri agent status` — health report with version, packs, vault stats.
5
+ * `soleri agent update` — OTA engine upgrade with migration support.
6
+ */
7
+
8
+ import { join } from 'node:path';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { execFileSync } from 'node:child_process';
11
+ import type { Command } from 'commander';
12
+ import * as p from '@clack/prompts';
13
+ import { PackLockfile, checkNpmVersion, checkVersionCompat } from '@soleri/core';
14
+ import { detectAgent } from '../utils/agent-context.js';
15
+
16
+ export function registerAgent(program: Command): void {
17
+ const agent = program.command('agent').description('Agent lifecycle management');
18
+
19
+ // ─── status ─────────────────────────────────────────────────
20
+ agent
21
+ .command('status')
22
+ .option('--json', 'Output as JSON')
23
+ .description('Show agent health: version, packs, vault, and update availability')
24
+ .action((opts: { json?: boolean }) => {
25
+ const ctx = detectAgent();
26
+ if (!ctx) {
27
+ p.log.error('No agent project detected in current directory.');
28
+ process.exit(1);
29
+ return;
30
+ }
31
+
32
+ // Read agent package.json
33
+ const pkgPath = join(ctx.agentPath, 'package.json');
34
+ const pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, 'utf-8')) : {};
35
+ const agentName = pkg.name || 'unknown';
36
+ const agentVersion = pkg.version || '0.0.0';
37
+
38
+ // Read @soleri/core version
39
+ const corePkgPath = join(ctx.agentPath, 'node_modules', '@soleri', 'core', 'package.json');
40
+ const coreVersion = existsSync(corePkgPath)
41
+ ? JSON.parse(readFileSync(corePkgPath, 'utf-8')).version || 'unknown'
42
+ : pkg.dependencies?.['@soleri/core'] || 'not installed';
43
+
44
+ // Check for core update
45
+ const latestCore = checkNpmVersion('@soleri/core');
46
+
47
+ // Read lockfile
48
+ const lockfilePath = join(ctx.agentPath, 'soleri.lock');
49
+ const lockfile = new PackLockfile(lockfilePath);
50
+ const packs = lockfile.list();
51
+
52
+ // Count vault entries if db exists
53
+ const dbPath = join(ctx.agentPath, 'data', 'vault.db');
54
+ const hasVault = existsSync(dbPath);
55
+
56
+ if (opts.json) {
57
+ console.log(
58
+ JSON.stringify(
59
+ {
60
+ agent: agentName,
61
+ version: agentVersion,
62
+ engine: coreVersion,
63
+ engineLatest: latestCore,
64
+ packs: packs.map((p) => ({
65
+ id: p.id,
66
+ version: p.version,
67
+ type: p.type,
68
+ source: p.source,
69
+ })),
70
+ vault: { exists: hasVault },
71
+ },
72
+ null,
73
+ 2,
74
+ ),
75
+ );
76
+ return;
77
+ }
78
+
79
+ console.log(`\n Agent: ${agentName} v${agentVersion}`);
80
+ console.log(
81
+ ` Engine: @soleri/core ${coreVersion}${latestCore && latestCore !== coreVersion ? ` (update available: ${latestCore})` : ''}`,
82
+ );
83
+
84
+ if (packs.length > 0) {
85
+ console.log(`\n Packs (${packs.length} installed):`);
86
+ for (const pack of packs) {
87
+ const badge =
88
+ pack.source === 'npm' ? ' [npm]' : pack.source === 'built-in' ? ' [built-in]' : '';
89
+ console.log(` ${pack.id}@${pack.version} ${pack.type}${badge}`);
90
+ }
91
+ } else {
92
+ console.log('\n Packs: none installed');
93
+ }
94
+
95
+ console.log(`\n Vault: ${hasVault ? 'initialized' : 'not initialized'}`);
96
+ console.log('');
97
+ });
98
+
99
+ // ─── update ─────────────────────────────────────────────────
100
+ agent
101
+ .command('update')
102
+ .option('--check', 'Show what would change without updating')
103
+ .option('--dry-run', 'Preview migration steps')
104
+ .description('Update agent engine to latest compatible version')
105
+ .action((opts: { check?: boolean; dryRun?: boolean }) => {
106
+ const ctx = detectAgent();
107
+ if (!ctx) {
108
+ p.log.error('No agent project detected in current directory.');
109
+ process.exit(1);
110
+ return;
111
+ }
112
+
113
+ const pkgPath = join(ctx.agentPath, 'package.json');
114
+ if (!existsSync(pkgPath)) {
115
+ p.log.error('No package.json found in agent directory.');
116
+ process.exit(1);
117
+ return;
118
+ }
119
+
120
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
121
+ const currentRange = pkg.dependencies?.['@soleri/core'] || '';
122
+ const latestCore = checkNpmVersion('@soleri/core');
123
+
124
+ if (!latestCore) {
125
+ p.log.error('Could not check npm for latest @soleri/core version.');
126
+ process.exit(1);
127
+ return;
128
+ }
129
+
130
+ // Check compatibility
131
+ const compatible = checkVersionCompat(latestCore, currentRange);
132
+
133
+ if (opts.check) {
134
+ console.log(`\n Current: @soleri/core ${currentRange}`);
135
+ console.log(` Latest: @soleri/core ${latestCore}`);
136
+ console.log(` Compatible: ${compatible ? 'yes' : 'no (range: ' + currentRange + ')'}`);
137
+ console.log('');
138
+ return;
139
+ }
140
+
141
+ if (opts.dryRun) {
142
+ p.log.info(`Would update @soleri/core to ${latestCore}`);
143
+ p.log.info('Would run: npm install @soleri/core@' + latestCore);
144
+ return;
145
+ }
146
+
147
+ const s = p.spinner();
148
+ s.start(`Updating @soleri/core to ${latestCore}...`);
149
+
150
+ try {
151
+ execFileSync('npm', ['install', `@soleri/core@${latestCore}`], {
152
+ cwd: ctx.agentPath,
153
+ stdio: 'pipe',
154
+ timeout: 120_000,
155
+ });
156
+
157
+ s.stop(`Updated to @soleri/core@${latestCore}`);
158
+ p.log.info('Run `soleri test` to verify the update.');
159
+ } catch (err) {
160
+ s.stop('Update failed');
161
+ p.log.error(err instanceof Error ? err.message : String(err));
162
+ process.exit(1);
163
+ }
164
+ });
165
+
166
+ // ─── diff ───────────────────────────────────────────────────
167
+ agent
168
+ .command('diff')
169
+ .description('Show drift between agent templates and latest engine templates')
170
+ .action(() => {
171
+ const ctx = detectAgent();
172
+ if (!ctx) {
173
+ p.log.error('No agent project detected in current directory.');
174
+ process.exit(1);
175
+ return;
176
+ }
177
+
178
+ p.log.info('Template diff is available after `soleri agent update --check`.');
179
+ p.log.info('Full template comparison will be added in a future release.');
180
+ });
181
+ }