claude-raid 0.1.1 → 0.1.3
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 +298 -196
- package/bin/cli.js +45 -18
- package/package.json +1 -1
- package/src/descriptions.js +57 -0
- package/src/detect-browser.js +164 -0
- package/src/detect-package-manager.js +107 -0
- package/src/detect-project.js +44 -6
- package/src/doctor.js +12 -188
- package/src/init.js +192 -17
- package/src/merge-settings.js +63 -7
- package/src/remove.js +28 -4
- package/src/setup.js +405 -0
- package/src/ui.js +168 -0
- package/src/update.js +62 -5
- package/src/version-check.js +130 -0
- package/template/.claude/agents/archer.md +46 -51
- package/template/.claude/agents/rogue.md +43 -49
- package/template/.claude/agents/warrior.md +48 -53
- package/template/.claude/agents/wizard.md +65 -67
- package/template/.claude/hooks/raid-lib.sh +182 -0
- package/template/.claude/hooks/raid-pre-compact.sh +41 -0
- package/template/.claude/hooks/raid-session-end.sh +116 -0
- package/template/.claude/hooks/raid-session-start.sh +52 -0
- package/template/.claude/hooks/raid-stop.sh +68 -0
- package/template/.claude/hooks/raid-task-completed.sh +37 -0
- package/template/.claude/hooks/raid-task-created.sh +40 -0
- package/template/.claude/hooks/raid-teammate-idle.sh +28 -0
- package/template/.claude/hooks/validate-browser-cleanup.sh +36 -0
- package/template/.claude/hooks/validate-browser-tests-exist.sh +52 -0
- package/template/.claude/hooks/validate-commit.sh +130 -0
- package/template/.claude/hooks/validate-dungeon.sh +114 -0
- package/template/.claude/hooks/validate-file-naming.sh +13 -27
- package/template/.claude/hooks/validate-no-placeholders.sh +11 -21
- package/template/.claude/hooks/validate-write-gate.sh +60 -0
- package/template/.claude/raid-rules.md +27 -18
- package/template/.claude/skills/raid-browser/SKILL.md +186 -0
- package/template/.claude/skills/raid-browser-chrome/SKILL.md +189 -0
- package/template/.claude/skills/raid-browser-playwright/SKILL.md +163 -0
- package/template/.claude/skills/raid-debugging/SKILL.md +6 -6
- package/template/.claude/skills/raid-design/SKILL.md +10 -10
- package/template/.claude/skills/raid-finishing/SKILL.md +11 -3
- package/template/.claude/skills/raid-implementation/SKILL.md +26 -11
- package/template/.claude/skills/raid-implementation-plan/SKILL.md +15 -4
- package/template/.claude/skills/raid-protocol/SKILL.md +57 -32
- package/template/.claude/skills/raid-review/SKILL.md +42 -13
- package/template/.claude/skills/raid-tdd/SKILL.md +45 -3
- package/template/.claude/skills/raid-verification/SKILL.md +12 -1
- package/template/.claude/hooks/validate-commit-message.sh +0 -78
- package/template/.claude/hooks/validate-phase-gate.sh +0 -60
- package/template/.claude/hooks/validate-tests-pass.sh +0 -43
- package/template/.claude/hooks/validate-verification.sh +0 -70
package/src/setup.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const { colors, box, header } = require('./ui');
|
|
9
|
+
|
|
10
|
+
// --- Helpers (private) ---
|
|
11
|
+
|
|
12
|
+
function tryExec(cmd) {
|
|
13
|
+
try {
|
|
14
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseVersion(str) {
|
|
21
|
+
const match = str && str.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
22
|
+
if (!match) return null;
|
|
23
|
+
return { major: +match[1], minor: +match[2], patch: +match[3] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function versionGte(v, min) {
|
|
27
|
+
if (v.major !== min.major) return v.major > min.major;
|
|
28
|
+
if (v.minor !== min.minor) return v.minor > min.minor;
|
|
29
|
+
return v.patch >= min.patch;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- Constants ---
|
|
33
|
+
|
|
34
|
+
const MIN_NODE = { major: 18, minor: 0, patch: 0 };
|
|
35
|
+
const MIN_CLAUDE = { major: 2, minor: 1, patch: 32 };
|
|
36
|
+
const VALID_TEAMMATE_MODES = ['tmux', 'in-process', 'auto'];
|
|
37
|
+
const REQUIRED_IDS = ['node', 'claude', 'jq'];
|
|
38
|
+
|
|
39
|
+
// --- Check functions (private) ---
|
|
40
|
+
|
|
41
|
+
function checkNode(nodeVersion) {
|
|
42
|
+
const v = nodeVersion || process.version;
|
|
43
|
+
const ver = parseVersion(v);
|
|
44
|
+
if (!ver) {
|
|
45
|
+
return {
|
|
46
|
+
id: 'node',
|
|
47
|
+
ok: false,
|
|
48
|
+
label: 'Node.js',
|
|
49
|
+
detail: `unknown version: ${v}`,
|
|
50
|
+
hint: 'Node.js >= 18 is required',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const ok = versionGte(ver, MIN_NODE);
|
|
54
|
+
const tag = `v${ver.major}.${ver.minor}.${ver.patch}`;
|
|
55
|
+
return {
|
|
56
|
+
id: 'node',
|
|
57
|
+
ok,
|
|
58
|
+
label: 'Node.js',
|
|
59
|
+
detail: ok
|
|
60
|
+
? `${tag} (>= ${MIN_NODE.major} required)`
|
|
61
|
+
: `${tag} — upgrade required (>= ${MIN_NODE.major})`,
|
|
62
|
+
hint: ok ? undefined : 'Upgrade Node.js to version 18 or later',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function checkClaude(exec) {
|
|
67
|
+
const raw = exec('claude --version');
|
|
68
|
+
if (!raw) {
|
|
69
|
+
return {
|
|
70
|
+
id: 'claude',
|
|
71
|
+
ok: false,
|
|
72
|
+
label: 'Claude Code',
|
|
73
|
+
detail: 'not found',
|
|
74
|
+
hint: 'Install: npm install -g @anthropic-ai/claude-code',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const ver = parseVersion(raw);
|
|
78
|
+
if (!ver) {
|
|
79
|
+
return {
|
|
80
|
+
id: 'claude',
|
|
81
|
+
ok: false,
|
|
82
|
+
label: 'Claude Code',
|
|
83
|
+
detail: `unknown version: ${raw}`,
|
|
84
|
+
hint: 'Expected semver from "claude --version"',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const ok = versionGte(ver, MIN_CLAUDE);
|
|
88
|
+
const tag = `v${ver.major}.${ver.minor}.${ver.patch}`;
|
|
89
|
+
return {
|
|
90
|
+
id: 'claude',
|
|
91
|
+
ok,
|
|
92
|
+
label: 'Claude Code',
|
|
93
|
+
detail: ok
|
|
94
|
+
? `${tag} (≥ ${MIN_CLAUDE.major}.${MIN_CLAUDE.minor}.${MIN_CLAUDE.patch} required)`
|
|
95
|
+
: `${tag} — update required (≥ ${MIN_CLAUDE.major}.${MIN_CLAUDE.minor}.${MIN_CLAUDE.patch})`,
|
|
96
|
+
hint: ok ? undefined : 'Update: npm update -g @anthropic-ai/claude-code',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkTeammateMode(homedir) {
|
|
101
|
+
const configPath = path.join(homedir, '.claude.json');
|
|
102
|
+
if (!fs.existsSync(configPath)) {
|
|
103
|
+
return {
|
|
104
|
+
id: 'teammate-mode',
|
|
105
|
+
ok: false,
|
|
106
|
+
label: 'teammateMode',
|
|
107
|
+
detail: 'not set — ~/.claude.json not found',
|
|
108
|
+
hint: 'Create ~/.claude.json with: { "teammateMode": "tmux" }',
|
|
109
|
+
fixable: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
let config;
|
|
113
|
+
try {
|
|
114
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
115
|
+
} catch {
|
|
116
|
+
return {
|
|
117
|
+
id: 'teammate-mode',
|
|
118
|
+
ok: false,
|
|
119
|
+
label: 'teammateMode',
|
|
120
|
+
detail: '~/.claude.json is not valid JSON',
|
|
121
|
+
hint: 'Fix the JSON syntax, then add: "teammateMode": "tmux"',
|
|
122
|
+
fixable: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (VALID_TEAMMATE_MODES.includes(config.teammateMode)) {
|
|
126
|
+
return {
|
|
127
|
+
id: 'teammate-mode',
|
|
128
|
+
ok: true,
|
|
129
|
+
label: 'teammateMode',
|
|
130
|
+
detail: config.teammateMode,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
id: 'teammate-mode',
|
|
135
|
+
ok: false,
|
|
136
|
+
label: 'teammateMode',
|
|
137
|
+
detail: config.teammateMode
|
|
138
|
+
? `set to "${config.teammateMode}" (expected one of: ${VALID_TEAMMATE_MODES.join(', ')})`
|
|
139
|
+
: 'not set in ~/.claude.json',
|
|
140
|
+
hint: `Add "teammateMode": "tmux" to ~/.claude.json`,
|
|
141
|
+
fixable: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function checkPlaywright(exec, cwd) {
|
|
146
|
+
// Try to read raid.json
|
|
147
|
+
let raidConfig = null;
|
|
148
|
+
if (cwd) {
|
|
149
|
+
const raidJsonPath = path.join(cwd, '.claude', 'raid.json');
|
|
150
|
+
try {
|
|
151
|
+
raidConfig = JSON.parse(fs.readFileSync(raidJsonPath, 'utf8'));
|
|
152
|
+
} catch {
|
|
153
|
+
// No raid.json or invalid — treat as browser not enabled
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const browserEnabled = raidConfig && raidConfig.browser && raidConfig.browser.enabled;
|
|
158
|
+
|
|
159
|
+
if (!browserEnabled) {
|
|
160
|
+
return {
|
|
161
|
+
id: 'playwright',
|
|
162
|
+
ok: true,
|
|
163
|
+
label: 'Playwright',
|
|
164
|
+
detail: 'not needed (browser testing disabled)',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Determine exec command prefix (e.g. "npx" or "pnpm dlx")
|
|
169
|
+
const execCommand = (raidConfig.project && raidConfig.project.execCommand) || 'npx';
|
|
170
|
+
|
|
171
|
+
// Check if playwright config file exists
|
|
172
|
+
const configFile = (raidConfig.browser && raidConfig.browser.playwrightConfig) || 'playwright.config.ts';
|
|
173
|
+
|
|
174
|
+
// Check playwright version
|
|
175
|
+
const raw = exec(`${execCommand} playwright --version`);
|
|
176
|
+
|
|
177
|
+
if (!raw) {
|
|
178
|
+
return {
|
|
179
|
+
id: 'playwright',
|
|
180
|
+
ok: false,
|
|
181
|
+
label: 'Playwright',
|
|
182
|
+
detail: 'not installed',
|
|
183
|
+
hint: `Install: ${execCommand} playwright install`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ver = parseVersion(raw);
|
|
188
|
+
const tag = ver ? `v${ver.major}.${ver.minor}.${ver.patch}` : raw.trim();
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
id: 'playwright',
|
|
192
|
+
ok: true,
|
|
193
|
+
label: 'Playwright',
|
|
194
|
+
detail: `installed (${tag})`,
|
|
195
|
+
hint: `Install: ${execCommand} playwright install`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function checkJq(exec) {
|
|
200
|
+
const found = exec('command -v jq');
|
|
201
|
+
if (found) {
|
|
202
|
+
return {
|
|
203
|
+
id: 'jq',
|
|
204
|
+
ok: true,
|
|
205
|
+
label: 'jq',
|
|
206
|
+
detail: 'installed',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
id: 'jq',
|
|
211
|
+
ok: false,
|
|
212
|
+
label: 'jq',
|
|
213
|
+
detail: 'not found',
|
|
214
|
+
hint: process.platform === 'darwin'
|
|
215
|
+
? 'Install: brew install jq'
|
|
216
|
+
: 'Install jq via your package manager (apt, dnf, brew, etc.)',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function checkPlatform(platform) {
|
|
221
|
+
if (platform === 'win32') {
|
|
222
|
+
return {
|
|
223
|
+
id: 'platform',
|
|
224
|
+
ok: false,
|
|
225
|
+
label: 'Platform',
|
|
226
|
+
detail: 'Windows is not supported — hooks require POSIX bash',
|
|
227
|
+
hint: 'Use WSL2 (Windows Subsystem for Linux) for full compatibility',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
id: 'platform',
|
|
232
|
+
ok: true,
|
|
233
|
+
label: 'Platform',
|
|
234
|
+
detail: platform,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function checkSplitPane(exec) {
|
|
239
|
+
const tmux = exec('command -v tmux');
|
|
240
|
+
const it2 = exec('command -v it2');
|
|
241
|
+
|
|
242
|
+
if (tmux || it2) {
|
|
243
|
+
const available = [tmux && 'tmux', it2 && 'it2'].filter(Boolean).join(', ');
|
|
244
|
+
return {
|
|
245
|
+
id: 'split-pane',
|
|
246
|
+
ok: true,
|
|
247
|
+
label: 'Split-pane',
|
|
248
|
+
detail: `available: ${available}`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
id: 'split-pane',
|
|
253
|
+
ok: false,
|
|
254
|
+
label: 'Split-pane',
|
|
255
|
+
detail: 'no split-pane tool found',
|
|
256
|
+
hint: process.platform === 'darwin'
|
|
257
|
+
? 'Install tmux: brew install tmux'
|
|
258
|
+
: 'Install tmux via your package manager (apt, dnf, brew, etc.)',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Interactive helpers (private) ---
|
|
263
|
+
|
|
264
|
+
const MODE_MENU = [
|
|
265
|
+
{ key: '1', value: 'tmux', desc: 'split panes, see all agents at once (requires tmux/iTerm2)' },
|
|
266
|
+
{ key: '2', value: 'in-process', desc: 'all in one terminal, cycle with Shift+Down' },
|
|
267
|
+
{ key: '3', value: 'auto', desc: 'split panes if available, otherwise in-process' },
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
function ask(question, stdin, stdout) {
|
|
271
|
+
return new Promise((resolve) => {
|
|
272
|
+
const rl = readline.createInterface({ input: stdin, output: stdout, terminal: false });
|
|
273
|
+
rl.question(question, (answer) => {
|
|
274
|
+
rl.close();
|
|
275
|
+
resolve((answer || '').trim());
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function writeTeammateMode(homedir, mode) {
|
|
281
|
+
const configPath = path.join(homedir, '.claude.json');
|
|
282
|
+
let config = {};
|
|
283
|
+
if (fs.existsSync(configPath)) {
|
|
284
|
+
try {
|
|
285
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
286
|
+
} catch {
|
|
287
|
+
config = {};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
config.teammateMode = mode;
|
|
291
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// --- Formatting helper (private) ---
|
|
295
|
+
|
|
296
|
+
function formatCheckLine(check) {
|
|
297
|
+
const icon = check.ok ? colors.green('✔') : colors.red('✖');
|
|
298
|
+
const lines = [` ${icon} ${check.label} ${check.detail}`];
|
|
299
|
+
if (check.hint) {
|
|
300
|
+
lines.push(` ${colors.dim('→')} ${colors.dim(check.hint)}`);
|
|
301
|
+
}
|
|
302
|
+
return lines;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// --- Exports ---
|
|
306
|
+
|
|
307
|
+
function runChecks(opts = {}) {
|
|
308
|
+
const homedir = opts.homedir || os.homedir();
|
|
309
|
+
const exec = opts.exec || tryExec;
|
|
310
|
+
const cwd = opts.cwd || undefined;
|
|
311
|
+
|
|
312
|
+
const nodeVersion = opts.nodeVersion || undefined;
|
|
313
|
+
|
|
314
|
+
const platform = opts.platform || process.platform;
|
|
315
|
+
|
|
316
|
+
const checks = [
|
|
317
|
+
checkPlatform(platform),
|
|
318
|
+
checkNode(nodeVersion),
|
|
319
|
+
checkClaude(exec),
|
|
320
|
+
checkJq(exec),
|
|
321
|
+
checkTeammateMode(homedir),
|
|
322
|
+
checkSplitPane(exec),
|
|
323
|
+
checkPlaywright(exec, cwd),
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
checks,
|
|
328
|
+
allOk: checks.filter(c => REQUIRED_IDS.includes(c.id)).every(c => c.ok),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function runSetup(opts = {}) {
|
|
333
|
+
const homedir = opts.homedir || os.homedir();
|
|
334
|
+
const exec = opts.exec || tryExec;
|
|
335
|
+
const stdin = opts.stdin || process.stdin;
|
|
336
|
+
const stdout = opts.stdout || process.stdout;
|
|
337
|
+
const actions = [];
|
|
338
|
+
|
|
339
|
+
let { checks, allOk } = runChecks({ homedir, exec });
|
|
340
|
+
|
|
341
|
+
const isInteractive = !!stdin.isTTY;
|
|
342
|
+
|
|
343
|
+
// Non-interactive: print all checks in a box and return
|
|
344
|
+
if (!isInteractive) {
|
|
345
|
+
const allLines = checks.flatMap(c => formatCheckLine(c));
|
|
346
|
+
stdout.write('\n' + box('Party Status', allLines) + '\n');
|
|
347
|
+
return { checks, allOk, actions: [] };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Interactive: print initial checks (not split-pane yet)
|
|
351
|
+
const initialChecks = checks.filter(c => c.id !== 'split-pane');
|
|
352
|
+
const initialLines = initialChecks.flatMap(c => formatCheckLine(c));
|
|
353
|
+
stdout.write('\n' + box('Party Status', initialLines) + '\n');
|
|
354
|
+
|
|
355
|
+
// Handle teammate-mode fix
|
|
356
|
+
const tmCheck = checks.find(c => c.id === 'teammate-mode');
|
|
357
|
+
let selectedMode = null;
|
|
358
|
+
|
|
359
|
+
if (!tmCheck.ok && tmCheck.fixable) {
|
|
360
|
+
stdout.write('\n Choose your formation:\n\n');
|
|
361
|
+
for (const item of MODE_MENU) {
|
|
362
|
+
stdout.write(` ${colors.amber(item.key + ')')} ${colors.bold(item.value.padEnd(12))} ${colors.dim(item.desc)}\n`);
|
|
363
|
+
}
|
|
364
|
+
const choice = await ask('\n Pick [1/2/3]: ', stdin, stdout);
|
|
365
|
+
const picked = MODE_MENU.find(m => m.key === choice);
|
|
366
|
+
if (picked) {
|
|
367
|
+
const confirm = await ask(` Write teammateMode: "${colors.bold(picked.value)}" to ~/.claude.json? [Y/n] `, stdin, stdout);
|
|
368
|
+
if (confirm.toLowerCase() !== 'n') {
|
|
369
|
+
writeTeammateMode(homedir, picked.value);
|
|
370
|
+
tmCheck.ok = true;
|
|
371
|
+
tmCheck.detail = picked.value;
|
|
372
|
+
delete tmCheck.hint;
|
|
373
|
+
delete tmCheck.fixable;
|
|
374
|
+
actions.push('teammate-mode');
|
|
375
|
+
selectedMode = picked.value;
|
|
376
|
+
stdout.write(' ' + colors.green('✔') + ' Updated ~/.claude.json\n');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} else if (tmCheck.ok) {
|
|
380
|
+
selectedMode = tmCheck.detail;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Handle split-pane check
|
|
384
|
+
const splitPane = checks.find(c => c.id === 'split-pane');
|
|
385
|
+
if (selectedMode === 'in-process') {
|
|
386
|
+
splitPane.ok = true;
|
|
387
|
+
splitPane.detail = 'not needed (in-process mode)';
|
|
388
|
+
delete splitPane.hint;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
stdout.write('\n ' + formatCheckLine(splitPane).join('\n ') + '\n');
|
|
392
|
+
|
|
393
|
+
// Recalculate allOk (required checks only: node + claude)
|
|
394
|
+
|
|
395
|
+
allOk = checks.filter(c => REQUIRED_IDS.includes(c.id)).every(c => c.ok);
|
|
396
|
+
|
|
397
|
+
if (checks.every(c => c.ok)) {
|
|
398
|
+
stdout.write('\n ' + colors.green('The party is assembled.') + ' Your quest awaits.\n');
|
|
399
|
+
stdout.write('\n claude --agent wizard\n');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { checks, allOk, actions };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
module.exports = { runChecks, VALID_TEAMMATE_MODES, runSetup };
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Color support detection (evaluated once at module load)
|
|
4
|
+
const noColor = ('NO_COLOR' in process.env) || !process.stdout.isTTY;
|
|
5
|
+
|
|
6
|
+
function wrap(code) {
|
|
7
|
+
if (noColor) return (str) => str;
|
|
8
|
+
return (str) => `\x1b[${code}m${str}\x1b[0m`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const colors = {
|
|
12
|
+
amber: wrap('33'),
|
|
13
|
+
green: wrap('32'),
|
|
14
|
+
red: wrap('31'),
|
|
15
|
+
dim: wrap('90'),
|
|
16
|
+
bold: wrap('1'),
|
|
17
|
+
boldAmber: wrap('1;33'),
|
|
18
|
+
boldRed: wrap('1;31'),
|
|
19
|
+
dimRed: wrap('2;31'),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function stripAnsi(str) {
|
|
23
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function banner() {
|
|
27
|
+
const { amber, boldAmber, boldRed, red, dimRed, dim } = colors;
|
|
28
|
+
const rule = amber(' ⚔ ═══════════════════════════════════════════════════════ ⚔');
|
|
29
|
+
|
|
30
|
+
const claudeArt = [
|
|
31
|
+
' ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗',
|
|
32
|
+
' ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝',
|
|
33
|
+
' ██║ ██║ ███████║██║ ██║██║ ██║█████╗ ',
|
|
34
|
+
' ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ ',
|
|
35
|
+
' ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗',
|
|
36
|
+
' ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const raidArt = [
|
|
40
|
+
' ██████╗ █████╗ ██╗██████╗ ',
|
|
41
|
+
' ██╔══██╗██╔══██╗██║██╔══██╗',
|
|
42
|
+
' ██████╔╝███████║██║██║ ██║',
|
|
43
|
+
' ██╔══██╗██╔══██║██║██║ ██║',
|
|
44
|
+
' ██║ ██║██║ ██║██║██████╔╝',
|
|
45
|
+
' ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═════╝ ',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// 5-tone vertical gradient: boldAmber -> amber -> boldRed -> red -> dimRed
|
|
49
|
+
// Lines 1-2 (CLAUDE top): boldAmber
|
|
50
|
+
// Lines 3-5 (CLAUDE bottom + transition): amber
|
|
51
|
+
// Line 6 (CLAUDE/RAID boundary): boldRed
|
|
52
|
+
// Lines 7-8 (RAID top): boldRed
|
|
53
|
+
// Lines 9-11 (RAID middle): red
|
|
54
|
+
// Line 12 (RAID bottom): dimRed
|
|
55
|
+
const gradientColors = [
|
|
56
|
+
boldAmber, boldAmber, // claudeArt[0-1]
|
|
57
|
+
amber, amber, amber, // claudeArt[2-4]
|
|
58
|
+
boldRed, // claudeArt[5]
|
|
59
|
+
boldRed, boldRed, // raidArt[0-1]
|
|
60
|
+
red, red, red, // raidArt[2-4]
|
|
61
|
+
dimRed, // raidArt[5]
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const allArt = [...claudeArt, ...raidArt];
|
|
65
|
+
const tagline = ' Adversarial multi-agent development for Claude Code';
|
|
66
|
+
|
|
67
|
+
const lines = [
|
|
68
|
+
'',
|
|
69
|
+
rule,
|
|
70
|
+
'',
|
|
71
|
+
...allArt.map((l, i) => gradientColors[i](l)),
|
|
72
|
+
'',
|
|
73
|
+
dim(tagline),
|
|
74
|
+
'',
|
|
75
|
+
rule,
|
|
76
|
+
'',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function box(title, contentLines) {
|
|
83
|
+
const { amber } = colors;
|
|
84
|
+
const PADDING = 2; // left padding inside box
|
|
85
|
+
const titleStr = ` \u2694 ${title} `;
|
|
86
|
+
|
|
87
|
+
// Calculate width: fit widest content + padding on both sides, or title
|
|
88
|
+
let maxContent = 0;
|
|
89
|
+
for (const line of contentLines) {
|
|
90
|
+
const w = stripAnsi(line).length;
|
|
91
|
+
if (w > maxContent) maxContent = w;
|
|
92
|
+
}
|
|
93
|
+
const innerWidth = Math.max(maxContent + PADDING * 2, titleStr.length + 4);
|
|
94
|
+
// Top border: ┌─── ⚔ Title ───...─┐
|
|
95
|
+
const titleDashesAfter = innerWidth - titleStr.length - 3;
|
|
96
|
+
const topBorder = amber('┌') + amber('───') + titleStr + amber('─'.repeat(Math.max(0, titleDashesAfter))) + amber('┐');
|
|
97
|
+
|
|
98
|
+
// Bottom border
|
|
99
|
+
const botBorder = amber('└') + amber('─'.repeat(innerWidth)) + amber('┘');
|
|
100
|
+
|
|
101
|
+
// Empty line
|
|
102
|
+
const emptyLine = amber('│') + ' '.repeat(innerWidth) + amber('│');
|
|
103
|
+
|
|
104
|
+
const lines = [topBorder, emptyLine];
|
|
105
|
+
for (const content of contentLines) {
|
|
106
|
+
const visLen = stripAnsi(content).length;
|
|
107
|
+
const rightPad = innerWidth - PADDING - visLen;
|
|
108
|
+
lines.push(amber('│') + ' '.repeat(PADDING) + content + ' '.repeat(Math.max(0, rightPad)) + amber('│'));
|
|
109
|
+
}
|
|
110
|
+
lines.push(emptyLine, botBorder);
|
|
111
|
+
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function header(text) {
|
|
116
|
+
const { amber, bold } = colors;
|
|
117
|
+
return ` ${amber(bold(`\u2694 ${text}`))}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function referenceCard() {
|
|
121
|
+
const howItWorks = box('How It Works', [
|
|
122
|
+
' You describe a task. The Wizard assesses complexity and',
|
|
123
|
+
' recommends a mode:',
|
|
124
|
+
'',
|
|
125
|
+
' ' + colors.bold('Full Raid') + ' 3 agents attack from competing angles',
|
|
126
|
+
' ' + colors.bold('Skirmish') + ' 2 agents, lighter process',
|
|
127
|
+
' ' + colors.bold('Scout') + ' 1 agent + Wizard review',
|
|
128
|
+
'',
|
|
129
|
+
' Every task flows through 4 phases:',
|
|
130
|
+
'',
|
|
131
|
+
' 1. ' + colors.bold('Design') + ' Agents explore and challenge the approach',
|
|
132
|
+
' 2. ' + colors.bold('Plan') + ' Agents decompose into testable tasks',
|
|
133
|
+
' 3. ' + colors.bold('Implement') + ' One builds (TDD), others attack',
|
|
134
|
+
' 4. ' + colors.bold('Review') + ' Independent reviews, fight over findings',
|
|
135
|
+
'',
|
|
136
|
+
' Hooks enforce discipline automatically:',
|
|
137
|
+
' ' + colors.dim('\u2022') + ' No implementation without a design doc',
|
|
138
|
+
' ' + colors.dim('\u2022') + ' No commits without passing tests',
|
|
139
|
+
' ' + colors.dim('\u2022') + ' No completion claims without fresh test evidence',
|
|
140
|
+
' ' + colors.dim('\u2022') + ' Conventional commit messages required',
|
|
141
|
+
'',
|
|
142
|
+
' ' + colors.dim('Hooks only activate during Raid sessions \u2014 they won\'t'),
|
|
143
|
+
' ' + colors.dim('interfere with normal coding outside of a Raid.'),
|
|
144
|
+
'',
|
|
145
|
+
' Config: ' + colors.bold('.claude/raid.json') + ' ' + colors.dim('project settings'),
|
|
146
|
+
' Rules: ' + colors.bold('.claude/raid-rules.md') + ' ' + colors.dim('editable team rules'),
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
const nextStep = box('Next Step', [
|
|
150
|
+
' ' + colors.bold('claude --agent wizard'),
|
|
151
|
+
'',
|
|
152
|
+
' Describe your task and the Wizard takes over.',
|
|
153
|
+
' ' + colors.dim('Tip: start with a small task (bugfix, config change) to'),
|
|
154
|
+
' ' + colors.dim('see the workflow before tackling something complex.'),
|
|
155
|
+
'',
|
|
156
|
+
' ' + colors.bold('Controls'),
|
|
157
|
+
' ' + colors.bold('Shift+Down') + ' Cycle through teammates',
|
|
158
|
+
' ' + colors.bold('Enter') + ' View a teammate\'s session',
|
|
159
|
+
' ' + colors.bold('Escape') + ' Interrupt a teammate\'s turn',
|
|
160
|
+
' ' + colors.bold('Ctrl+T') + ' Toggle the shared task list',
|
|
161
|
+
'',
|
|
162
|
+
' Review this anytime: ' + colors.bold('claude-raid heal'),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
return howItWorks + '\n' + nextStep;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { colors, banner, box, header, stripAnsi, referenceCard };
|
package/src/update.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { mergeSettings } = require('./merge-settings');
|
|
6
|
+
const { detectProject } = require('./detect-project');
|
|
7
|
+
const { banner, header, colors } = require('./ui');
|
|
6
8
|
|
|
7
9
|
const TEMPLATE_DIR = path.join(__dirname, '..', 'template', '.claude');
|
|
8
10
|
|
|
@@ -34,7 +36,7 @@ function performUpdate(cwd) {
|
|
|
34
36
|
const skippedAgents = [];
|
|
35
37
|
|
|
36
38
|
if (!fs.existsSync(path.join(claudeDir, 'raid-rules.md'))) {
|
|
37
|
-
return { success: false, message: '
|
|
39
|
+
return { success: false, message: 'No party found. Run `claude-raid summon` first.', skippedAgents };
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
// Update agents — skip if user has customized them
|
|
@@ -84,9 +86,53 @@ function performUpdate(cwd) {
|
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
// Migrate existing raid.json — add missing browser/packageManager fields
|
|
90
|
+
const raidConfigPath = path.join(claudeDir, 'raid.json');
|
|
91
|
+
const migratedFields = [];
|
|
92
|
+
if (fs.existsSync(raidConfigPath)) {
|
|
93
|
+
let config;
|
|
94
|
+
try {
|
|
95
|
+
config = JSON.parse(fs.readFileSync(raidConfigPath, 'utf8'));
|
|
96
|
+
} catch {
|
|
97
|
+
config = null;
|
|
98
|
+
}
|
|
99
|
+
if (config !== null) {
|
|
100
|
+
const detected = detectProject(cwd);
|
|
101
|
+
// Add packageManager fields if missing
|
|
102
|
+
if (detected.packageManager && config.project && !config.project.packageManager) {
|
|
103
|
+
config.project.packageManager = detected.packageManager;
|
|
104
|
+
if (detected.runCommand) config.project.runCommand = detected.runCommand;
|
|
105
|
+
if (detected.execCommand) config.project.execCommand = detected.execCommand;
|
|
106
|
+
if (detected.installCommand) config.project.installCommand = detected.installCommand;
|
|
107
|
+
migratedFields.push('packageManager');
|
|
108
|
+
}
|
|
109
|
+
// Add browser section if detected and missing
|
|
110
|
+
if (detected.browser && !config.browser) {
|
|
111
|
+
config.browser = {
|
|
112
|
+
enabled: true,
|
|
113
|
+
framework: detected.browser.framework,
|
|
114
|
+
devCommand: detected.browser.devCommand,
|
|
115
|
+
baseUrl: `http://localhost:${detected.browser.defaultPort}`,
|
|
116
|
+
defaultPort: detected.browser.defaultPort,
|
|
117
|
+
portRange: [detected.browser.defaultPort + 1, detected.browser.defaultPort + 5],
|
|
118
|
+
playwrightConfig: 'playwright.config.ts',
|
|
119
|
+
auth: null,
|
|
120
|
+
startup: null,
|
|
121
|
+
};
|
|
122
|
+
migratedFields.push('browser');
|
|
123
|
+
}
|
|
124
|
+
if (migratedFields.length > 0) {
|
|
125
|
+
fs.writeFileSync(raidConfigPath, JSON.stringify(config, null, 2) + '\n');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
87
130
|
mergeSettings(cwd);
|
|
88
131
|
|
|
89
132
|
let message = 'The Raid has been updated to the latest version.';
|
|
133
|
+
if (migratedFields.length > 0) {
|
|
134
|
+
message += `\nMigrated raid.json: added ${migratedFields.join(', ')}`;
|
|
135
|
+
}
|
|
90
136
|
if (skippedAgents.length > 0) {
|
|
91
137
|
message += `\nSkipped customized agents: ${skippedAgents.join(', ')}`;
|
|
92
138
|
}
|
|
@@ -94,17 +140,28 @@ function performUpdate(cwd) {
|
|
|
94
140
|
message += '\nSkipped customized raid-rules.md';
|
|
95
141
|
}
|
|
96
142
|
if (skippedAgents.length > 0 || skippedRules) {
|
|
97
|
-
message += '\nUse `claude-raid
|
|
143
|
+
message += '\nUse `claude-raid dismantle` then `claude-raid summon` to reset.';
|
|
98
144
|
}
|
|
99
145
|
|
|
100
|
-
return { success: true, message, skippedAgents };
|
|
146
|
+
return { success: true, message, skippedAgents, migratedFields };
|
|
101
147
|
}
|
|
102
148
|
|
|
103
149
|
function run() {
|
|
104
150
|
const cwd = process.cwd();
|
|
105
|
-
console.log('\
|
|
151
|
+
console.log('\n' + banner());
|
|
152
|
+
console.log(header('Reforging the Arsenal...') + '\n');
|
|
153
|
+
|
|
106
154
|
const result = performUpdate(cwd);
|
|
107
|
-
|
|
155
|
+
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
console.log(' ' + colors.red('✖') + ' No party found. Run ' + colors.bold('claude-raid summon') + ' first.');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(' ' + colors.green('✔') + ' The party\'s arsenal has been reforged.');
|
|
162
|
+
if (result.skippedAgents.length > 0) {
|
|
163
|
+
console.log(' ' + colors.dim('Preserved customized warriors: ' + result.skippedAgents.join(', ')));
|
|
164
|
+
}
|
|
108
165
|
}
|
|
109
166
|
|
|
110
167
|
module.exports = { performUpdate, run };
|