@soleri/cli 1.9.0 → 1.10.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/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 +38 -6
- 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 -62
- package/dist/prompts/archetypes.js.map +1 -1
- package/dist/prompts/create-wizard.d.ts +3 -3
- package/dist/prompts/create-wizard.js +99 -50
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/prompts/playbook.d.ts +8 -7
- package/dist/prompts/playbook.js +201 -15
- 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 +152 -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 +117 -61
- package/src/prompts/playbook.ts +207 -21
- package/src/utils/checks.ts +1 -1
- package/code-reviewer/.claude/hookify.focus-ring-required.local.md +0 -21
- package/code-reviewer/.claude/hookify.no-ai-attribution.local.md +0 -18
- package/code-reviewer/.claude/hookify.no-any-types.local.md +0 -18
- package/code-reviewer/.claude/hookify.no-console-log.local.md +0 -21
- package/code-reviewer/.claude/hookify.no-important.local.md +0 -18
- package/code-reviewer/.claude/hookify.no-inline-styles.local.md +0 -21
- package/code-reviewer/.claude/hookify.semantic-html.local.md +0 -18
- package/code-reviewer/.claude/hookify.ux-touch-targets.local.md +0 -18
- package/code-reviewer/.mcp.json +0 -11
- package/code-reviewer/README.md +0 -346
- package/code-reviewer/package-lock.json +0 -4484
- package/code-reviewer/package.json +0 -45
- package/code-reviewer/scripts/copy-assets.js +0 -15
- package/code-reviewer/scripts/setup.sh +0 -130
- package/code-reviewer/skills/brainstorming/SKILL.md +0 -170
- package/code-reviewer/skills/code-patrol/SKILL.md +0 -176
- package/code-reviewer/skills/context-resume/SKILL.md +0 -143
- package/code-reviewer/skills/executing-plans/SKILL.md +0 -201
- package/code-reviewer/skills/fix-and-learn/SKILL.md +0 -164
- package/code-reviewer/skills/health-check/SKILL.md +0 -225
- package/code-reviewer/skills/second-opinion/SKILL.md +0 -142
- package/code-reviewer/skills/systematic-debugging/SKILL.md +0 -230
- package/code-reviewer/skills/verification-before-completion/SKILL.md +0 -170
- package/code-reviewer/skills/writing-plans/SKILL.md +0 -207
- package/code-reviewer/src/__tests__/facades.test.ts +0 -598
- package/code-reviewer/src/activation/activate.ts +0 -125
- package/code-reviewer/src/activation/claude-md-content.ts +0 -217
- package/code-reviewer/src/activation/inject-claude-md.ts +0 -113
- package/code-reviewer/src/extensions/index.ts +0 -47
- package/code-reviewer/src/extensions/ops/example.ts +0 -28
- package/code-reviewer/src/identity/persona.ts +0 -62
- package/code-reviewer/src/index.ts +0 -278
- package/code-reviewer/src/intelligence/data/architecture.json +0 -5
- package/code-reviewer/src/intelligence/data/code-review.json +0 -5
- package/code-reviewer/tsconfig.json +0 -30
- 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
|
+
}
|