@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.
- package/README.md +4 -0
- package/dist/commands/agent.d.ts +8 -0
- package/dist/commands/agent.js +150 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/create.js +30 -4
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/install-knowledge.js +65 -3
- package/dist/commands/install-knowledge.js.map +1 -1
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +80 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/pack.d.ts +10 -0
- package/dist/commands/pack.js +512 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +167 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +74 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/hook-packs/installer.d.ts +0 -7
- package/dist/hook-packs/installer.js +1 -14
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +1 -18
- package/dist/hook-packs/registry.d.ts +2 -1
- package/dist/hook-packs/registry.ts +1 -1
- package/dist/main.js +40 -1
- package/dist/main.js.map +1 -1
- package/dist/prompts/archetypes.d.ts +1 -0
- package/dist/prompts/archetypes.js +177 -32
- package/dist/prompts/archetypes.js.map +1 -1
- package/dist/prompts/create-wizard.js +105 -60
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/prompts/playbook.d.ts +8 -7
- package/dist/prompts/playbook.js +312 -30
- package/dist/prompts/playbook.js.map +1 -1
- package/dist/utils/checks.d.ts +0 -1
- package/dist/utils/checks.js +1 -1
- package/dist/utils/checks.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/archetypes.test.ts +84 -0
- package/src/__tests__/doctor.test.ts +2 -2
- package/src/__tests__/wizard-e2e.mjs +508 -0
- package/src/commands/agent.ts +181 -0
- package/src/commands/create.ts +146 -104
- package/src/commands/install-knowledge.ts +75 -4
- package/src/commands/install.ts +101 -0
- package/src/commands/pack.ts +585 -0
- package/src/commands/skills.ts +191 -0
- package/src/commands/uninstall.ts +93 -0
- package/src/hook-packs/installer.ts +1 -18
- package/src/hook-packs/registry.ts +1 -1
- package/src/main.ts +42 -1
- package/src/prompts/archetypes.ts +193 -62
- package/src/prompts/create-wizard.ts +114 -58
- package/src/prompts/playbook.ts +207 -21
- 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);
|